Maturity: Draft. This page covers how each Cyberpunk 2077 mod type gets code into the running game, where each one logs, and how XF Studio's local bridge reaches the game. It is consolidated from source reading of RED4ext 1.30.0, RED4ext.SDK 1.0.0, redscript 0.5.31, CET 1.37.1, TweakXL 1.11.4 and psiberx's plugins, plus the decompiled 2.31 scripts and the offline build, self-test and tests of projects/. Claims seen in the game (the bridge's first in-game run, 26 September 2026) are marked runtime; their evidence is the test card's script-call check and first-session results, and later sessions' experiment pages (session 4, sessions 5 and 6). The rest wait for the test card. Crash fingerprints are on game crashes. The full citations, capability matrix and phase-2 plan are in the runtime bridge design.
1. Load order and entry points
| Mod type | How the game picks it up | Entry point | Grade |
|---|---|---|---|
| RED4ext plugin | Found at red4ext/ (at most one folder deep). Needs the exports Supports (API version), Query (name, version, runtime, SDK) and Main(Load/. Refused if its runtime doesn't match the game build, or its SDK is older than 0.5.0-compat. | Main(Load) runs before the game starts, and the plugin registers RTTI callbacks there | source RED4ext PluginSystem.cpp:89-154, 223-321 |
| Global natives for scripts | CRTTISystem::AddPostRegisterCallback, then CGlobalFunction::Create with flags {isNative, isStatic}, then RegisterFunction | Called by redscript as a declared native func, and by CET as Game.Name(...) | source SDK examples/; CET RTTIHelper.cpp:305-354 |
| redscript | r6/, plus any path a plugin adds with sdk->scripts->Add (resolved against the plugin folder) | Class bodies, @wrapMethod/@addMethod, ScriptableSystem callbacks | source RED4ext v1/, ScriptCompilationSystem.cpp:101-134; redscript unit.rs |
| TweakXL data | r6/, imported right after TweakDB loads | none (data) | source TweakXL Environment.hpp:13-16, TweakService.cpp:25-59 |
| CET mod | bin/ | registerForEvent at top level only; Game and Observe are available from onInit | source CET ScriptStore.cpp:31-64, ScriptContext.cpp:46-65, 194-196 |
2. Rules learned the hard way (from source)
- Declare natives outside a module. Redscript prefixes the module onto global natives (
XFRuntimeBridge.XFBridge_), but the plugin registers the plain name. Declare them in a module-less file, as Codeware does source redscriptPing unit.rs:1023,symbol.rs:222-246. - Ship
.redsthat declare natives through the plugin (scripts->Add), notr6/. A redscript compile error blocks every mod's scripts behind a message box, and a declared native without its DLL is such an error sourcescripts scc/.lib/ src/ lib.rs:86-94 - Call game and script functions from a plugin the way script code does: with a context and a caller frame. Script calls some natives as statics that the engine registers as member functions (every
TweakDBInterfacefunction,TDBID.ToStringDEBUG); the VM hands them the calling script's context, and with none the game dereferences null inrtti::Function::InternalCallNativeand crashes (exe+0x1e28769on 2.31; game crashes §2).RED4ext::ExecuteFunction(nullptr, …)starts a script with no context. Use Cyber Engine Tweaks' and RedLib's recipe instead: the engine's internal execute (SDK hashCBaseFunction_) with a caller frame passing the arguments asInternalExecute ExternalVarinstructions, a dummy caller function, and a dummyentEntitycontext for statics runtime crash and probe, first in-game run; source CETRTTIHelper.cpp:765-812, Codewarelib/, SDKRed/ TypeInfo/ Invocation.hpp:9-107 Functions-inl.hppExecuteNative; the fix itself [offline] (design §3.5). - A redscript class's static functions are global functions, named
<Class>::<Name>;<ParamTypes>;CClass::GetFunctionon the class finds nothing, so look them up amongCRTTISystem::GetGlobalFunctionsruntime. - Return
falsefrom a RunningOnUpdate. RED4ext removes a state callback that returnstrue, even though the SDK comment says the Running result does not matter sourceStateSystem.cpp:128-160. - Include
RED4ext/before anyRED4ext.hpp RED4ext/header. The SDK becomes header-only only whenApi Common.hppis included first; otherwiseCreateSemVer/CreateFileVerfail to link [offline]. - CET cannot open network connections. Its sandbox exposes no sockets, HTTP,
ffior process spawning, and file access is confined to the mod folder source CETLuaSandbox.cpp:10-79, 152-161, 695-719. Any external link must be native. - CET API details: CET has
spdlog.warning, notspdlog.warn; its Lua is LuaJIT (5.1), so useunpack;onDrawruns every frame even with the overlay closed source CETLuaSandbox.cpp:641-666,LuaVM.cpp:50-56. - Retail
Log/LogChannelprint nowhere unless CET (gamelog.log,scripting.log) or Red Hot Tools hooks them. Route script logs through a plugin native instead source CETLuaVM_.Hooks.cpp:192-250 - Bound JSON nesting before handing it to nlohmann/json, and serialise with
error_.handler_ t::replace - Parsing and destruction are iterative in 3.12, but copying, comparing and
dump()recurse. A 10 KB value nested 5,000 deep overflowed a 1 MB stack, and the game thread's 2 MB would take only about twice that [offline]. dump()throwstype_on invalid UTF-8 unless given a replace or ignore handler source json 3.12.0error.316 serializer::dump_.escaped
- Parsing and destruction are iterative in 3.12, but copying, comparing and
- Don't
FlushFileBuffersa named pipe before disconnecting. It waits until the client reads, which a client can simply not do [offline: held a stop 12 s].DisconnectNamedPipediscards unread data, so give a client a short, bounded window to read a final reply instead [doc] MicrosoftDisconnectNamedPipe. node:netcan't choose a pipe client's impersonation level or query the server PID. UseCreateFileWwithSECURITY_andSQOS_ PRESENT | SECURITY_ IDENTIFICATION GetNamedPipeServerProcessId(throughbun:ffiin Bun), or .NET'sNamedPipeClientStreamwithTokenImpersonationLevel.Identification[offline].redscript-cli lintexits 0 even on errors, so parse its output for them. It also doesn't check@wrapMethodparameter lists oncbmethods [offline].- Check a pre-2.3 RTTI dump against the 2.31 script bundle before relying on a signature.
TimeSystem.SetPausedStatehas no parameters in 2.31, although the dump lists two source. PhotoModeMenuListItem.ForceValuesilently selects the first option for an option value it doesn't know. Validate against the option data the menu set up (OnSetupOptionSelector) before calling it source 2.31 photo-mode scripts.- A photo-mode expression is selected by its option data (the
faceId), not its position in the menu source; the next session's expression checks R2 tell option data and menu position apart in game (test card). - Game scripts can't read the engine's render options (
GameOptionssuch asEditor/): no script type exposes them. Cyber Engine Tweaks can, because it hooks the options' initialiser at start-up and keeps the list (Characters/ Hair/ GlobalLight/ R GameOptions.Get(category, name), returning text, empty when unknown) source CETsrc/,patches/ OptionsPatch.cpp src/. The player's user settings (graphics, display) are script-readable throughscripting/ GameOptions.cpp inkISystemRequestsHandler.GetUserSettings()→UserSettings.GetGroup(path).GetVars(false)and theConfigVar…types; their groups and names are the game'sr6/source 2.31.config/ settings/ platform/ pc/ options.json - A mod can add its own event to a game menu scenario with
@addMethod(<scenario>) protected cb func On…() -> Bool; menu instance events are dispatched to the current scenario by name, as Mod Settings' pause-menu entry shows source Mod SettingsExtensions/. From anywhere in script,MenuScenario_ PauseMenu.reds MenuEventBlackboard.MenuEventToTriggerbecomes such an event through the in-game menu controller, which is how the game's devices open menus (GameObject.TriggerMenuEvent, set toNonefirst so the same event fires again) source 2.31gameObject.script:2480-2490,inGameMenuGameController.script:76, 294. PrintWindowwithPW_returns black for a window that is entirely off-screen, while legacyRENDERFULLCONTENT WM_works there but not for DirectX content; capture the game on-screen [offline].PRINT - RED4ext loads DLLs only from
red4ext/and that folder's direct files. It skips anything deeper and silently ignores a folder with no DLL.plugins/ <dir>/ sdk->scripts->Addtakes an absolute path or one relative to the calling plugin, so one plugin can add another folder's.redsto compilation. The proposed XF Core relies on both to gate its plugins' scripts source RED4ext v1.30.0PluginSystem.cpp,ScriptCompilationSystem.cpp:101-134(XF Core architecture §1). - A redscript compile error switches off every mod's scripts for that session. The game still starts, with none of them in effect source redscript v0.5.31
scc/.lib/ src/ lib.rs - CET runs each mod in its own context and keeps hotkey bindings per mod folder.
- A mod reaches another mod's exported table with
GetMod(name). - Bindings live in CET's own
bindings.json, keyed by the mod's folder name and the hotkey id. - By default CET deletes a mod's bindings once its folder is gone, so renaming a CET mod's folder loses its players' keys source CET v1.37.1
ScriptStore.cpp,Paths.cpp:76,VKBindings.cpp.
- A mod reaches another mod's exported table with
3. Logging locations
| Layer | File (MO2 writes new files to overwrite/) | Bound |
|---|---|---|
| RED4ext and its plugins | red4ext/, red4ext/ | max_ (10 MB) × max_ (5) per plugin, older ones pruned source RED4ext Utils.cpp:62-76, Config.hpp:24-27 |
| psiberx plugins (ArchiveXL, TweakXL, Codeware) | Their own red4ext/ | 100 MiB × 2, 10 logs (ArchiveXL 5) source SpdlogProvider.hpp:61-63 |
| redscript compiler | r6/ | daily, 4 kept source scc/ |
| CET mod | …/ | 5 MiB × 3, appended; info lines flushed only on warnings or at exit source CET Utils.cpp:105-118 |
| XF bridge (all layers) | red4ext/, sid= lvl= layer= cid= evt= lines | RED4ext's bound |
4. The XF bridge
Transport: a Windows named pipe owned by the RED4ext plugin, off unless
[bridge] enabled = true. It is local only (PIPE_, user-only DACL, first-instance flag, random name), and uses newline-delimited JSON, protocol 1.REJECT_ REMOTE_ CLIENTS Security boundary: the Windows user at medium integrity. Other users, remote machines, browsers and low-integrity or AppContainer processes can't reach the pipe. Same-user processes can, but they could already read or change the game's memory (design §4).
Security measures:
- A 256-bit token per session, published in
%LOCALAPPDATA%\. The plugin has no override for this folder.XFStudio\ runtime-bridge\ session.json - Only allowlisted methods exist, and writes are gated by
allow_.writes bridge.killis a control method, which only changes the bridge. - Input is bounded: 64 KiB per line and 32 levels of nesting, and only scalar
idvalues are echoed. Deep nesting would otherwise overflow the stack when a value is copied or serialised [offline: a 5,000-deep request crashed the host before the fix]. - A rate limit covers unauthenticated requests too. Five malformed or unauthenticated requests drop the connection. Every refusal is logged.
- A kill switch covers the CET hotkey,
bridge.killand aKILLfile. Stopping is bounded and never waits for a client to read. - Every request is logged with a correlation ID. No C++ exception can unwind into the game.
- A named pipe was chosen over loopback HTTP because browsers and other network clients cannot reach it (design §3.1).
- A 256-bit token per session, published in
Threading: the pipe thread never touches the game. Game work is queued and drained from the plugin's Running
OnUpdate(at most four tasks per tick), which runs on the game's main thread, in the same state tick as CET's LuaonUpdate[runtime; source CETGameHooks.cpp:93-104].- A task not started before its timeout is cancelled and never runs.
- A task already running at the timeout gets a 1 s grace. If it still hasn't finished, the client gets
timeout_and the late completion is logged.after_ start
Game access: by RTTI name at run time, plus a check of the static flag, parameter types and return type before each call. A missing function fails with
rtti_and a changed signature withmissing rtti_. A function whose signature is unchanged but whose behaviour changed is not caught; the runtime pin is the main guard. Two routes are in use:signature GetPlayer;GameInstance→entEntity.GetWorldPosition;ScriptGameInstance.GetPhotoModeSystem→gamePhotoModeSystem.IsPhotoModeActive.
Both worked in the game runtime. Phase-2 game access goes through our redscript actions layer instead (plugin → redscript statics by name), which lints against the installed 2.31 bundle. Every call, native or script, goes through one routine (
plugin/) that calls with a caller frame and a context (rule above).ScriptCall.cpp Clients: Bun (
bun:fficalls kernel32, [offline] Bun 1.4.2) and PowerShell 7 (NamedPipeClientStream). Both open the pipe at the Identification impersonation level. Both check the server PID againstsession.jsonbefore sending the token.node:netcan do neither, which is why the Bun client doesn't use it.
5. Phase 2: commands, writes and captures
One catalogue, many frontends. Every command (name, plain text, JSON Schema input, permission class, undo note) is defined once in
tools/; the MCP server, the CLI'sapi/ catalogue.ts runand the session runner derive from it, so a command added once appears everywhere [offline]. Permission classes:read,write-photo,write-world,write-character,control.Writes are gated twice. The tools can withhold classes (
--read-onlyor--allow, never both), and the game refuses every write unlessallow_in the plugin's config, which only thewrites = true -writesbuild sets for the test profile, and then each class (photo, world, character) unlessallow_lists it [offline].write_ classes Every write is reversible and recorded. It takes a save lock first, logs
write.done … undo=and returnsundo {method, params}for exactly what it changed (null when nothing changed or an earlier value is unknown); the kill switch undoes a freeze and keeps the save lock until a save is loaded runtime (test card S7), and restores a hidden photo-mode menu [unverified]; no queued write can run after it [offline].Photo mode is driven through its own menu.
GetMenuItem(key)andForceValuereachOnAttributeUpdated, with values checked against the ranges the menu set up; keys come from four photo-mode mods' source and matched the first session's menu dump (design §3.2) runtime. Codeware'sQuestsSystem.ExecuteNodewithquestOpenPhotoMode_opens only a restricted photo mode (first person, no V tab), so the full photo mode still needs the player's key runtime; the routes and why are in photo mode §2. The bridge'sNodeType photo.opensends that key, the player's own binding, to the game window only: the one allowlisted input, approved for the test profile [offline].Framing needs measuring, not fixed values. Where photo mode's drone camera spawns decides what fixed offsets show runtime.
photo.framereads V's head slot and the game's own projection (CameraSystem.ProjectPoint) and corrects V's placement and the field of view in a few steps; test-profile camera presets (attribute 23) can place the camera first [offline].Character options change only in the open creator, through the option's own row (
SetSelected…(info, index, true), as its arrows do, so the label follows; the bareApplyChangeToOptionthe first session used left the row's label stale runtime). An option can be named by its internal name, its on-screen label or its UI slot (the one active option there, e.g.piercings_), and a value by index, internal name or on-screen label (batch 3) [offline]. Confirm and Back go through the menu's owncolor ConfirmCustomizedCharacter()andConfirmBackConfirmation(), behind their own config switch (the -writes build only) source.Opening the creator from normal play (
cc.open, behind the same switch): the redscript layer checks the moment (phasegameplay; not in combat, a vehicle or a scene tier above 2; photo mode allowed; no combat, scene, tier or moving-platform save lock), takes the save lock and waits until the game reports saving locked. Then, since 0.4.2, it takes Character Customization Anywhere's route: it raises the pause menu's own event (OnOpenPauseMenu) through the menu-event blackboard, and a wrap ofMenuScenario_switches toPauseMenu.OnEnterScenario MenuScenario_with aCharacterCustomizationMirror MorphMenuUserDatacarrying the edit tag, instead of opening the pause menu, only for the bridge's own request (photo mode §3.1) source. In game this route works away from any mirror runtime (29 September 2026, sessions 5 and 6; experiment 032):- from the street and a megabuilding corridor, with the edit tag NewGame (
edit_) and with the default HairDresser tag alike, the creator opened fully drawn, not busy, andmode: "new_ game" cc.backclosed it cleanly back to gameplay; the player's own Esc still opens the ordinary pause menu; - the game ignores the tag on this route: with HairDresser the menu still reported
NewGameand made the same 860 of 1,209 options editable, skin tone and face shape included, which the mirror's tag doesn't allow (CC file chain); - it needs no Character Customization Anywhere: with CCA disabled both opens worked (session 6, T8).
Session 4's half-open creator came from the older route (0.3.0–0.4.1), which raised an added event on the idle scenario without the pause menu, not from the edit tag.
- from the street and a megabuilding corridor, with the edit tag NewGame (
The world clock and a freeze:
SetGameTimeByHMS/SetGameTimeBySeconds, and time dilation 0 on the world and V as the mirror screen does source. The clock may also be set with the appearance screen open: the game's own time-skip menu sets it while its full-screen menu is open (timeSkipPopup.script:274), and the screen only freezes the world with time dilation source; whether the screen's light follows the new time is the next session's check [unverified].Settings for the record:
game.options.read(read) returns the user settings of the graphics and display groups and, through the CET layer (two natives,XFBridge_andOptionsWanted XFBridge_, polled four times a second; the plugin checks every answer), the engine's character render options [offline].OptionsReport Captures are external (
PrintWindowor the screen), cropped to named regions sized in window heights so they frame the same area on 16:9 and 21:9, saved at full resolution and returned downscaled with an exact area filter [offline].
| API | Grade | Status |
|---|---|---|
gameuiPhotoModeMenuController.GetMenuItem / PhotoModeMenuListItem.ForceValue / OnAttributeUpdated | runtime | Used for camera, placement, lights (on/off, type and shadow [unverified]), expression, grain, aberration, camera preset |
gameuiPhotoModeMenuController.OnExitConfirmed(Bool) | runtime | Used for photo.exit |
Protected OnFadeVisibility(Float) through an added method | runtime | Used for photo.hud.hide (hides the menu, not the cursor) |
CursorGameController.ProcessCursorContext wrapped to play Hide while a flag is set | source 2.31; AMM overrides the same function | photo.hud.hide's cursor [unverified] |
CameraSystem.GetActiveCameraWorldTransform, GetActiveCameraForward/, GetActiveCameraFOV, GetAspectRatio, ProjectPoint; SlotComponent.GetSlotTransform(n"Head") on the photo-mode stand-in (caught in PhotoModePlayerEntityComponent.SetupInventory) | source 2.31; GetActiveCameraFOV equalled the photo-mode FOV runtime | photo.subject, photo.frame [unverified] |
Codeware QuestsSystem.ExecuteNode + questOpenPhotoMode_ | source; opens a restricted photo mode runtime | photo.enter {route: "quest"}, research only |
The player's photo-mode key (TogglePhotoMode; the UserSettings.json binding or IK_), SendInput to the game window | resource game input config; [doc] Microsoft | photo.open [unverified] |
gameuiICharacterCustomizationSystem.ApplyChangeToOption, GetUnitedOptions | runtime | Used in the creator; now through the option's row (characterCreationBodyMorphOption.SetSelected…) [unverified] |
ConfirmCustomizedCharacter (keeps: ReFinalizeState), ConfirmBackConfirmation (discards: CancelFinalizedStateUpdate) on the creator menu | source 2.31 | cc.confirm, cc.back behind allow_ (the -writes build only) [unverified] |
MenuEventBlackboard.MenuEventToTrigger → the pause menu's OnOpenPauseMenu → a wrapped MenuScenario_ → SwitchToScenario(n"MenuScenario_ | source 2.31; runtime sessions 5–6 | cc.open behind allow_; the edit tag is ignored (the menu reports NewGame) |
The base creator menu's protected RequestCameraChange(slot) through an added method; slots from GetSlotName (UI_, UI_, …) | source 2.31 | cc.page [unverified] |
GameInstance.IsSavingLocked(game, out locks) and gameSaveLockReason (Combat, Scene, Tier, PlayerOnMovingPlatform, …); PlayerPuppet.IsInCombat, VehicleComponent.IsMountedToVehicle, the PSM blackboard's SceneTier | source 2.31 | cc.open's safe-moment check [unverified] |
UserSettings.GetGroup(path).GetVars(false) and ConfigVarBool/ getters | source 2.31 | game.options.read's settings [unverified] |
CET GameOptions.Get(category, name) | source CET 1.37.1 | game.options.read's render options, through the CET layer [unverified] |
TimeSystem.SetGameTimeByHMS, SetGameTimeBySeconds, SetTimeDilation, SetTimeDilationOnLocalPlayerZero | source 2.31 | Used for the clock (also with the appearance screen open, batch 3 [unverified]) and the freeze |
TimeSystem.SetPausedState() | source 2.31, no parameters | Not used |
SaveLocksManager.RequestSaveLockAdd/ | source 2.31 | Held from the first write until a save is loaded (the kill switch keeps it); blocking autosaves in practice [unverified] |
Autosave triggers: vendor, ripperdoc and perk exits (MenuUIUtils.RequestAutoSave), fast travel, legendary loot, drop points, quest checkpoints, the timed autosave setting | source 2.31 | Why sessions start with a manual save and end by loading it |
6. What agents can and cannot do yet
- Seen working in game: reads, the photo-mode camera, placement, light values, fade and expression,
cc.apply, the clock, the freeze and the kill switch's restore, through the CLI and a session script (the MCP server shares the same catalogue and code path but was first registered with a client on 30 September 2026; its own layer is tested offline) runtime. Sessions 4–6 (28–29 September) added runtime (experiment 029, experiment 032):photo.open(only with the game window in front: keys posted to a background window don't reach it),photo.framewithout drift, lights placed about V, photo mode's own clock and exposure, expressions by label and by table index,cc.openandcc.backfrom the street,cc.applyby label and through a row's slot,game.loadandgame.waitthrough a load, and spawning, lighting, turning and clearing XF Finish Showroom heads. Every other command works with the game in the background. - Found not to work in game: placing photo mode's camera directly (
photo.camera.place: photo mode puts its camera back,held: false), and the vanilla-wardrobe writes while an outfit hides every area (worn clothing) runtime. - Built, not yet confirmed in game: light type and shadow, the cursor hidden with the menu, the clock with the creator open,
game.options.read, the full-body camera preset, and the refusal paths that need a missing engine address (script_); the session-7 ink demos (bridge 0.5.3) (design, test card).calls_ unavailable - Verified to exist (still untested in game):
- reading photo-mode state, camera transform and FOV;
- time of day, pause and dilation, teleport;
- weather (with Codeware);
- listing and loading saves by ID;
- the character-customisation system calls;
- quitting through
ExitGame.
- No API found:
- taking a game screenshot to a chosen file;
- opening the full photo mode without input (Codeware's quest-node route opens a restricted one; photo mode);
- saving to a chosen slot.
- Driving V (design only): teleport with facing, the camera turned by the game's own look-at, navmesh paths, crouch, weapons, consumables, menus and the HUD's interaction choices all have script routes; walking with the game's locomotion, jumping, sprinting and choosing dialogue lines need input, and the retail build carries CD PROJEKT RED's functional-test input natives (
FunctionalTestsGameSystem.FakeInput*, parameters stripped) as the lead for input without window focus (player control, design). - Capture paths: capture is external for now (window capture after post-processing). The lossless, before-effects route is an optional ReShade add-on (6.7.x headers, full add-on build only), which must never change the user's preset.
Details and citations: design §7.
Open questions
Are script calls from RED4ext's RunningNo: one made after the scripting layer detached for a load crashed the game (session 5); the bridge now refuses script calls from the detach until the loaded session's player attaches, and the same load passed in session 6 runtime (game crashes §2.3). Main menu, gameplay and photo mode are safe with a context runtime (script-call check).OnUpdate, made with a context, safe during loading screens?Can CET call a redscript class that a plugin added withYes (scripts->Add, as the Lua globalModule_?Class XFRuntimeBridge_) runtime.XFBridgeQuery - When does a
ScriptableSystem'sOnAttachfirst run: at the main menu or on the first save load? - Do the RTTI names from the pre-2.3 dump still match on 2.31? Settle this with a fresh RTTIDumper run.
- Does window capture return the game image in its fullscreen mode, or only in borderless windowed mode?
Does the Codeware quest node open photo mode outside a quest?Yes, but restricted to first person runtime. Does the save lock hold off autosaves?Do the photo-mode keys from mod source match this install?Yes; light on/off is 44, chromatic aberration 13, grain 25 runtime (photo mode §8.1).- Does
GameInstance.IsSavingLockedreport the bridge's save lock within the three tickscc.openwaits (the lock is a queued scriptable-system request)? If not,cc.openanswerssave_and a retry works.lock_ not_ held - Which screen space does
CameraSystem.ProjectPointanswer in, and is the photo-mode camera the active camera for it?photo.framedetects the space at run time; the next session records it.
Related pages
Runtime bridge design · Player control · Test card · Photo mode and the creator from script · Mod loading · Game crashes · Validation · Toolchain · The game's UI (ink)