Maturity: Draft. How Cyberpunk 2077's photo mode and the in-game character creator (the mirror's appearance screen) can be opened, driven and left from script, and which techniques from published photo-mode mods an automated test session can reuse. Consolidated on 26 September 2026 from the decompiled 2.31 script bundle (redscript-cli 0.5.31), the game's input configuration, RED4ext.SDK and the pre-2.3 RTTI dump, the source of photo-mode, camera and lighting mods (installed releases, and public repositories where they exist), and the XF Runtime Bridge's first in-game session. Claims marked runtime were seen in the bridge's sessions (26 and 28 September 2026: sessions 1, 3 and 4, game 2.31, a heavily modded test profile); everything else waits for a game session. Grades follow the knowledge rules. The bridge itself is described in runtime access.
This page answers: what can a bridge do by itself in photo mode and the creator, and what still needs the player's hands?
Sources
| Source | Version studied | Used for |
|---|---|---|
| Game scripts (decompiled bundle) | 2.31 | Photo-mode menu, cursor, creator menus and menu scenarios |
Game input configuration (r6/, inputUserMappings.xml) | 2.31 | The photo-mode key and its action |
RED4ext.SDK generated types; red-dump-json RTTI dump | ad727771; a8e52990 (pre-2.3) | Native photo-mode types, quest node fields |
| Photo Mode Ex (psiberx, MIT) | source v1.4.1 (dd1245b1); installed 1.4.0 | Native photo-mode system hooks, extra characters, attribute sync |
| Codeware | v1.20.4 (613a1cb8) | QuestsSystem.ExecuteNode, photo-mode imports |
| Cyber Engine Tweaks | v1.37.1-9 (9a8522f) | How CET writes script properties |
| Appearance Menu Mod (AMM) | installed 2.12.5; repository 5427235 has the same Lua | Cursor hiding, spawned camera and lights, teleport, NPCs, time, weather |
| Character Customization Anywhere (Nexus 3930) | 1.2 | Opening the creator from anywhere |
| CharLi – Character Lighting Suite for Photomode (Nexus 8176) | 2.2a | Spawned light rigs that follow V |
| Photo Mode Unlocker XL (Nexus 4319), Portrait Enhancer for Photo Mode (8237) | 2.3.1, 2.2 | Photo-mode TweakDB flats: limits, restrictions, camera presets |
| Photo Mode Pose Selector (32633), Photo Mode Preferences (32736), Customisable Photo Mode UI (32815) | 1.2.0, 0.1.1, 0.2 | Menu attributes, light selection, page sync, photo-mode ink tree |
| Photomode NPCs Extended (18837), Photomode Facial Expression Mega Pack (7912), Multi Pose Pack Framework (4098) | 1.2, 2.1, 1.0 | Extra photo-mode characters, expressions and pose loading |
Hot-Sampled Photomode Renders (IGPT, Nexus 26318; RED4ext plugin InGamePhotomodeTweaks) | 0.1.4 | Rendering captures above screen resolution (§12); a string search of its DLL, no source |
REDmod TweakDB source tools/ | 2.31 | Photo-mode camera limits, defaults, effect ranges |
Installed-mod citations give paths inside each mod's folder in the mod manager (for MO2, mods/). Nothing was extracted from any archive. Who made each mod and what it taught us is in the community credits. The per-mod deep dive of the photo-mode mods (hooks, bugs, conflicts with the bridge) is survey batch B3; §11-§13 distil it.
1. The short answers
| Question | Answer | Grade |
|---|---|---|
| Can a script open the full photo mode the way the player's key does? | No script-visible route found. The key sends the input action TogglePhotoMode, which native code handles. PhotoModeSystem exposes only five script functions, none of which opens it. Codeware's quest-node route opens a restricted photo mode (§2). The practical routes are sending the player's own photo-mode key to the game window, or a native call that still needs reverse engineering. The key works with SendInput, but only with the game window in front, and bringing it forward catches whatever the player is typing elsewhere (session 4), so the bridge (0.4.2) sends it only while the game is already in front. Posted key messages don't reach it: the game reads the keyboard through Raw Input (session 5, §2.3). | source resource; restricted result, the key and posted keys runtime |
| Can a script open the character creator anywhere? | Yes, from the pause menu: open the pause menu and switch its scenario to MenuScenario_, which is what Character Customization Anywhere does (§3.1). Switching straight from the idle scenario, away from a mirror, left the creator half open (session 4), so the bridge's cc.open uses CCA's route since 0.4.2. | source; CCA's route runtime; the idle route's failure runtime; the bridge's CCA route [offline] |
| Can a script confirm or cancel the creator? | Yes, through the menu's own functions: Confirm calls ReFinalizeState(), and Back then "confirm" calls CancelFinalizedStateUpdate(). The menu has no autosave of its own (§3.3). | source |
| Can the photo-mode camera be placed exactly? | Partly. The camera PRESET attribute puts the camera at a TweakDB-defined offset from V. The menu's placement rows move V, not the camera (session 5). Moving the camera's own entity is built as a research command (photo.camera.place, 0.5.2) that reads back whether photo mode keeps it; not yet seen in game (§4). | resource runtime; exact placement hypothesis |
| Can photo-mode lights be switched and moved? | Switched and shaded, yes: attribute 44 is on/off (seen in the first session's menu dump; switching it from script is built but untested). Moved, not through the menu. Spawned light entities, as CharLi and AMM use, can be placed exactly (§5). | runtime menu; source |
| Can the photo-mode mouse cursor be hidden? | Yes: make the cursor controller play its Hide context, as AMM does (§6). | source |
| Can a scripted creator change update the row's label? | Yes: drive the change through the row's own controller, or push the updated option back into it (§7). | source |
| Can a capture be rendered above screen resolution, without the menu? | The engine has the renderer for it: its screenshot request takes a resolution preset, a ×1/×2/×4 multiplier, forced LOD0, PNG or EXR and render-debug view modes, and IGPT drives it from a plugin; its output has no UI and no ReShade. Reaching it ourselves needs the engine entry point (§12). | request fields source; IGPT's use source resource; no UI hypothesis |
2. Opening photo mode
2.1 What the player's key does
- The photo-mode key is the input action
TogglePhotoMode, mapped by default toIK_and the left thumbstick press (N TogglePhotoModeButton), in thePhotoModeinput context that gameplay contexts include resourceinputContexts.xml:77, 115, 847-849,inputUserMappings.xml:2066-2069. - No script handles it. The decompiled 2.31 scripts never name
TogglePhotoMode. AMM only reacts to the key: it observesPlayerPuppet.OnActionforTogglePhotoModeandExitPhotoModebutton releases source AMMinit.lua:698-705. Opening happens natively. - The script-visible system is small:
PhotoModeSystemhasIsPhotoModeActive,CanPhotoModeBeEnabled,IsExitLocked,UnlockPhotoModeItemandGetCameraLocation(WorldPosition), and nothing that opens photo mode source 2.31orphans.script:18453-18464;red-dump-jsongamePhotoModeSystem.json. - Native functions exist but aren't the entry point. Photo Mode Ex reaches the native
PhotoModeSystem::Activate(system) -> boolthrough the address library (hash2593396187), together withDeactivate,Finalize,SetAttributeValueand about 20 others. It only hooksActivateto register extra characters when photo mode starts, and never calls it source PMExsrc/,Red/ PhotoMode.hpp:100-118 src/,Red/ Addresses/ Library.hpp:19-43 PhotoModeExService.cpp:71, 247-274.Activatelooks like a setup step inside the opening sequence rather than the opening itself, so calling it alone would probably skip the camera and menu setup hypothesis. - What blocks the key: the status-effect tag
NoPhotoMode(AMM removesGameplayRestriction.NoPhotoModeevery frame when its photo-mode enhancements are on) source AMMinit.lua:1531-1534, andCanPhotoModeBeEnabled()(combat, scenes, some vehicles) source.
2.2 Why the quest node gives a restricted photo mode
The bridge's first route built the game's own quest node questOpenPhotoMode_ (fields factName, forceFppMode, alwaysAllowTPP, lockExitUntilScreenshot resource SDK quest/) with alwaysAllowTPP = true and forceFppMode = false, and ran it through Codeware's QuestsSystem.ExecuteNode. Codeware runs a single node in the root quest phase with a fresh quest context source Codeware src/.
- Result: photo mode opened, but first-person only. The camera-type list offered only First-person Perspective, the V tab was unavailable, and field-of-view writes changed nothing. Opened with the player's key in the same place, the menu had Drone, and every attribute worked runtime first session, step 9, and the two
photo.statedumps. - Not the TweakDB restriction lists. The profile already had
photo_andmode.general.onlyFPPPhotoModeInPlayerStates onlyFPPPhotoModeForStatusEffectsWithTagsemptied by Photo Mode Unlocker XL, and AMM empties the first too resource PMUr6/; source AMMtweaks/ SILVER_ PMU_ 2_ 3_ XL.yaml:27-28 init.lua:821. So the restriction follows the quest-node route itself. - Why: the node is the tool that quests use for their scripted photo moments, and the native handler appears to apply its own restricted setup whatever the two flags say hypothesis. Varying
factNameorlockExitUntilScreenshotis unlikely to help, and is not worth a session on its own.
2.3 Routes to the full photo mode
| Route | How | Risk | Grade |
|---|---|---|---|
| Send the player's key to the game | Read the effective binding (the default IK_, or the player's rebinding) and send that key press to the game window. photo.exit closes photo mode through the menu, so only opening needs the key. | The game must have focus: SendInput with the game in front opened the full photo mode in session 4 (21 of 23 opens; the command log). Bringing the window forward (SetForegroundWindow) catches the player's typing: in session 4 the maintainer was typing in another program when photo.open pulled the game in front, the keystrokes reached the game and the next open answered photo_. So since 0.4.2 the bridge sends only while the game is already the foreground window and otherwise refuses without touching any window (focus: bring_ restores the old route and warns). Posted window messages (route: postmessage, which never changes focus) don't reach it (session 5; the game reads Raw Input, see the next rows). It is input injection, so it targets only the game window and is allowlisted and logged. | SendInput runtime; focus stealing runtime session 4; postmessage fails runtime session 5 |
| Native entry point | Find the native function the TogglePhotoMode handler calls (the caller of Activate) in the address library or by disassembly, and call it on the main thread from the plugin | Needs reverse engineering. A wrong call crashes, and the address must be refreshed per game build. Re-checked for 0.4.2 (28 September 2026): the 2.31 scripts still have no handler or event that opens photo mode (the PhotoMode blackboard has only IsActive and PlayerHealthState; the menu only reacts to OnPhotoModeFailedToOpenEvent), the installed mods that name TogglePhotoMode (AMM, LUT Switcher, Photo Mode Pose Selector) only observe the key, and Photo Mode Ex only hooks Activate. Nothing offline shows a call that is safe, so the key stays the default | source hypothesis |
| Quest node | As above | Opens only the restricted photo mode | runtime |
Posted key messages (photo.open {route: "postmessage"}) | PostMessage of WM_/WM_ to the game window, never changing focus | Don't reach the game (session 5, S3: photo_). The executable imports RegisterRawInputDevices and GetRawInputData (and no DirectInput): the keyboard is read as Raw Input (WM_), which only the system's input stream produces, so no posted message, and no faked WM_, can deliver a key. SendInput feeds that stream but goes to whichever window has focus | runtime S3; imports resource (a string search of the installed executable) |
gamePhotoModeEnableEvent | An event class with a native SetEnable(Bool) / GetEnable() and no script user (importonly in the 2.31 scripts); its receiver is unknown | A lead, not a route: whether it opens photo mode, only allows it, or crashes, and on which entity it must be queued, is unknown. Test plan: one supervised probe in the test profile (a log line before the call, a fresh save), QueueEvent(player, e) with SetEnable(true), then game.status | source red-dump-json gamePhotoModeEnableEvent.json, SDK game/; effect hypothesis |
Opening photo mode without focus (0.5.2 research, asked for after session 5). In the order asked: (1) the native toggle the key handler reaches: no script names TogglePhotoMode; the input system dispatches the action natively, and Photo Mode Ex only hooks PhotoModeSystem::Activate (address-library hash 2593396187), which looks like one setup step of opening; calling it, or finding the handler, needs disassembly and a per-build address, so nothing is provably safe offline. (2) A synthetic input event in the game's own input system: the 2.31 scripts have no input-injection API, and none of the clones (CET, Codeware, AMM, RED4ext) injects one. (3) Making the game believe it's active: it reads the keyboard as Raw Input, so no window message delivers a key whatever the game believes. The key route with the game in front stays the default; the one in-process lead is the enable event above, for a supervised probe.
3. Opening, confirming and leaving the character creator
3.1 How Character Customization Anywhere opens it
A 38-line CET mod source bin/:
- It captures the in-game menu controller:
Observe('gameuiInGameMenuGameController', 'RegisterGlobalBlackboards', …)storesself(:5-7). This is why the mod says to reload a save if the hotkey does nothing. - On its hotkey it opens the pause menu:
inGameMenu:SpawnMenuInstanceEvent('OnOpenPauseMenu'), and sets a flag (:30-38), which is the same event the game's own pause key sends source 2.31inGameMenuGameController.script:210. - It redirects the pause menu:
Override("MenuScenario_callsPauseMenu", "OnEnterScenario", …) this:SwitchToScenario("MenuScenario_instead of opening the pause menu while the flag is set (CharacterCustomizationMirror") :9-16). - It supplies the creator's menu data:
Override("MenuScenario_opens the menusCharacterCustomizationMirror", "OnCCOPuppetReady", …) player_andpuppet character_with a newcustomization MorphMenuUserData(:18-27).
The vanilla mirror scenario does the same thing when it gets its data: OnEnterScenario marks the data m_ and opens character_, and OnCCOPuppetReady opens player_ and character_ source 2.31 inGameScenarios.script:217-247.
A 2.31 caveat. CCA writes the fields optionsListInitialized, updatingFinalizedState, editMode and currMenuName, but in 2.31 they are m_, m_, m_ and m_ source 2.31 orphans.script:52919-52927. CET stores a write to an unknown property on the Lua object and never reaches the game object source CET src/, RTTIHelper.cpp:949-975. So CCA's own field writes don't reach the game on 2.31. In game its F12 still gives a creator whose Confirm keeps the look [runtime, session 4, 28 September 2026]: the bridge read edit_ NewGame with updating_ true, and cc.confirm kept a real change (changes 1, closed_). Every preset of that session's finish sweep, confirmed this way, showed in photo mode afterwards. The vanilla mirror scenario's OnEnterScenario sets m_ itself, which explains it; the edit mode stays the default NewGame, so every row is editable (experiment 029).
The bridge's route since 0.4.2 is CCA's [source; in game unverified]: cc.open raises the pause menu's own event OnOpenPauseMenu through the menu-event blackboard (the in-game menu controller's OnTriggerMenuEvent makes the same SpawnMenuInstanceEvent call CCA makes, inGameMenuGameController.script:294), and a wrap of MenuScenario_ switches to the mirror's scenario with a new MorphMenuUserData carrying the edit tag instead of opening the pause menu, only when the bridge asked in the last three seconds and the pause menu came from normal play (prevScenario MenuScenario_; the credits picker's Back and the debug hub also enter the pause menu source 2.31 pauseScenario.script:31, debugHubMenu.script:170); any other pause opens the pause menu as usual, and so does a request the moment no longer allows (combat, a scene tier above 2, a vehicle or a stand-in player, re-checked as the pause menu opens). Since 0.5.1 the plugin makes that decision (unit-tested), and a request withdrawn after its event (a timeout, the kill switch) says the pause menu may still open. Which edit tag hypothesis: 0.4.2 passes HairDresser (or Ripperdoc), but CCA's F12, the route proven in session 4, ran in NewGame, and 0.4.1's stuck creator was HairDresser too, so the edit mode's world freeze, not the idle route, may have caused the stuck state. cc.open (0.5.1) takes edit_ to open exactly as CCA's F12 did; session 5 tries both. Because the data goes with the switch, the vanilla OnEnterScenario and OnCCOPuppetReady open the creator in the edit-V's-look mode without CCA's override of OnCCOPuppetReady. Why: in session 4 (28 September 2026) the batch-3 route below, used away from a mirror, left the creator's backdrop half open over gameplay with its busy flag stuck, and only loading a save recovered it, while CCA's F12 opened the creator in the same places runtime. Which native step the pause menu adds (the menu instance, its preview world) is not established hypothesis.
In game since (sessions 5 and 6): this route opens the creator away from mirrors with either edit tag, the menu reports NewGame regardless, and CCA isn't needed (runtime access §5, experiment 032) runtime.
The batch-3 route (0.3.0-0.4.1, replaced) source: no pause menu and no wrapped vanilla function. The bridge adds its own event to the idle scenario (@addMethod(MenuScenario_, the way Mod Settings adds its pause-menu event) and raises it through the game's menu-event blackboard (MenuEventBlackboard.MenuEventToTrigger, set to None first, as GameObject.TriggerMenuEvent does; the in-game menu controller turns the name into a scenario event) source 2.31 gameObject.script:2480-2490, inGameMenuGameController.script:76, 294. The event does something only when the bridge asked for it in the last three seconds (a flag on its registry), and checks the moment again; it then calls SwitchToScenario(n"MenuScenario_ with a new MorphMenuUserData carrying an edit tag. The vanilla OnEnterScenario sets m_, and OnCCOPuppetReady passes the same data to the creator, so it opens in the mirror's edit-V's-look mode and Confirm keeps the look. Before asking, the bridge refuses outside normal play, in combat, in a vehicle, with a scene tier above 2, where photo mode isn't allowed, or while the game locks saving for combat, a scene, a tier or a moving platform; it takes its save lock and asks only once the game reports saving locked. A request the menu never picks up (another menu open) is withdrawn after the wait, and the kill switch withdraws a pending one; a screen already open stays open for the player to leave.
Scene tiers. GameplayTier runs from 1 (full gameplay) through 2 (staged gameplay), 3 (limited), 4 (first-person cinematic) to 5 (cinematic) source 2.31 orphans.script:3105-3112. V's apartment reports tier 2 in normal play: session 3 found every cc.open there refused at tier 2 while Character Customization Anywhere's F12 worked runtime 28 September 2026. The game's own rule for changing clothes blocks tiers 3 to 5 only (InventoryGPRestrictionHelper.BlockedBySceneTier: the high-level state above 2) source inventoryItemData.script:842-847, and CCA checks no tier at all source its init.lua. So the bridge (0.4) accepts tiers 1 and 2 and refuses 3 and above.
Away from a mirror the mirror scenario doesn't finish [runtime, session 4]. In V's apartment, 0.4.1's cc.open answered opened: true (HairDresser), but the creator's backdrop stayed half-open over gameplay, the busy flag stayed set (cc.confirm refused busy), the game never returned to gameplay, and only loading a save recovered. Character Customization Anywhere's route (the pause-menu redirect) worked all session in the same room. So the bridge should open the creator through that route when the mod is installed, and keep its own event for a mirror.
3.2 Edit modes
MorphMenuUserData.m_ is gameuiCharacterCustomizationEditTag: NewGame (0, the default), HairDresser (1) or Ripperdoc (2) source 2.31 orphans.script:8175-8179. In any mode other than NewGame, the creator freezes the world and V with time dilation 0 under the reason VendorStash and restores it on close; NewGame enables the voice switcher; HairDresser hides some randomiser options source characterCreationBodyMorphMenu.script:222-224, 262-264, 881-883, 1107-1117; characterCreationPunkRandomizerMenu.script:380. Which mode the in-world mirror passes has not been traced.
The mode decides which rows can change. The menu calls ApplyEditTag(mode) before listing options, and each option carries editTags source characterCreationBodyMorphMenu.script:878. In the vanilla female CCO, hairstyle, eyes_, eyebrows, piercings (and its colour options), makeupEyes and teeth carry all three tags, while eyes (the eye shape), nose, skin_ and cyberware carry NewGame and Ripperdoc only resource female_; XF Eye Artistry's rows carry all three. So cc.open offers mode: "mirror" (HairDresser) and mode: "ripperdoc" (Ripperdoc, which adds the face-shape, skin and cyberware rows), and since 0.5.1 edit_ (NewGame, every row, no world freeze; what CCA's F12 ran as in session 4, where Confirm still kept the look because m_ was true, §3.1).
The creator's camera moves by body region, not page: the base menu's protected native RequestCameraChange(slot, delayed) with the slots GetSlotName returns (UI_, UI_, UI_, UI_, UI_, UI_, UI_, UI_, UI_, UI_, and m_ on open) source characterCreationMenu.script:73, characterCreationBodyMorphMenu.script:1075-1105. Hovering a row and a row's own change both request that row's slot (OnHoverOverOption, OnSliderChange), so cc.apply through the row moves the camera to the option's region; an option outside the eyes, hair, teeth, nose, lips and jaw lists (an XF row, for example) goes to UI_. cc.page requests a slot directly, which makes the cc-eyes crop's eyes zoom (UI_) reproducible after an XF row change source; whether UI_ is the zoom the crop was measured on is hypothesis.
3.3 What Confirm and Back do
| Action | Call chain | Grade |
|---|---|---|
Confirm (the Next button, or the one_ action when no colour picker or confirmation box is open) | ConfirmCustomizedCharacter() → if m_: ReFinalizeState() on the customisation system → event OnReFinalizeStateCompleteEvent → NextMenu() → OnAccept → the scenario's GotoIdleState() → MenuScenario_. In new-game mode it only calls NextMenu(). | source characterCreationBodyMorphMenu.script:272-277, 427-429, 701-712, 768-773; characterCreationMenu.script:118-124; inGameScenarios.script:212-214, 238-240 |
| Back | The first press shows a confirmation box; confirming it runs ConfirmBackConfirmation() → CancelFinalizedStateUpdate() → event → OnOutro() and OnCancel → idle. | source same file :682-699, 431-434, 752-767 |
| Autosave | No creator or mirror code requests an autosave. The script callers of RequestAutoSave and RequestCheckpoint are vendors, ripperdocs, perks, fast travel and delayed AutoSaveEvents. Whether the native ReFinalizeState saves is not established. | source 2.31 inGameScenarios.script:260-276, gameObject.script:2651-2672, fastTravelSystem.script:234, 611-620; native hypothesis |
The busy flag. A row's change (its arrows, or cc.apply through the row) sets the menu's m_ to SWAPPING in OnSliderChange, and only the system's OnAppearanceAppliedEvent sets it back to AVAILABLE (presets and randomise have their own completion events) source characterCreationBodyMorphMenu.script:303-313, 382-386, 473-483. While it is set, Confirm, the presets and randomise do nothing (OnRelease returns early, :268-271). Selecting the value a row already shows applies nothing, so the completion event never comes and the flag stays set for good: session 3 re-applied skin type 0 over 0 at 04:14:44, and every later change and Confirm was refused until the player backed out runtime plugin log, 28 September 2026 (the change before it, 3 to 0, had cleared normally). The bridge (0.4) never sends a same-value change, wraps OnSliderChange so a same-value change puts the flag back, clears a flag left set with nothing pending as stale, and answers cc.confirm with nothing changed on the screen by closing it with Back ("nothing to confirm").
To leave safely from script, call the menu's own ConfirmBackConfirmation() (discard) or ConfirmCustomizedCharacter() (keep) on the captured characterCreationBodyMorphMenu, never ReFinalizeState directly. The menu's own path also runs its outro and scenario change source. Keeping a look finalises it into V's state in the running game; a test session that loads its save at the end undoes that.
4. Camera placement
| Technique | What it gives | Grade |
|---|---|---|
| Camera PRESET (attribute 23: Customization, then Preset 1–9) | Moves the camera to a fixed offset from the edited character. Each preset is a TweakDB record photo_…_ with dist, pitchDeg, yawDeg, rollDeg, distUpDown, distLeftRight and fov. Portrait Enhancer for Photo Mode overrides all nine, for example preset 1 = distance −1.8, pitch −13°, up/down 0.07, FOV 33, and says some presets need about 10 m of free space in front of V. | attribute runtime (the menu dump); records resource r6/; relative to the character hypothesis |
| Camera limits | photo_/max_, max_, max_, min_/max_, min_/max_; PMU widens them to ±1000 m, 1–120° and ±180°. photo_ widens V's own offsets. | resource PMU SILVER_ |
| Collision | Attribute 39 (Full collision) and the collisionRadius…/collisionHeight… flats (PMU zeroes them). The camera sitting inside a wall in a small bathroom is a collision and spawn-point effect. | runtime resource |
| The camera entity | The native menu sends PhotomodeCameraSwitchedEvent { camera: wref<Entity> } to the light indicator controller, which keeps it in its public m_; the entity is a gamePhotoModeCameraObject, a game object whose data (0x240-0x4B0) is opaque in the SDK. The bridge's photo.camera.place (0.5.2, research) reads it from the menu's light indicator, checks its class, moves it with TeleportationFacility.Teleport to a position aimed at a point (V's eyes, a showroom head's eyes or a world point), and reads the active camera four frames later: held says whether photo mode kept it or places its camera itself every frame. The photo-mode lights, also native photo-mode objects, did hold when teleported (§5.1) | event and field source 2.31 photoModeLightIndicatorController.script:30, 47-49, orphans.script:53445-53448; SDK game/; holding hypothesis, session 6 |
| A camera preset written at run time | A second route if the entity doesn't hold: the presets are TweakDB records (photo_: dist, pitchDeg, yawDeg, rollDeg, distUpDown, distLeftRight, fov), and TweakXL's script API (TweakDBManager.SetFlat, UpdateRecord) can rewrite one while the game runs; selecting it (attribute 23) would then place the camera relative to V by photo mode's own code. Whether photo mode reads the record when the preset is selected or caches it when it opens, and the preset's frame (its pivot and angle conventions, which the default presets' numbers don't settle), are unknown. Built as research in XF Runtime Bridge 0.6 (photo.camera.preset: write the flats, UpdateRecord, select Customization then the preset, read the camera four ticks later, camera_); session 7's D1 answers it (session 6's T5: moving the camera entity did not hold, held: false at off_ 1.3 and 1.165 runtime) | records resource REDmod photomode.tweak; API source TweakXL v1.11.4 scripts/; behaviour hypothesis |
| Camera position (read) | PhotoModeSystem.GetCameraLocation(out WorldPosition), shown by the menu's debug location text | source photoModeCameraLocation.script |
| Framing V instead of the camera | V's offsets relative to where photo mode placed her: attributes 7 (yaw), 3421 (pitch), 3422 (roll), 8 (left/right), 9 (close/far), 37 (up/down), ±5 m in 0.01 steps with Photo Mode Ex. Framing presets built from these depend on where the drone camera spawned. | runtime first session, step 12 |
| A free camera outside photo mode | AMM spawns base\ with exEntitySpawner.Spawn, polls FindEntityByID, takes its camera component and calls Activate(blend, false), SetFOV, SetLocalOrientation; it moves it with TeleportationFacility.Teleport and restores the FPP camera with GetFPPCameraComponent():Activate(). Exact position; orientation is the entity's (from V's yaw at spawn) times the component's local rotation. Outside photo mode V must wear the third-person head (AMM swaps the TppHead slot item) or the face doesn't render. It loses photo mode's lights, poses and expressions. | source AMM Modules/, Modules/ |
What session 5 showed (29 September 2026, bridge 0.5.1) runtime: the placement rows up_, near_ and left_ move V (the stand-in) relative to the camera; none moves the camera. So the menu can't point the camera at anything but V, which is why the finish showroom's heads could only be framed by moving V or by spawning them where the camera already looks.
What session 3 showed (28 September 2026, test profile) runtime:
- The XF presets were overridden. Portrait Enhancer for Photo Mode sets all nine presets in a
.tweakfile, and its presets 5-9 roll the camera 90 degrees (rollDeg = 90); preset 7 arrived with its field of view 28 and roll 90, not the XF values resourcer6/. TweakXL reads tweak files in directory order, except that a file name starting withTweaks/ Portrait_ Enhancer/ PortraitEnhancer.tweak ^is read last and one starting with_,#,$or!first source TweakXL 1.11.4TweakImporter.cpp:22-80, 178-188, so the bridge now stages its presets as^xf_, andphoto_ mode_ presets.yaml photo.framelevels the camera (roll 0) after selecting any preset. - A read straight after a change is one frame stale. After
photo.camera.setchanges V's placement or the field of view, aphoto.subjectread about 20 ms later reports the new menu value but V's stand-in and the camera where they were before; the next read shows the change. Session 3's framing measured every probe against the previous step and drove V to the ends of the ±5 m range (no_) runtime plugin log 03:29:18-03:30:43. Framing now reads until the world has taken each change.response - V's placement axes are fixed in the world for the session, not turned with V's rotation slider: left/right moved V along the same world direction at rotation 10 and at -35 runtime same log. They need not be the camera's axes, so framing measures each axis's screen response (left/right, close/far, up/down) and uses whichever moves V across the screen.
What session 4 showed (28 September 2026, bridge 0.4.1) [runtime; experiment 029]:
- Framing converges on the first try with the camera level, but it starts from the current pose.
- V's close/far placement sets the camera's distance, about 1 m minus the value: framing the face converged at about 66 degrees with close/far 0.75 (about 35 cm, a selfie lens that inflates the nose and cheeks) and at about 9 degrees with close/far -1.2 (about 2.2 m, a portrait lens). The framer only changes the field of view, so the distance comes from where the frame starts;
photo.frame(0.4.2) starts face and eyes frames from the portrait pose (field of view 22, close/far -1.2). The eyes framing converged at 38° and 0.39 m. - Repeated frames drifted because the framer left its close/far probe (+0.05) in place whenever it centred with left/right: over a sweep of yaw frames close/far went from 0.1 to 0.9 and the field of view from 22 to 92 degrees, then close/far became the sideways axis and ran to -5, and the next frames sat at 3 degrees with the camera behind a wall while answering converged. Since 0.4.2 the probe is put back, each frame starts from its lens's pose, and a frame that would leave the lens's bounds is refused.
- V's rotation (key 7) runs -180 to 180 with Photo Mode Ex, so framing can turn V's back to the camera (
yaw_up to ±180, the back-lit ear check).offset - The bridge's yaw offset isn't the yaw attribute.
photo.frame'syaw_of ±30 set V's yaw (attribute 7) to ∓36–38°, ±15 to ∓18.5°, and 45 to −56°: opposite in sign, and the attribute about 1.25 times larger.offset
Recommendation: make framing deterministic in two steps. First put V somewhere known with open space (teleport, §8). Then pick a camera preset whose numbers the test profile controls, and fine-tune with V's offsets. Test whether the preset is relative to V's spawn and survives collision before relying on it (open question 4).
5. Lights
The light component itself, what scripts can change on it, CharLi in detail, the spawning routes and how a Studio lighting setup maps onto game lights are on photo-mode and spawned lights; the mirror built on them is designed in research/runtime/lighting-mirror-design.md.
5.1 Photo-mode lights
| Attribute | Meaning | Grade |
|---|---|---|
| 43 | Light number (1–3); selects which light the other rows edit | runtime |
| 44 | State: Off / On | runtime first session |
| 45 | Type: Spot / Ambient (listed on page 0 with value −1) | runtime |
| 46 | Shadow Off / On | runtime |
| 47, 48 | Brightness 0–100, range 0–100 | runtime |
| 49, 50 | Inner and outer cone angle, 15–175° | runtime |
| 51, 52, 53 | Hue 0–360, saturation, luminance | runtime |
- Lights reset every time photo mode opens, so a session sets them each time runtime.
- Select, wait, then set. Photo Mode Preferences sets attribute 43 to a light, waits two frames (about 0.03 s), sets the values, then puts the selection back source
PhotoModePreferences/; the bridge waits three ticks.init.lua:772-810 - No attribute places a light: photo mode puts it where the camera is when it switches on, and it stays in the world there: in session 3 light 1 lit V only when it was switched on after framing, and the maintainer saw its reflection outside a window once the camera had moved on runtime 28 September 2026. Switching it off and on again after framing re-places it at the camera (the bridge's
photo.light.set {place: {camera: true}}), but in session 3 even that lit V unreliably (face mean about 18 against 72 once), so a direct placement is the next step. ResetCurrentLightButton(IK_) is native only: no 2.31 script names it orR PhotomodeLightResetEventsource decompiled bundle; only the input configuration maps it resourceinputUserMappings.xml. Using it would mean sending a second key, which the approval forphoto.open(the photo-mode key only) doesn't cover.- Where
place.azimuthpoints source (the bridge'sXFPhoto.PlaceLight): azimuth turns V's stand-in's forward direction (GetWorldForward) counter-clockwise seen from above, so 0 is in front of V's face, 90 V's left and 180 straight behind the head, whatever the camera does. Afterphoto.framethe camera sits at azimuthyaw_from V's facing, which is why azimuth 180 with the camera at V's side put the light behind the head, blocked from the camera's view (session 4's ear check) runtime.offset - The light's entity is reachable from script: the light indicator keeps one screen projection per light, and
PhotomodeLightIndicatorController.GetProjection(index).GetEntity()returns the entity each follows sourcephotoModeLightIndicatorController.script:18-66,orphans.script:51645-51690; the entity is agamePhotomodeLightObject, a game object source SDKgame/, soPhotomodeLightObject.hpp TeleportationFacility.Teleportaccepts it. The bridge (0.4) moves it about V's head this way and reads its position back a few frames later (held). Photo mode keeps it there [runtime, session 4]: - Placement about V works. A light placed at an azimuth, elevation and distance around V's head answered
held: trueand lit the face well. That session's finish sweep lit every preset the same way this way: azimuth 25°, elevation 20°, 1 m, brightness 45, hue and saturation 0. - Re-placing at the camera doesn't. Switching the light off and on again after framing (
place: {camera: true}) didn't light the face. - Brightness 60 at 1 m overexposes the face; 45 doesn't.
Lighting decides how a face reads in photo mode. In session 4, faces and expressions that look right in the Studio read noticeably worse in game except under near-perfect lighting. A lighting rig that the bridge applies to match the Studio's lighting is banked (backlog; lighting mirror design).
- The light type row (attribute 45) isn't in 2.31's menu on the test profile: setting it answered "not in the menu" while 44 and 46-53 worked runtime session 3. The bridge skips it with a note. Photo-mode lights are native
gamePhotomodeLightObjectentities with agamePhotomodeLightComponent(anentLightComponent), and the menu receivesPhotomodeLightInitializedEvent { light: wref<Entity> }and projects each light's position for its on-screen indicator source SDK generated types; 2.31orphans.script:53440-53443,photoModeLightIndicatorController.script. Getting hold of these entities to move them is a lead hypothesis.
5.2 Spawned lights (CharLi and AMM)
Both mods spawn their own light entities and control them through the light component, in or out of photo mode:
| Step | CharLi | AMM | Grade |
|---|---|---|---|
| Templates | Its own base\ (spot, area, point) in charli.archive | Its own base\ | source |
| Spawn | exEntitySpawner.Spawn(template, transform, 2) at V's position plus an offset and Euler rotation, then poll Game.FindEntityByID (up to 10 tries) | exEntitySpawner.Spawn(template, transform, app, record) | source CharLi cl.glow.lua:1465-1470, 297-330, 485-490; AMM Modules/ |
| Follow V | Watches the puppet's position and yaw each update and moves every light with TeleportationFacility.Teleport(entity, position, angles). In photo mode it follows the photo-mode puppet, caught with ObserveAfter('PhotoModePlayerEntityComponent', 'SetupInventory') → self.fakePuppet. | Moves with Teleport | source CharLi init.lua:33-35, cl.core.lua:27-42, 1567-1630, cl.glow.lua:1477-1479 |
| Control | FindComponentByName(name), then SetColor(Color), SetIntensity, SetRadius, SetAngles(inner, outer), ToggleLight(bool) | Finds components whose class contains LightComponent; same setters. Local shadows can't be switched at run time, so AMM respawns the _ variant. | source CharLi cl.glow.lua:1562-1688; AMM Modules/ |
| Remove | GetEntity():Destroy() with a destruction poll; everything on CET shutdown | Dispose() | source CharLi cl.glow.lua:333-366, 1490, cl.core.lua:1646-1655 |
exEntitySpawner is a CET function. From redscript, Codeware's DynamicEntitySystem is the equivalent (AMM spawns NPCs with it) source AMM Modules/. A third route sets every light field, not only those with a setter: World Builder spawns an empty entity through Codeware's StaticEntitySystem and adds a light component built from data as the entity assembles (photo-mode lights §3). None of these mods' templates may be reused (no open licence, provenance follow-ups), so a bridge rig needs its own light entity.
A scripted light sweep follows from this: spawn one spot light, then for each step set its position on a circle around V's head (fixed radius and height, azimuth in steps), aim it at the head, wait a frame and capture. With photo-mode lights alone, a sweep can vary brightness, colour and cone but not direction.
6. The photo-mode cursor
- The cursor is its own game controller.
CursorGameControllershows and hides the cursor by playing a context animation throughProcessCursorContext(context, data, force), where the contexts includeShowandHidesource 2.31cursorGameController.script:120-175, 343-359. - AMM's technique: override
CursorGameController.ProcessCursorContextto keep a reference to the controller and replace any context withHidewhile its setting is on; toggling callsProcessCursorContext("Hide"/on the kept controller. It applies this when photo mode opens source AMM"Show", nil) init.lua:443-451,Modules/. In redscript the same is atools.lua:4419-4432, 4533-4535 @wrapMethod(CursorGameController)onProcessCursorContextplus a flag, and the kill switch clears the flag hypothesis. - Not the menu fade. Fading the menu (
OnFadeVisibility) hides the menu, not the cursor; the first session's captures showed the cursor runtime. - The bridge's version [offline]:
photo.hud.hidesets a flag that our@wrapMethod(CursorGameController)onProcessCursorContextreads (every context becomesHide, but only while photo mode is active, so no other menu can inherit a hidden cursor); the wrap notes each cursor controller it sees and replays each one's own state when the flag changes. Photo mode opening or closing, any bridge status read outside photo mode, an idle disconnect and the kill switch clear the flag (projects/). The next session checks it with the pointer over V's face.xf-runtime-bridge/ redscript/ XFRuntimeBridgeActions.reds - Other candidates, not tested: the native
PhotoModeCursorStateChangedEvent { cursorEnabled, keepCursorPosition }(imported by Codeware), the menu layer'sinkMenuLayer_event, or moving the Windows cursor out of the capture region before a capture source CodewareSetCursorVisibility.Init(false) scripts/; 2.31Base/ Imports/ PhotoModeCursorStateChangedEvent.reds entityPreviewGameController.script:201-209; effect hypothesis.
7. Keeping the creator's rows in step with scripted changes
- A row applies its own change. Each option row is a
characterCreationBodyMorphOption. ItsSetSelectedSwitcherOption,SetSelectedAppearanceDefinitionandSetSelectedMorphName(info, index, force)set the row's label text and fireOnSliderChange, to which the menu responds by callingApplyChangeToOption(option, index), moving the preview camera to that body region and marking itself busy source 2.31characterCreationBodyMorphListItem.script:365-391, 422-470;characterCreationBodyMorphMenu.script:473-483. - A direct system call bypasses the row. The menu only refreshes rows when the system sends
OnOptionUpdatedorOnAppearanceSwitched(for linked options); it then callsUpdateOption(i, lookup, new)→ the row'sSetOption(newOption)source same menu:388-464. A bareApplyChangeToOptionfrom outside leaves the label stale unless the system sends one of those events. - So the bridge has two options [source; behaviour in game untested]:
- Drive the row (preferred): find the row whose option's
info.uiSlotmatches, and call itsSetSelected…(info, index, true). That is what the arrows do, so label, camera and busy state stay consistent. - Refresh after the fact: re-read the option from
GetUnitedOptionsand call the menu'sUpdateOptionfor each row, or the row'sSetOption.SetOptionpassesforce = true, so it re-sendsOnSliderChange; the repeatedApplyChangeToOptionwith the same index should do nothing further.
- Drive the row (preferred): find the row whose option's
- The bridge drives the row [offline]:
cc.applyfinds the row byuiSlotand callsSetSelectedAppearanceDefinition,SetSelectedMorphNameorSetSelectedSwitcherOption(or the colour row'sSetSelectedAppearanceDefinitionColor) withforce = true, and uses the bare system call only when no row shows the option; the result says which (route). - Colour rows (
characterCreationBodyMorphColorOption) have their ownSetOptionand a picker path that also ends inApplyChangeToOptionsourcecharacterCreationBodyMorphMenu.script:804-830. - For photo mode the same idea already works:
GetMenuItem(key).ForceValue(value, true)updates the row and the system together. Photo Mode Pose Selector and Photo Mode Preferences fall back toOnForceAttributeVauluesource PMPSinit.lua:787-814, PMPinit.lua:616-630. Photo Mode Ex rebuilds a whole option list and its label with the nativeFireSetupOptionSelectorEventsource PMExPhotoModeExService.cpp:1031-1054.
8. Other levers for automated sessions
8.1 Photo-mode attributes worth knowing
The full menu on this install had 92 items runtime (the first session's photo.state dump):
| Page | Attributes |
|---|---|
| Camera (0) | 16 camera type (FPP / Drone), 23 preset, 1 FOV (1–120 with PMU), 26 depth of field, 33 autofocus, 3 focus distance, 4 aperture, 2 roll, 39 full collision, 40 aspect ratio, 73 black bars, 42 camera speed; 3401 control scheme and 3402 snap to terrain (Photo Mode Ex) |
| Environment (1) | 69 weather, 70 time of day (0–1440 minutes), 71 game speed (0–1), 72 frame skip |
| V (2) | 38 character, 27 character visible, 5 pose category, 6 pose, 83 outfit preset, 32 muzzle flash, 28 facial expression (207 with the Mega Pack, 210 with one three-face XF set; the option's value is its position in the list, not the face table index: menu 56 "Static: Sleeping" was index 60 in session 4 runtime; the list's records are named by photo_, a string[] of record names, so a script reads it with GetStringArray, not GetForeignKeyArray, which finds nothing there: why photo.state gave no table index in session 5 resource REDmod photomode.tweak:400), 15 look at camera, 74–77 look-at body part and angles, 7/3421/3422 yaw/pitch/roll, 8/9/37 left-right/close-far/up-down |
| Characters (3) | 54 surrounding NPCs, 55 characters (spawn), 68 edit character, 3433 appearance (Photo Mode Ex), 56/65/57 NPC expression, category and pose, 60–62/66 and 3431/3432 NPC placement |
| Lights (4) | 43–53, see §5.1 |
| Effects (5), colour balance (6) | 10 exposure (-2.2 to 2.2; photo.camera.set {exposure} since 0.5.2, with 11 contrast, 12 vignette and 24 highlights), 13 chromatic aberration, 25 grain, 14/64 effect; 84–93 colour balance |
| Stickers and frames (7), saved settings (8) | 35, 22, 34; 29–31 |
Attribute numbers can shift with the game version and with mods that add rows; the bridge validates each one against the options the menu set up.
8.2 World, NPCs and V
| Need | Technique | Grade |
|---|---|---|
| Put V in a known open spot | GetTeleportationFacility().Teleport(player, position, EulerAngles(0, 0, yaw)); AMM keeps named spots as JSON (x, y, z, w, yaw) | source AMM Modules/ |
| Time and weather | In photo mode: attributes 69–71 (the bridge's world.time.set refuses there, not_ runtime session 4, so the time attribute is the route in photo mode). In gameplay: TimeSystem.SetGameTimeByHMS, Codeware WeatherSystem.SetWeather(name, blend, priority) / ResetWeather | runtime menu; source AMM Modules/ |
| Freeze the world | TimeSystem.SetTimeDilation(reason, 0) and SetTimeDilationOnLocalPlayerZero (the creator's own freeze); AMM clamps 0 to 1e-13 for slow motion | source §3.2; AMM Modules/ |
| Hide NPCs | In photo mode: attribute 54 off. In gameplay: AMM disposes NPCs within 20 m found with TargetingSystem (TSQ_) | runtime; source AMM Modules/, Modules/ |
| Keep NPCs harmless | SetAttitudeTowards(player, AIA_, GodModeSystem.AddGodMode(id, Immortal, …); V SetInvisible(true) | source AMM Modules/, Modules/, Modules/ |
| Freeze one character | TimeDilationHelper.SetIndividualTimeDilation(entity, reason, 0) (AMM, Photo Mode Pose Selector) | source AMM Modules/; PMPS init.lua:335-345 |
| Catch the photo-mode V puppet | Wrap PhotoModePlayerEntityComponent.SetupInventory and keep fakePuppet (record Character.Player_) | source PMPS r6/; CharLi init.lua:33-35 |
| A reference NPC beside V | Photo Mode Ex turns every Character record with persistentName: PhotomodePuppet, an entity template and an icon into a photo-mode character (Photomode NPCs Extended adds 208 this way); spawn with attribute 55 and place with 60–62/66; three NPC slots. Outside photo mode: Codeware DynamicEntitySystem.CreateEntity(spec) (AMM). | source PMEx PhotoModeExService.cpp:102-245, src/; resource NPCs Extended yaml; AMM Modules/ |
| Poses and expressions | In photo mode: attributes 5/6 and 28 (an expression's option data is its position in the menu list, not its faceId: with the Mega Pack "Static: Sleeping" is option 56 and faceId 60 runtime session 4; the table index reaches the face only through the stand-in, facial expressions §3). Outside: AMM plays poses through a workspot entity (PlayInDeviceSimple + SendJumpToAnimEnt) and NPC expressions through AnimFeature_; see facial expressions. How the pose lists, records and clips fit together is in poses | runtime source AMM Modules/, Modules/ |
| A calmer look-at | GameOptions.SetFloat("LookAt", "MaxIterationsCount", …) slows or freezes the look-at-camera turn for every character until reset | source AMM Modules/ |
| Hide the photo-mode menu, not the game | The ink tree under inkPhotoModeLayer (options_, input_, others) can be hidden widget by widget, as Customisable Photo Mode UI does on each OnShow | source r6/ |
Persistence to watch: AMM's HUD toggle changes the player's real / settings, and the look-at option is global; any bridge use must record and restore them source AMM init.lua:1353-1367. Photo Mode Ex keeps some values per save (§9.4) source.
9. The three save slots
Vanilla photo mode's "saved settings" page (attributes 29–31) keeps three slots. Players report that restoring a slot loses details they set. What follows was decoded from the maintainer's local 2.21 and 2.31 saves, read-only with the Studio's save container codec, and cross-read with Photo Mode Ex's native layouts. [offline] marks our reading of those saves. The design built on it is in photo-mode snapshots.
9.1 Where the slots live
- In the save file, in five root nodes:
PhotoMode_(the slots),Settings PhotoMode_,LightSettings PhotoMode_(2.3 and later),OutfitWeather PhotoMode_andNewStickersEditor PhotoMode_, next toQuestRequest photoModeSystem[offline].photoModeSystemis a count and 8-byte TweakDBIDs, probably the unlocked photo-mode items hypothesis.- So the slots belong to one save. A slot written after the last save is gone once an earlier save loads, and each save and character has its own three [offline; the in-game effect is a hypothesis until tested].
- Save and load are native. The menu passes the hold actions
PhotoMode_andSaveSettings PhotoMode_with the slot's attribute key to the nativeLoadSettings OnHoldComplete(attributeKey, actionName). No script sees what is stored source 2.31photoModeMenuController.script:134, 831-854. - The native system reads and writes attribute values through
PhotoModeSystem::GetAttributeValue,SetAttributeValue(key, value, apply)andProcessAttribute. These are in the address library (IDs 818420519, 815536999, 2433030483), next toUpdatePoseDependents(231086722),UpdateCategoryDependents(3893174133),ApplyPuppetTransforms(2694650124) andSetRelativePosition/SyncRelativePosition(1847598004, 4175827669). A slot load is probably a replay of the stored values through these source Photo Mode Exsrc/,Red/ Addresses/ Library.hpp:19-43 src/; replay hypothesis.Red/ PhotoMode.hpp
9.2 What a slot holds
PhotoMode_ is a u32 count (3) and three slot records, then a second array of three records that is empty in every save seen (its purpose is unknown). Counts inside a record are the save's packed integers (VLQ). Each record holds, in order [offline]:
| Field | Encoding | What it holds |
|---|---|---|
| Label | Save string | The date and time of saving; empty for an unused slot |
| Attribute values | Count, then float32 × count, indexed by attribute key | 94 values in 2.3x (keys 0–93), 69 in 2.21 (keys 0–68): the vanilla key range of that version. Values in real slots match the menu: FOV (1), pose category and pose (5, 6), V's close/far (9), look at camera (15), camera type (16), expression (28), look-at body part and angles (74–76), time of day (70), colour balance (83–91) |
| Stickers | Count (10), then 10 × 16 bytes | Zero in every slot seen |
| Pose category | i32 | Repeats key 5; −1 when unused |
| Pose | Save string <animationName>_ | e.g. solo_, CrossArms_: the pose by clip name as well as by list index |
| Camera | 7 × float32 | Small values, e.g. (−0.80, −1.66, 0, 1.55, 1.67, 0.04, 24), with the FOV last. Probably pitch and yaw in degrees and a position relative to V hypothesis |
PhotoMode_: three slots of three light records, then three 42-byte entries per slot [offline]. The 2.21 record (about 110 bytes) held each light's world position and rotation, e.g. (−570.0, 805.2, 26.9). The 2.3x record is 58 bytes, and every 2.3x save seen has default lights, so whether positions are still stored is open.LightSettings PhotoMode_: three slots, each with three inner entries (per NPC slot? hypothesis). Not decoded.OutfitWeather PhotoMode_: three slots of 10 sticker indices, −1 for empty [offline].NewStickersEditor
9.3 Why details get lost
| Cause | What it hits | Grade |
|---|---|---|
| Keys outside the vanilla range are never stored | Every Photo Mode Ex row: V's pitch and roll (3421, 3422), NPC pitch, roll and appearance (3431–3433), control scheme, snap to terrain, adjustable steps. Photo Mode Ex keeps them in its own per-character map (s_), which the native slot never reads. Also everything other mods add (AMM props and NPCs, spawned lights) and anything changed outside the menu | [offline] save layout; source Photo Mode Ex PhotoModeExService.cpp:32-36, 666-716 |
| One value per key for three NPCs | The NPC rows (55–57, 60–62, 65, 66) edit whichever NPC is selected, and the slot has one value per key. Photo Mode Pose Selector has to switch the menu page and selection to read each NPC's placement for the same reason | [offline]; source Photo Mode Pose Selector init.lua:1644-1760 |
| Stored, but reset by the pose | Changing a pose runs UpdatePoseDependents, which applies the pose record's lookAtPreset, positionOffset and rotation and resets the character's look-at state (allowLookAtCamera, lookAtCameraPreset, lootAtCameraState on the native character). If the replay sets look-at (74–77), expression (28) or placement (7, 8, 9, 37) before the pose finishes applying, the pose can overwrite them. Photo Mode Pose Selector re-applies V's look-at 3 frames (0.05 s) after events that reset it, which fits this reading | source Photo Mode Ex PhotoMode.hpp character layout, PhotoModeExService.cpp:380-400; Photo Mode Pose Selector init.lua:72-73, 976-996; the reset on slot load hypothesis |
| Placement depends on the control scheme | Photo Mode Ex's alternative controls rewrite the character's relativePosition, so a slot saved under one scheme may land elsewhere under the other | source Photo Mode Ex PhotoModeExService.cpp:455, 560, 570 (FixRelativePosition); effect hypothesis |
| Pose lists shift | Keys 5 and 6 are list indices, which move when pose packs change. Whether the loader falls back to the stored clip name is unknown | [offline]; hypothesis |
| Per-save storage | Loading an earlier save, or switching characters, brings a different set of slots | [offline] |
9.4 Persistence in Photo Mode Ex
Photo Mode Ex stores alternativeControls, snapToTerrain and depthOfField per save in its own persistent state (PhotoModeExPS : PersistentState, fetched from the persistency system when photo mode starts). Its pitch and roll values are not persisted source src/, PhotoModeExService.cpp:272.
10. Holding the head and eyes
10.1 The global look-at option
- What mods do: the legacy Photo Mode Tools script and AMM both call
GameOptions.SetFloat("LookAt", "MaxIterationsCount", …). 0.9 "freezes" the head and eyes, 1.0 lets them move, and 3.0 is written back as the "restore" value. Neither reads the original value first source legacyphoto_; AMMmode_ tools/ init.lua:2-22 Release/.bin/ x64/ plugins/ cyber_ engine_ tweaks/ mods/ AppearanceMenuMod/ Modules/ tools.lua:484-491 - CET passes the float through unchanged to the engine's option setter, typed as a float, and logs an error if the engine refuses it. So the freeze is not CET truncating 0.9 to 0 source CET
9a8522fsrc/.scripting/ GameOptions.cpp:107-110, 133-145, 264-278 - It is an engine config variable, registered at the engine's option init as CET's option patches are. It is not in
engine/, and CET never writes it to disk, so it resets when the game restarts source CETconfig GameOptions.h:3-56,OptionsPatch.cpp:9-38; resource negative search ofengine/; reset hypothesis.config - Why below 1 freezes hypothesis: the engine takes the whole part as the look-at solver's iteration count. At 0 the solver stops and the last look-at pose holds. The option is global, so while it is below 1 every character's look-at holds, including NPCs and in scenes. Whether 3.0 is the engine default is unconfirmed;
GameOptions.Print("LookAt", "MaxIterationsCount")in game shows the type, value and default.
10.2 Actor-scoped levers on the photo-mode stand-in
| Lever | What it does | Grade |
|---|---|---|
| Individual time dilation, following the world's | Photo Mode Pose Selector freezes one character with SetIndividualTimeDilation(n"PMPSPhotoMode", 1.0, 0.0, n"None", n"None", false, false) and releases it with the same call and ignoreGlobalDilation = true. The stand-in normally ignores the frozen world's dilation, and this makes it follow it, so its graph (look-at and blinks) stops. It releases the freeze automatically when a pose, category or expression changes, and on shutdown | source Photo Mode Pose Selector init.lua:19-26, 329-400; 2.31 orphans.script:11871-11873 |
| Look-at attributes | 15 (look at camera) and 74–77 (body part: upper body 0, head 1, eyes 2; angles) are menu rows, stored in slots, and reset by pose changes (§9.3) | runtime menu; source Photo Mode Pose Selector init.lua:27-32 |
LookAtAddEvent / LookAtRemoveEvent | A static target with per-part weights (eyes, head, chest) queued on the stand-in; untested on a posed stand-in (poses open question 8) | source; effect hypothesis |
AnimFeature_ | The graph's head and chest rotation inputs (RotateHeadX…), on top of any pose | resource source (poses §7); effect hypothesis |
Recommendation. Freeze with individual time dilation, which is scoped to one character, already works in a published mod, and releases by name. Hold an exact direction by restoring look-at part and angles after the pose settles (§9.3). Keep the global option only as a fallback behind a settings guard: read the original value, apply only while photo mode is open, and restore the original on exit, save load, CET reload or shutdown, session end and the kill switch.
11. The native photo-mode system: lifecycle, attributes and placement
11.1 Lifecycle
| Native step | What is true at that point | Grade |
|---|---|---|
PhotoModeSystem::Activate | Characters are being registered; Photo Mode Ex registers its extra NPCs and fetches its per-save state here | source Photo Mode Ex PhotoModeExService.cpp:247-273 |
PhotoModeSystem::Finalize | Attributes exist: Photo Mode Ex writes its saved rows with SetAttributeValue(…, apply = false) | source :276-282 |
The menu's OnShow | Script mods start here, but the menu is still setting up rows: Photo Mode Preferences waits 3 frames and 0.05 s before its first write | source PMP init.lua:699-705 |
PhotoModeSystem::Deactivate | Before the stand-ins go | source :285-291 |
A plugin that needs "photo mode is ready" can hook after Finalize; none of the three is an entry point that opens photo mode (§2).
11.2 Three layers for reading and writing an attribute
| Layer | Read | Write | Grade |
|---|---|---|---|
| Menu row (script) | GetMenuItem(key).GetSliderValue() / GetSelectedOptionIndex() | GetMenuItem(key).ForceValue(value, true); fallback the native event OnForceAttributeVaulue(key, value, true) (misspelt in the game) | source Photo Mode Pose Selector init.lua:628-655, 787-811 |
| Menu controller (native, script-visible) | Observe OnAttributeUpdated(key, value, doApply); each row's range and step from OnSetupScrollBar/OnSetupHueBar | OnAttributeUpdated(key, value, doApply), which a row calls on every change | source 2.31 photoModeMenuController.script:128; Photo Mode Ex scripts/ |
| System (native only) | GetAttributeValue(system, key, float&) | SetAttributeValue(system, key, value, apply); the system then runs ProcessAttribute(key) | source Photo Mode Ex src/ |
- Exposure (attribute 10) is raw ±2.2, step 0.022; the menu shows −100 to 100. Every effect row takes its range from a
photo_flat and its step as range / 100:mode.postFX brightness_2.2 (exposure),range contrast_0.2,range highlights_0.75,range chromatic_2.0,aberration_ range grain_1.0 resourcemax photomode.tweak:180-194; Photo Mode Preferences' fallback steps agree source PMPinit.lua:126-133. Soraw = shown × range / 100, and a wider exposure range needs only that flat. The unit (stops?) is not established hypothesis. - Defaults are flats:
photo_60,mode.camera.default_ fov photo_4 andmode.attributes.dof_ aperture_ default dof_8;focus_ dist_ default photo_50 m andmode.general.force_ lod0_ characters_ dist force_80 m force full detail near the camera resourcelod0_ vehicles_ dist photomode.tweak:74-75, 113, 165-178. AMM rewrites the first two in CET'sonTweak, as TweakDB loads source AMMinit.lua:776-822. - Order and waits. Set child rows before the switch that enables them (focal distance, aperture, autofocus, then depth of field; the colour-balance values, then colour balance), with 2 frames and 0.03 s between stages; lights as §5.1 source PMP
init.lua:699-838. - Another writer may be running. Photo Mode Preferences re-applies its saved values after every open (full collision off by default), finishing about 10 frames later when lights are included; a write in that window can be overwritten, so wait or read back source PMP
init.lua:137, 640-838.
11.3 How photo mode places a stand-in, and what Photo Mode Ex changes
- Transforms are written only when flagged. Each
PhotoModeCharacter(0x190 bytes) holds its spawn position and orientation,relativePosition, relative rotation and offsets, and anupdateTransformflag that attribute processing sets andApplyPuppetTransformsconsumes source Photo Mode Exsrc/,Red/ PhotoMode.hpp:13-77 PhotoModeExService.cpp:512-548, 600-720. So a stand-in moved withTeleportationFacility.Teleportstays put until the next menu change for it. AMM relies on this: it moves stand-ins by teleport and, after any menu arrow click (PhotoModeMenuListItem.StartArrowClickedEffect), watches them for ten ticks and teleports back any the menu moved source AMMinit.lua:406-426,Modules/. Whether the camera, which moves under input every frame, behaves the same is whattools.lua:776-837, 2243-2262 photo.camera.place'sheldtests hypothesis; test it across a menu change too. - Photo Mode Ex changes placement for every tool, whenever it is installed source
PhotoModeExService.cpp:56-65, 406-450, 550-598, 973-975:- the ground offset is always 0 and position adjustment a plain sum;
- V's and NPCs' placement rows are always ±5 m (its clamp
min(max(min, −1), −5)is −5 whatever the flat says, so Photo Mode Unlocker XL's widermax_never applies); this is the ±5 m the framer ran into in session 3;position_ adjust - with its default "alternative" control scheme (row 3401, on unless the save turned it off), left/right and forward follow V's spawn orientation and the native
SyncRelativePositionis skipped. That explains session 3's placement axes that stayed fixed in the world while V's rotation changed runtime session 3; the explanation source. NPCs then spawn at V's spawn transform, ±0.75 m to the side or 0.75 m forward by slot.
- The camera's own limits:
photo_distance ±40 m, pitch −55° to 70°, up/down and left/right 50 m, FOV 5-90° (widened by Photo Mode Unlocker XL and AMM) resourcemode.camera photomode.tweak:81-163. - The photo-mode system's slots: edit slot at
+0x36C, spawn slot+0x370, the three spawned NPCs at+0x1D0; Photo Mode Pose Selector's native helper reads the first and the third to find the exact NPC stand-in being edited source Photo Mode ExPhotoMode.hpp:91-98; resource byte search ofPMPSPhotoTargetBridge.dll. These offsets move with game patches.
12. Rendering above screen resolution
- The engine's screenshot request (
rendSingleScreenShotData) carriesmode(rendScreenshotMode:NORMAL1,HIGH_5, layered variants),RESOLUTION resolution(rend::dim::EPreset),resolutionMultiplier(×1, ×2, ×4),forceLOD0,saveFormat(SF_2,PNG SF_32, both 34) andEXR emmModes, an array ofEEnvManagerModifierview modes that includes surface albedo, world and view normals, roughness, metalness, hair ID, depth and the multilayered masks source SDKad727771rend/,SingleScreenShotData.hpp rend/,ScreenshotMode.hpp ESaveFormat.hpp,EEnvManagerModifier.hpp. Path tracing has separate ray and bounce counts for screenshots (PathTracingSettings.rayNumberScreenshot,bounceNumberScreenshot) source SDKPathTracingSettings.hpp. - IGPT drives it from a plugin source its
Scripts/; resource its DLL strings: settings that mirror those fields (base resolution 1280×720 to 3840×2160, ×1/×2/×4, "Force LOD0", PNG/EXR); a nativeIGPT.reds TakeFancyScreenshot(resolution, scale, format)thenWaitForRender(format)deferred through a Codeware callback event; a watcher that takes the engine's file (named fromHIGH_) from the temporary folder and renames itRES_ EMM_ None photomode_in the PicturesDDMMYYYY_ HHMMSS.png|exr Cyberpunk 2077folder. The render blocks the game: IGPT patches the engine's two-minute watchdog for its duration, renders over about five minutes are lost, and it targets 2.31 with a ray-tracing GPU. It rebinds photo mode's Space (the vanillaPhotoModeTakeScreenshot_, F being the plain screenshot) to itself resource itsHiRes r6/; gameinput/ InGamePhotomodeTweaks.xml inputUserMappings.xml:2092-2103. Its engine entry point is not visible without disassembly. - Why XF wants it: the render has no menu, no mouse cursor and no ReShade (its description; the mechanism, that the engine renders the scene itself rather than copying the screen, is hypothesis), the three things the bridge's captures now work around (§6, creator lighting). If the shipping renderer honours
emmModes, one request could also return a face's albedo and normals for direct comparison with the Studio hypothesis. IGPT has no licence, so any bridge route is written from the SDK types and our own reverse engineering.
13. Stand-ins beyond the menu
| Lever | What it does | Grade |
|---|---|---|
| A workspot on the stand-in | AMM adopts the stand-in from SetupInventory and plays any workspot clip on it: spawn the pose's workspot entity at the stand-in turned 180°, PlayInDeviceSimple(workspot, stand-in, false, component, n"AMM_, SendJumpToAnimEnt(stand-in, clip, instant) | source AMM init.lua:392-404, Modules/ |
| Freeze one character | §10.2; the release also runs on that character's category, pose or expression change, on OnHide and on CET shutdown | source Photo Mode Pose Selector init.lua:329-409, 2660-2663, 2738-2747 |
| Show or hide makeup and earrings | FindComponentByName(name):Toggle(bool) on the stand-in: hx_ (the male lips makeup has the generated name MorphTargetSkinnedMesh1265), hx_, i1_…04, and every *seamfix* component. An in-game A/B switch for a decal | source AMM Modules/ |
| NPC look-at and expression rows | NPC look-at mode 58 and body part 78 (V: 15 and 74), NPC expression 56, character selector 68 | source Photo Mode Pose Selector init.lua:1-12 |
| Keep V's look-at through pose changes | Re-apply the saved body part 3 frames and 0.05 s after a category, pose or character change | source Photo Mode Pose Selector init.lua:2805-2828 |
Open questions
- What does the native
TogglePhotoModehandler call, and can a plugin call it safely? What doesgamePhotoModeEnableEventdo, and who receives it? (Posted key presses don't reach the game: it reads Raw Input runtime session 5.)SendInputwith focus does runtime. Does Character Customization Anywhere's Confirm keep the look on 2.31?Answered in session 4: yes; it opens withupdating_true in thefinalized_ state NewGameedit mode (§3.1).- Which
m_does the in-world mirror pass, and does the nativeeditMode ReFinalizeStatesave or autosave? (cc.open's idle-scenario event away from a mirror: no, it leaves the backdrop half-open and the menu busy; §3.1.) Doescc.open's pause-menu redirect (0.4.2) open it anywhere, as CCA's does, and with which edit tag (HairDresser, or NewGame as CCA's F12 ran;edit_, 0.5.1)? Does photo mode's own time-of-day slider (attribute 70, whichmode world.time.setsets in photo mode since 0.4.2) give the world's clock back when photo mode closes? (Session 5 comparesgame_'sstatus world_before and after; until then the bridge's photo-mode undo never touches the world's clock.)time_ seconds - Is the camera PRESET placed relative to V's spawn position and yaw, and how does collision change it? Can the camera entity from
PhotomodeCameraSwitchedEventbe teleported and stay put (photo.camera.place'sheld, session 6)? Does a preset rewritten at run time with TweakXL'sSetFlatapply when selected? Does photo mode keep a light moved with the teleportation facility where it was put?Answered in session 4: yes (held: true, and the face is lit from there; §5.1).- Does the AMM-style
Hidecontext hide the photo-mode cursor without side effects on the menu's mouse input? - Does the system send
OnOptionUpdatedafter an externalApplyChangeToOption, for any option type? - Does a vanilla slot load restore look-at (74–77), expression and V's placement, or does the pose's own setup reset them? Does it find a moved pose by the stored clip name? Do 2.3x light slots keep light positions? What do the second
PhotoMode_array andSettings PhotoMode_hold? (The snapshot design's session answers these.)OutfitWeather - Is
LookAt/a float with default 3, and does 0.9 hold every character's look-at, NPCs and scenes included? Does following the world's time dilation freeze the stand-in's look-at without side effects on its pose changes?MaxIterationsCount - Does a teleported photo-mode camera survive a menu change (a camera row or preset), as teleported stand-ins survive until their own row changes (§11.3)?
- Which engine function takes a
rendSingleScreenShotDatarequest, can the bridge plugin call it on the main thread without IGPT's watchdog patch at ×1 or ×2, and does the shipping renderer honouremmModesother thanEMM_(§12)?None - What unit is the exposure attribute's raw value (a stop per unit?), and does widening
photo_widen the row?mode.postFX.brightness_ range
Related pages
Runtime access · Facial expressions · Poses · Character-creator lighting · Game crashes · Bridge autonomy backlog · Runtime bridge design · Test card