HTML overlay
Status: O6a Runtime built, in-game test pending. O1 to O5 have all passed; the O4 scene threshold is still open (see the O4 result). O6a runs Node mods in the host beside overlay apps, with one rednode package for both. To build an app today, see the Overlay apps guide. The proof of concept passed in game, alone and side by side with CET and 10 other RED4ext plugins (2026-10-11). See Test results and the plan.
REDnode overlay apps are small web apps (HTML, CSS and JavaScript) drawn on top of the game, like CET’s ImGui windows. They talk to the game through the same rednode API as Node mods.
Why HTML
| CET + ImGui | REDnode HTML overlay | |
|---|---|---|
| Language | Lua | JavaScript or TypeScript |
| UI model | Immediate mode: the UI is redrawn by code every frame | Retained: a normal web page, any web framework |
| Look | Developer tool | Anything CSS can do, including the REDnode HUD style |
| Where app code runs | Inside the game, on the game thread | Outside the game, in the overlay host |
| Game calls | Synchronous | Asynchronous, at least one frame per round trip |
| Tooling | CET console | Browser dev tools, hot reload, npm packages |
Architecture
Overlay host (Electron, out of process) Game process (REDnode plugin)
┌──────────────────────────────────────┐ ┌───────────────────────────────────┐
│ Node: rednode API, Tier 2 mods │ │ Present hook: draw overlay quad │
│ Chromium, off-screen: renders apps │ frames → │ Window hook: hotkey, mouse, keys │
│ to a BGRA image │ ← input │ Game thread: run bridge requests │
│ Input injection into the page │ requests→│ through RTTI │
│ Bridge: window.rednode in each app │ ←results │ Events from scripts (Tier 1 path) │
└──────────────────────────────────────┘ events └───────────────────────────────────┘
one named pipe: \\.\pipe\rednode-overlay
| Part | Where | Job |
|---|---|---|
| Overlay host | Out of process | An Electron app. Chromium renders each app off-screen. Node holds the bridge. Crashes here cannot take the game down |
| Compositor | In the game | Hooks IDXGISwapChain::Present through the swapchain’s vtable and draws the latest frame as a textured quad, using the game’s own D3D12 device and command queue from the RED4ext SDK (GpuApi::GetDeviceData()) |
| Input | In the game | Subclasses the game window. A hotkey toggles interactive mode. In that mode mouse and keyboard go to the overlay, not the game |
| Bridge | Both | JSON requests from apps run on the game thread, between frames, through RTTI calls. Results and game events go back the same way |
Synchronous and asynchronous kernels
Chromium always runs page JavaScript in its own renderer process. So an overlay app can never be part of the game’s synchronous path.
| Kernel | What an overlay app can do |
|---|---|
| Tier 2 (async) | Everything. Events in, commands and queries out, as promises |
| Tier 1 (sync) | Configure it: set interceptor parameters, toggle declarative rules. Changes apply from the next frame. The synchronous code itself stays native or declarative |
Proof of concept
Goal: decide whether the approach is worth building out. Each step is a separate thing to check in game.
| Step | Proves | How to check |
|---|---|---|
| 1. Compositor | REDnode can draw over the game with the game’s own D3D12 queue | A panel is visible in the main menu and in gameplay |
| 2. Live HTML | An Electron off-screen page reaches the game as frames | The demo app’s clock ticks and its animation moves |
| 3. Input | Mouse and keyboard reach the page, and the game ignores them in interactive mode | F10, then click a button, type in a text box. The camera should not move |
| 4. Bridge | A page can call into the game and get an answer | “Read player position” shows V’s coordinates, and they change after walking |
Running the test
The overlay needs REDnode 0.3.0 in the game and the host running on the desktop. Setup is in Dev setup, section 10.
- Start the host in a terminal and leave it running:
npm run overlay. It printsWaiting for the game. - Launch the game. At the main menu a panel appears in the middle of the screen. Without the host it is a dark placeholder with a yellow frame and a moving cyan line. That alone means step 1 works.
- With the host running, the panel shows the demo app, and the terminal prints
Connected to the game. The clock should tick (step 2). - Press F10 for interactive mode. A yellow cursor appears in the panel. Click Click me, click the text box and type (step 3). Press Esc to give input back to the game.
- Load a save. The events tile shows
PlayerPuppet.OnGameAttached. In interactive mode, click Read player position, walk a few steps, and read it again (step 4). - Press F10 twice more to hide the panel, and once more to show it again.
- Quit to desktop through the menu.
Note what you see at each step, plus any stutter, flicker, wrong colors, or input that leaked to the game. The plugin’s log lines start with Overlay: in red4ext/logs/rednode-*.log.
Simplifications
Simplifications in the proof of concept, all to revisit if it goes further:
- CPU frame copy. Frames travel through the pipe as raw BGRA pixels and are uploaded each time the page repaints. The real version would share a GPU texture with Chromium.
- One fixed panel of 1024 x 640 pixels, scaled with the screen height. No window management.
- One app, loaded by the host at start.
- The plugin draws the cursor in its shader, from the position it tracks. Off-screen Chromium does not paint one, and a page-drawn cursor lags (see the test results).
- HDR: the shader converts for scRGB and PQ output, but the brightness level is a guess.
Test results
Run 1: 2026-10-11, clean setup
| Item | Value |
|---|---|
| Game | 2.31, Vortex vanilla profile: RED4ext, redscript and REDnode 0.3.0 only. No CET |
| Display | 3840 x 2160 swapchain, format 28 (R8G8B8A8_UNORM), SDR. The panel is drawn at 2x |
| Host | Electron 44.7.0, 30 frames a second |
| Step | Result |
|---|---|
| 1. Compositor | Passed. The panel was drawn at the main menu and in gameplay |
| 2. Live HTML | Passed. The clock ticked and the bar moved |
| 3. Input | Passed, with two usability issues below. Clicks and typing reached the page |
| 4. Bridge | Passed. player.position returned V’s position. OnGameAttached events reached the page at the main menu and on load |
| F10 modes | Passed. Three full cycles of passive, interactive and hidden |
| Logs | No errors or warnings. redscript compiled cleanly |
| Issue | Cause | Fix (confirmed in run 2) |
|---|---|---|
| The cursor lagged | The page drew it. Each move went through the pipe, a Chromium repaint at 30 fps and a 2.6 MB frame back | The plugin draws the cursor in its shader. The host now runs at 60 fps |
| No text caret in the text box | Off-screen Chromium did not treat the page as focused. Typing still worked | The host calls focusOnWebView() in interactive mode. It focuses the page without taking OS focus from the game. webContents.focus() did not work, and showing the window would take OS focus |
Run 2: 2026-10-11, with CET and other mods
| Item | Value |
|---|---|
| Game | 2.31, the user’s modded profile: CET, redscript, REDnode 0.3.0 and 10 other RED4ext plugins (ArchiveXL, Codeware, TweakXL, audioware, Mod Settings and more) |
| Display | Same as run 1: 3840 x 2160, SDR |
| Host | Electron 44.7.0, 60 frames a second |
| Check | Result |
|---|---|
| All four steps | Passed |
| Frame rate | Good. No visible drop with the panel streaming at 60 fps at 4K |
| Cursor | Fixed. Drawn by the plugin, visible and with no lag |
| Text caret | Fixed. The text box shows its focus border and a blinking caret |
| REDnode and CET overlays drawn together | Passed. Both visible at once |
| Input routing | Passed. Input went to whichever overlay was active, and not to the game |
| Stability | No flicker, stutter or crash reported. No errors in the REDnode, RED4ext, redscript or CET logs. CET logged its own D3D12 and window hooks as successful |
Observation: CET windows can be dragged. The REDnode panel cannot, because the proof of concept has one fixed panel. Window management belongs in the real version.
Risks and open questions
| Risk | Status |
|---|---|
Coexisting with CET, which also hooks Present and the window procedure |
Worked in run 2. REDnode patches the vtable entry and chains to whatever was there, so CET’s code hook still runs |
| Mouse position while the game hides and locks the cursor | Handled by summing raw mouse movement into a virtual cursor. Worked in run 1 |
| Blocking game input | Worked in runs 1 and 2, with and without CET |
| Memory | Electron adds a few hundred MB. Not measured yet |
| Frame pacing | Uploading a frame costs GPU time on the present path. Only done when the page repaints |
| Upscalers and frame generation | Unverified. The overlay is drawn on the final back buffer, after them |
| Scene detection threshold | SceneTier >= 4 counts as a scene. Unverified; test on a fresh playthrough (see the O4 result) |
Decision after the test
| Result | Next step |
|---|---|
| All four steps work, no stutter | Plan the real version: shared GPU textures, multiple apps and windows, an app manifest, and folding the host into the Tier 2 runtime |
| Drawing works, input does not | Look at how CET blocks input, the deferred CET analysis |
| Drawing does not work | Fall back to the game’s own ink UI, driven by the same bridge |
The result was the first row.
Plan for the real version
Status: draft for review. Phases are numbered O1 to O7 so they do not clash with the project phases P0 to P4. Each phase ends with an in-game test, like the proof of concept.
Goal
Overlay apps that can look like part of the game: borderless, any shape, transparent where the app is transparent, either interactive windows or HUD widgets that never take input. They are written as web pages and use the same rednode API as Node mods.
Window model
The plugin owns window placement, so moving a window never waits on Chromium. The host owns rendering, one off-screen page per window.
| Property | Values | Notes |
|---|---|---|
kind |
window or hud |
A hud window is drawn but never focused and never takes input |
frame |
none (default) |
REDnode draws nothing but the page. A title bar with close and drag comes from the UI kit’s <rn-window>, not from the plugin |
size |
page pixels | Scaled with the screen height, like the proof of concept. Later also with the game’s UI scale |
anchor |
center, top-left and so on, plus an offset |
Lets HUD widgets sit at a screen edge at any resolution and aspect ratio |
draggable |
yes or no | Drag handles are marked in the page’s HTML, for example data-rednode-drag |
hideWith |
game UI states | For example menu, photo-mode, scene. The plugin hides the window while that state is on |
Rules that make borderless windows behave:
| Rule | Why |
|---|---|
| Click-through on transparent pixels. A click where the page’s alpha is zero goes to the window below, or to the game | A borderless window’s invisible rectangle must not block anything |
| Focus follows the click. Only the focused window gets keyboard input. Clicking the game area gives input back to the game | Matches CET and desktop behavior |
| Positions are saved per app | Windows come back where the player left them |
Phases
| Phase | Builds | In-game test proves |
|---|---|---|
| O1 Windows | Several windows, each with its own texture and placement. Drag handles, focus, z-order, click-through on transparent pixels, saved positions. Drag runs in the plugin, at the game’s frame rate | Two demo windows can be dragged, overlapped and focused. Clicks pass through transparent parts |
| O2 UI kit | @rednode/ui: web components with the rednode and game themes, keyboard and gamepad navigation, UI sounds through the game. See UI kit |
A titled window with widgets, and a borderless native-style button, both built from markup only |
| O3 Apps | App folders with a rednode-app.json manifest (id, name, entry page, kind, size, anchor). The host loads every app it finds. A launcher app, built with the UI kit, opens and closes the others. Player install: the host packaged as a standalone app and started by the plugin (see Player install) |
A packaged build copied into the game folder starts on its own with the game, and a third-party style app in its own folder loads without changing the host |
| O4 HUD widgets | The hud kind, anchors, and hideWith, driven by game UI state events from the bridge. Research which game state shows menus, photo mode and scenes |
A HUD widget sits at a screen edge, never takes input, and hides in menus and photo mode |
| O5 Performance | Shared GPU textures: Chromium renders to a D3D11 texture that the plugin opens in D3D12, so no pixel copy through the pipe. Updates only for changed regions. Measure memory and GPU cost | Several animated windows at 4K with no visible frame drop. Numbers recorded |
| O6 One runtime | The overlay host becomes the Tier 2 runtime: Node mods and overlay apps in one process, one rednode package for both. Bridge methods grow from the two demo calls into the real API, generated from the game API data. Node mods get the UI builder |
A Node mod and an overlay app use the same API call and get the same answer. A Node mod opens a window without writing HTML |
| O7 Display | HDR brightness calibration against the game’s own HUD, DLSS frame generation, resolution and window-mode changes, ultrawide | The overlay looks right and stays stable across display modes |
O1 Windows: test
Built 2026-10-11, REDnode 0.4.0. Window logic is covered by 19 unit tests in native/tests/windows_test.cpp, run by tools/build-native.ps1. The host was tested against a mock of the plugin. Two apps open: the demo (1024 x 640, title bar is its drag handle) and Signal (a round 320 x 320 widget with a transparent middle and a yellow drag tab).
- Start the host (
npm run overlay) and the game. Both windows appear at the main menu. - F10 for interactive mode. Drag the demo by its title bar and Signal by its yellow tab. They should follow the cursor with no lag.
- Overlap them. Click one: it comes to the front, and the other one’s title dims.
- Put Signal over the demo. Click inside Signal’s transparent middle: the click should reach the demo underneath, for example its Click me button.
- Click Ping on Signal: it shows the game frame.
- Click empty screen, away from both windows: interactive mode ends and the game gets input back.
- Quit the game and the host, start both again: the windows come back where you left them.
Result, 2026-10-11: passed. Modded profile with CET, 3840 x 2160 SDR. Every step worked: dragging, front-to-back order and focus, click-through on Signal’s transparent middle, Ping, leaving interactive mode by clicking empty screen, and saved positions across a restart of the game and the host. No errors in the logs.
O2 UI kit: test
Built 2026-10-11, REDnode 0.5.0. The kit is packages/ui: 12 components as markup, two themes, keyboard navigation, and UI sounds through the new bridge call audio.play. Both test apps were checked against a mock of the plugin. Gamepad navigation is left for later, because the game also reads the controller.
The host now opens two apps: Kit, a titled window with every widget, and Native, a borderless game-style button at the lower right.
- Start the host and the game. Kit and the red Open stash button appear.
- F10. In Kit, try each tab: press buttons, flip toggles, drag the slider, type a call sign and press Enter, pick list and menu items. Hover Hover me for a tooltip.
- Flip Game theme in Kit’s title bar: the window switches to the game look.
- Use the keyboard: Tab between widgets, arrows in the list and menu, Enter to choose.
- Click Open stash, pick an item: a game-style notification appears. Drag the button by its striped grip.
- Kit’s Sound tab: pick a name and press Play. Note which names make a sound; the names are unverified.
Result, 2026-10-11: passed. Modded profile with CET, 3840 x 2160 SDR. All widgets, both themes, keyboard use, the native-style button and its notifications worked. Game UI sounds played through audio.play, so AudioSystem.Play with no entity works for overlay sounds. No errors in the logs.
O3 Apps: test
Built 2026-10-11, REDnode 0.6.0. Apps are folders with a rednode-app.json manifest. The launcher, built into the host, lists them and opens or closes them. The host is packaged as REDnode-overlay.exe and the plugin starts it with the game, in a job object so it never outlives the game. Tested against a mock of the plugin: app discovery, anchors, the launcher, opening an app from it, saved choices, the log file and the single-instance lock. Window anchoring has 5 new unit tests.
This is the player path: no npm run overlay.
- Close any host you started by hand. In the game folder, REDnode now has
host/(the packaged host) andapps/(Kit, Native, Demo, Signal). - Launch the game normally. Do not start anything else. At the main menu: Kit, the Open stash button, and a small REDNODE · 2/4 tab at the lower left appear on their own.
- F10, then click the REDNODE tab. The launcher lists the four apps with their versions and descriptions.
- Switch Signal and Demo on: they open. Switch Kit off: it closes.
- Quit the game through the menu. The host closes with it:
REDnode-overlay.exeis no longer in Task Manager. - Launch the game again. The apps you left on are on, and the ones you switched off stay off.
Logs: the host writes red4ext/logs/rednode-overlay.log in the game folder.
Result, 2026-10-11: passed. Modded profile with CET. The game started the packaged host by itself (the plugin logged started the host); the host found the 5 apps and opened the defaults. The launcher opened Signal and Demo and closed Kit, and the choices were saved. The host closed with the game. The user confirmed the saved choices on the next launch.
O4 HUD widgets: test
Built 2026-10-11, REDnode 0.7.0. Apps can be "kind": "hud": drawn, but never focused and never in the way of a click. "hideWith" hides any window while the game shows a menu, photo mode, a scene or the main menu. The plugin reads those states on the game thread a few times a second and hides windows itself, so there is no delay through the host. Apps also get a ui-state event. 4 new unit tests cover HUD input and hiding.
| State | Read from |
|---|---|
menu |
The UI_System blackboard, IsInMenu |
photo-mode |
PhotoModeSystem.IsPhotoModeActive() |
scene |
The player’s PlayerStateMachine blackboard, SceneTier 4 or higher. The threshold is unverified: this test calibrates it |
main-menu |
No player: the main menu and loading screens |
Two new apps open: Coords HUD, a strip at the top of the screen with V’s position, and Game state, which shows what the plugin reads.
- Launch the game. At the main menu, Coords HUD is not shown (hidden with
main-menu); Game state shows Main menu / loading: yes. - Load a save. Coords HUD appears at the top and its numbers change as you walk.
- F10, then click on the Coords HUD: the click goes through it (here, to empty screen, which ends interactive mode).
- Open the inventory or map, then the pause menu: Coords HUD hides, Game state shows Menu open: yes.
- Enter photo mode: Coords HUD hides.
- Talk to an NPC, and if you can, trigger a cutscene. Note the scene tier Game state shows in each, and whether Coords HUD hid when you think it should have.
- F10 to hidden mode: everything hides, HUD included.
Result, 2026-10-11: passed, with one open item. Modded profile with CET. The HUD was hidden at the main menu, showed in gameplay with live coordinates, let clicks through, and hid with the inventory, map, pause menu and photo mode. It stayed visible while talking to NPCs, as intended: conversations are normal play, with the dialogue choices shown in the world. Hidden mode hid everything.
Open: the scene threshold. A conversation with an NPC is scene tier 2, so the threshold must be above 2, and at 4 the HUD correctly stays up in conversations. No cutscene could be triggered in this save, so whether cutscenes are tier 4 or 5, and what tier 3 is, is still unverified. Test it on a fresh playthrough, where the opening has several scenes: watch the scene tier in the Game state app and check that the HUD hides in cutscenes but not in normal conversations.
O5 Performance: test
Built 2026-10-11, REDnode 0.8.0. Three changes, each measurable in the new Perf app:
| Change | What it does | Checked before the game |
|---|---|---|
| Measurement | The plugin times its own CPU work and its GPU work (timestamp queries) on every game frame, and counts the game’s frame rate. The host adds its memory and CPU use. Reported once a second as a perf event |
Builds; the Perf app shows the numbers |
| Changed areas | On the CPU path only the changed rectangle of a window is sent and uploaded | 5 unit tests; with normal apps the mock saw 20% less data. Animated apps change almost everything, so the saving is small for them |
| GPU sharing | Chromium renders into a shared GPU texture; the plugin copies it on the GPU, with no pixels through the pipe. Click-through reads each window’s alpha back from the GPU up to 5 times a second. On any failure the host falls back to the CPU path | Electron 44 streamed shared textures at about 56 fps on this machine; against the mock, handles flowed and were released, and the fallback switched paths. The GPU copy itself is untested until the game runs it |
The launcher has a GPU sharing switch at the bottom to compare the two paths. The host’s memory is about 60 to 100 MB per open window, because every window is its own Chromium process: 786 MB with 6 windows in the mock run.
- Launch the game. The Perf window is at the top right. It should say Render path: GPU sharing, with GPU-shared frames counting up.
- Check that every window looks right: no garbage, flicker, tearing or stale frames. Switch Signal on in the launcher: its ring should animate smoothly.
- In interactive mode, click through Signal’s transparent middle: click-through must still work on the GPU path.
- Switch Stress on. Note the game’s frame rate, the overlay’s CPU and GPU times, and the host’s memory and CPU.
- Switch GPU sharing off at the bottom of the launcher. The windows reopen on the CPU path. Note the same numbers with Stress still on.
- Switch GPU sharing back on and Stress off.
Logs: the plugin logs GPU sharing failed if it had to give up, and the host logs Released ... shared frame(s) the game did not answer if frames were not acknowledged.
Result, 2026-10-12: passed. Modded profile with CET, 3840 x 2160 SDR, REDnode 0.8.0. GPU sharing ran the whole session without falling back. All windows looked right, click-through worked through Signal’s transparent middle on the GPU path, and the GPU sharing switch reopened every window on the other path and back. The host logged a perf line every 5 seconds; these are the 30-second stretches with Stress open:
| With Stress open | GPU sharing | CPU path |
|---|---|---|
| Game frame rate (gameplay) | 29 to 31 fps | 29 to 31 fps |
| Overlay CPU time per game frame | about 1.0 ms (worst 1.7 to 2.5) | about 1.0 ms (worst 1.2 to 1.6) |
| Overlay GPU time per game frame | 0.06 to 0.10 ms | 0.03 ms |
| Pixels uploaded in the game | 0 MB/s | 55 MB/s |
| Sent through the pipe | 0 MB/s | 110 MB/s |
| Host memory | about 910 MB | about 1,080 MB |
| Host CPU | 1% | 2% |
| Finding | Notes |
|---|---|
| GPU sharing removes the pixel traffic | 110 MB/s through the pipe and 55 MB/s of uploads go to zero, and the host uses about 170 MB less memory. This is what lets many animated apps run together |
| The overlay’s GPU cost is tiny | Under 0.1 ms per frame on both paths |
| No visible effect on the game’s frame rate | The game ran at about 30 fps in gameplay on both paths, which is too slow to show a cost of about 1 ms. Measure again in a lighter scene |
| Overlay CPU time in gameplay | About 0.7 to 1.0 ms per game frame even without Stress, against about 0.15 ms at the main menu. 3% of a frame at 30 fps, about 14% at 144 fps. A likely cause is opening every shared texture handle again each frame. To fix in O6 and check in its test |
| Unanswered shared frames at startup and while hidden | The plugin skips its work before Present is hooked and in hidden mode, so the host’s one-second release frees those textures. The plugin could acknowledge them at once. To fix in O6 and check in its test |
| Focus bug found and fixed | Clicking the window that had focus before leaving interactive mode did not refocus it (seen with Signal). Fixed in 2b94a6b with two unit tests, confirmed in the retest |
O1 to O4 make the feature usable. O5 makes it cheap. O6 joins it to the rest of REDnode. O7 hardens it.
Order agreed on 2026-10-12: REDclaude, an in-game Claude assistant built as an overlay app, comes right after O6. O7 follows it.
O6 One runtime: plan
Agreed 2026-10-12. Node mods and overlay apps run in the same host, use the same rednode package and get the same answers. This also covers most of P2 (the pipe bridge and the Node SDK), and builds what REDclaude needs: live game queries, mod logs and hot reload. O6 has two test gates, O6a and O6b.
O6a Runtime
| Piece | How |
|---|---|
| Node mods in the host | A Node mod runs in an Electron utilityProcess: full Node and npm, outside the game and outside the host’s main process |
| Process per mod, or a shared group | By default each mod gets its own process. The player can put mods in a shared group to save memory: one process, each mod in its own worker thread. A mod author can require its own process with "isolation": "process" |
| Manifest | rednode-app.json gets "main" for Node code, beside "entry" for HTML. A package can have both |
One rednode package |
The same game API in both places. Mods import { game } from "rednode". Apps import it from rednode://sdk/index.js, through an import map so the import line is the same |
| Events | game.on covers the events the plugin already sees: session attach (main menu and save load) and UI state. Events from hooked game functions stay in P2 |
| UI builder | ui.window({ title, children: [ui.button("Apply", onClick)] }) from a Node mod. The host renders it with the UI kit; clicks and changes go back to the mod |
| Mod logs | console.log from a mod goes to red4ext/logs/rednode-mods.log, tagged with the mod id. The launcher shows a mod’s errors |
| Hot reload | When a mod’s files change, the host restarts that mod and reopens its windows. Always on for developers, a launcher switch for players |
| Launcher | Lists mods beside apps: on and off, the shared-process switch, the state (running, crashed, not responding) and a restart button |
| O5 follow-ups | Acknowledge shared frames at once while a window is not drawn. Split the overlay CPU time into its parts (the GPU fence wait, opening shared handles, the rest) and reuse opened shared textures |
O6b Game API
| Piece | How |
|---|---|
| One generic call | rtti.call calls any global function, static function or method by name, after checking the arguments against the game’s type data, on the game thread between frames |
| Values | bool, numbers, CName, String, enums by name, TweakDBID, structs, arrays and Vector4 convert to and from JSON. Other types give an error that names the type |
| Object references | Game objects come back as { $ref, $type }. The plugin holds them weakly; all are cleared on each session attach |
| Generated types | TypeScript types generated from gameapi/2.31/rtti.json (15,830 classes, about 52,000 functions), so calls are typed. Size and editor speed measured first |
| Hand-written calls | player.position and audio.play become thin wrappers over rtti.call |
| Risk | Note |
|---|---|
| Calling any game function | Type checks catch wrong types, not wrong values. A blocklist of dangerous functions may be needed |
| Object lifetime | Weak references, cleared on each session attach (unverified that this is enough) |
| Generated types size | Measured before deciding how to split them |
| Memory per mod | Measured in the O6a test, for a process per mod and for a shared group |
O6a Runtime: test
Built 2026-10-12: plugin 0.9.0, host 0.2.0, rednode 0.1.0. Tested against a stand-in for the game (a mock of the plugin’s pipe), packaged and unpackaged:
| Checked against the mock | Result |
|---|---|
| Hello mod’s builder window and the Coords HUD app | Both show the same position from game.player.position() |
| A button in a mod window | Reaches the mod; game.audio.play reaches the bridge |
game.on("PlayerPuppet.OnGameAttached") and game.onState |
Arrive in both mods |
| Throw, hang, exit | Stop only the crash mod, in its own process and in the shared group. A hang shows NOT RESPONDING after 10 s; Restart ends it |
| Killing the shared process | All three mods show CRASHED, their windows close, and they restart 2 s later |
| Hot reload | Saving mod.mjs restarts the mod with the new code in about a second |
Memory (host log, Mod processes:) |
About 80 MB per own process; three mods in the shared process about 92 MB |
| Mod log app | New lines arrive live; a crash shows in red with only the mod’s own stack lines; Errors only and Copy put the crash on the clipboard |
New in the plugin: shared frames are given back at once when nothing draws them, opened textures are reused, and the perf line splits the overlay CPU time into GPU wait (waiting for the frame from three frames ago) and opening (opening shared textures).
In the game, do these in order, with a 30-second pause after each, timed on a phone stopwatch:
- Start the game and load a save. Stand still in gameplay for 60 seconds. Hello mod sits at the right edge, the Coords HUD at the top: both show the same X, Y and Z, and change together when you walk.
- F10 for interactive mode. In Hello mod, press Play sound: a sound plays. Switch Follow off: the position stops changing. Switch it on again. Drag Hello mod by its title.
- Open the launcher (the REDNODE tab, bottom left). Hello mod and Ticker mod show RUNNING · own process and their memory; Crash mod is off. Switch Hot reload on. Then either tell me “edit” in chat and I change Hello mod’s file in the game folder, or open
red4ext\plugins\REDnode\apps\hello-mod\mod.mjsin Notepad and changefirst versionto anything, then save. Within a few seconds, Hello mod’s window comes back with the new text. - In the launcher, switch Mod log on. It opens at the bottom right with every line the mods printed so far. Then switch Crash mod on. Its window opens at the left. Press Throw an error: its window closes, the launcher shows CRASHED, the Mod log shows the error in red, and Hello mod keeps updating. Press Restart in the launcher.
- In the Mod log, switch Errors only on and press Copy. Paste it into this chat: it should be the crash and the line in
mod.mjsthat threw. Switch Errors only off. - In Crash mod, press Hang forever. Hello mod keeps updating. After about 10 seconds the launcher shows NOT RESPONDING. Press Restart.
- Switch Shared process on for all three mods. Each restarts; the launcher then shows shared process (3 mods) and one memory figure.
- In Crash mod, press Kill the process. All three mods show CRASHED and their windows close; about 2 seconds later all three are running again.
- Press F10 until the overlay is hidden. Wait 30 seconds. Press F10 to come back to passive.
- Open the inventory for 30 seconds: the Coords HUD hides, Hello mod and the Mod log stay. Close it.
- Load a save. Hello mod’s Saves loaded count goes up by one.
- Quit the game.
After the test, switch Shared process off again if you want each mod in its own process.
What I read from the logs afterwards: rednode-overlay.log (the perf lines with GPU wait and texture reuse, and the Mod processes: memory lines every 10 seconds) and rednode-mods.log (everything the mods printed, and their crashes).
UI kit
UI is written as markup, not built by hand in code. Two layers keep it optional:
| Layer | What it is | Required? |
|---|---|---|
Window API, in rednode |
Open, place, show and hide windows (kind, size, anchor, drag handles, hideWith), plus the game bridge |
Used by every app |
UI kit, @rednode/ui |
Ready-made widgets as web components, with two themes | Optional. Apps can use their own HTML or any web framework |
Web components (custom elements like <rn-button>) work in plain HTML, React, Vue or anything else. Their styles are sealed in Shadow DOM, so app CSS cannot break them, and themes are CSS custom properties.
<!-- A titled REDnode window with widgets in it (kind: window) -->
<rn-window title="Weather control" draggable>
<rn-toggle label="Freeze time"></rn-toggle>
<rn-slider label="Rain" min="0" max="100"></rn-slider>
<rn-button>Apply</rn-button>
</rn-window>
<!-- A borderless button that looks like part of the game (kind: hud or window) -->
<body style="background: transparent">
<rn-button theme="game" sound="ui_click">Open stash</rn-button>
</body>
| Item | Plan |
|---|---|
| First widgets | rn-window, rn-panel, rn-button, rn-toggle, rn-slider, rn-input, rn-list, rn-tabs, rn-menu, rn-progress, rn-tooltip, rn-notification |
Theme rednode |
The REDnode HUD house style, as in the demo app |
Theme game |
A look-alike of the game’s own UI, rebuilt in CSS. No CDPR textures or fonts are copied |
| Built in | Keyboard and gamepad navigation, focus that works with click-through and the F10 modes, UI sounds played through the game’s audio by the bridge |
Every REDnode kernel reaches the same widgets, because only the overlay host can render a page:
| Who | How they make UI |
|---|---|
| Overlay app (HTML and JavaScript) | Uses the components in its markup |
| Tier 2 Node mod | A builder that produces the same components, for example rednode.ui.window({ title, children: [button('Apply')] }). No HTML needed. Arrives in O6 |
| Tier 1 and declarative rules | Ask through the bridge for a ready-made piece of UI, such as a notification. The host renders it with the kit |
Player install
Running npm run overlay by hand is the developer setup. Players should install from Nexus Mods or with Vortex and have it work, like CET. Planned for O3:
| Piece | How |
|---|---|
| Packaged host | Build the Electron host into a standalone app with its own Node, with @electron/packager. No Node, npm or repo needed. Ships inside the REDnode mod as red4ext/plugins/REDnode/host/. 320 MB unpacked with Electron 44: the executable alone is 235 MB. The zipped size is not measured yet |
| Auto-start | The plugin starts the host when the game starts. The host exits when the pipe closes, so it never outlives the game |
| Apps as separate mods | Overlay apps, and later Tier 2 Node mods, install into a REDnode apps folder, each with its manifest. On Nexus they list REDnode as a requirement, like CET mods list CET |
| Player settings | Enabled apps, the hotkey (F10 today) and similar, held by the launcher app |
The player’s path then is: install RED4ext and REDnode, add any REDnode apps, launch the game.
| Risk | Note |
|---|---|
| Antivirus | A game plugin that starts a program may cause false alarms. Code signing helps. Unverified |
| Size | 320 MB unpacked is large for a mod. Nexus allows it; a shared runtime for all REDnode apps keeps it to one copy |
| Vortex | The archive layout must let Vortex deploy REDnode as a normal mod |
Looking like the game
| Topic | Approach | Status |
|---|---|---|
| Visual effects | CSS and WebGL in the page: glow, scanlines, glitch, chromatic aberration | Standard web techniques |
| Fonts | The game’s fonts cannot be shipped with REDnode. Use close open-source fonts, or load fonts from the player’s own install at runtime | Loading from the install is unverified, and needs a licensing check |
| Colors | Match the game’s palette. In HDR, match the game HUD’s brightness (O7) | |
| Sound | Play UI sounds through the game’s audio system with a bridge call, so they mix with the game | Unverified |
Known limits and research items
| Item | Notes |
|---|---|
| Always on top | REDnode draws after the game finishes the frame, so its windows sit above everything, including game menus. Drawing under a game menu would need a hook inside the game’s own UI rendering. Research item, not planned |
| Game input while a window is focused | The game is paused for input while an interactive window has focus. This is intended, the same as CET |
| World-space UI | Labels attached to 3D objects would need the camera through the bridge each frame and would draw over everything. Research item |
| Steam Deck and Proton | Untested. D3D12 runs through vkd3d-proton there |