From 1f8b36937b463f1243e983a5b581f27b7e0b2161 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 21 Jul 2026 11:01:50 -0400 Subject: [PATCH] Document ADV system menu input path --- docs/engine-ctx-reference.md | 1 + docs/engine-re.md | 34 ++++++++++++++++++++++++++++++++++ docs/opcode-reference.md | 13 +++++++------ docs/phase-a-slice-plan.md | 18 ++++++++++++++++++ tools/age_opcodes_himegari.py | 3 ++- vm-map/engine-ctx.toml | 5 +++++ vm-map/opcodes.toml | 20 ++++++++++---------- 7 files changed, 77 insertions(+), 17 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index 2dd9195..8ee4d9a 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -21,6 +21,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata | | `0x5190` | `shared_profile_int_table` | `int` | open-addressing 12-byte string-key to 32-bit value table; op 0x1a2 stores, 0x1a3 loads, shared SAVE.DAT serializes it | | `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) | +| `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero | | `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it | | `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) | | `0x46188` | `renderer_device_state` | `int` | embedded D3D9 device/render state; op 0x20e passes this base to the target/depth clear worker | diff --git a/docs/engine-re.md b/docs/engine-re.md index 8ddf714..a96c491 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -2311,6 +2311,40 @@ enabled. That transient channel is kept separate from persistent op-`0x88` Skip, cannot clear the user's toggle; both channels share the existing text completion, wait advance, voice deferral, and skip cadence. This needs no profile storage, boot seed, or game-specific conditional. +### ADV right-click/X system-menu path (2026-07-21) + +Right-click during ADV is not a frontend-owned menu shortcut. It is the same script-owned keyed-hotspot +mechanism already partially modeled for SC0000. SYSTEM4 maps physical right mouse and keyboard X to logical +action 7. Each of the 136 SC-family scripts registers a 1x1 dummy rectangle whose activation callback is +the local branch that cancels the ADV hotspot wait, calls raw script id `0x1f` (`MENU.BIN`), then rebuilds +the parent ADV controls and redraws the retained page. Opcode `0x97` binds action 7 to that rectangle. + +Native `adv_input_service_poll@0x411230` polls the configured logical-action mask first, then calls +`input_hotspot_poll_bound_action_callback@0x403fb0`. The helper scans armed records in registration order; +for each nonnegative op-`0x97` action index it tests `mask & (1 << action)` and returns the record's ordinary +activation callback PC. This is the missing port seam. `HotspotRegistry.BindKey` already retains the action +on the matching record, but no code consumes `Entry.InputBit`; Godot currently routes only pointer-left +activation and action-4/5 page advance. Right mouse therefore reaches `InputBindings` as action 7 but never +queues the MENU callback. + +The first implementation slice is narrow and engine-generic: expose bound-action activation on the armed +hotspot registry, route pressed keyboard/mouse/joystick logical-action masks through it before ordinary page +advance, wake the existing callback service, and reuse the same consume/rearm behavior as pointer activation. +No MENU-specific branch belongs in Godot or the VM. + +The reached script path is promising but should be validated incrementally. `MENU.BIN` is 48/49 opcodes +handled and its only static gap is op `0x80`; `INFO.BIN`, which selects character/enemy/voice/affinity/item +information pages, is 30/30 handled. The large `CHMENU` and five `INFO*` detail screens are roughly +91.5–95% handled and may expose secondary visual/data gaps after the shell opens. SAVE and CONFIG are only +about 80% handled and remain separate storage/audio-settings work, not prerequisites for opening or closing +the system menu. + +The sole `MENU.BIN` gap is now decoded: `op_0x80_set_default_gfx_object_slot@0x41ed40` stores operand 1 at +EngineCtx `+0x14e08`; `op_0x1d9_handler@0x420a30` substitutes that selected slot only when its explicit +object-slot operand is zero. MENU-family scripts select slots 7/8/9 on entry and restore slot 1 during +teardown. This selector should join the input bridge implementation for state correctness, though the +surveyed MENU/INFO scripts do not themselves call op `0x1d9`, so it is not the cause of the missing launch. + --- ## Native walls backlog (targets for this loop) diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 096d1a0..03e60a6 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -395,6 +395,11 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit The handler uses an alpha step of 1 and timer interval=argument when argument <=64. Above 64 it uses step=16 and interval=argument/16. The main loop polls that timer, advances by skipped intervals plus the current interval, and does not resume the script until the target endpoint has been presented. ROOM uses (1,2,10) for button/character entry and exit fades and (1,2,30) for the final fade before returning to TITLE. The port captures retained-frame snapshots whenever op 0x20c presents to a selected offscreen render target, then reproduces this blocking alpha lifecycle in the interactive host. +### 0x80 `set-default-gfx-object-slot` (u0041AF00, argc 1) +- **summary:** (slot) - select the retained graphics-object slot used when op 0x1d9 receives explicit slot zero. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x80_set_default_gfx_object_slot@0x41ed40 writes operand 1 to EngineCtx+0x14e08. op_0x1d9_handler@0x420a30 reads that field only when its operand-1 object slot is zero, otherwise it indexes the explicit slot; both paths write object+0x70. MENU-family scripts select working slots 7/8/9 on entry and restore slot 1 during teardown. + ### 0x131 `get-message-window-alpha` (get-message-window-alpha, argc 1) - **summary:** (out) - read the configured `message:MesWinAlpha` value used to alpha-modulate the ADV chrome. - **grounding:** source=investigation, confidence=high @@ -700,10 +705,10 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **evidence:** Ghidra /v2: op_0x94_arm_hotspot_wait@0x4166d0 sets ctx+0xc6e4=1 and calls input_hotspot_arm_cursor_tracking@0x404230 with 10000. SC0000 executes it immediately after the five control-strip and three keyed registrations. ### 0x97 `bind-hotspot-key` (u0041C150, argc 5) -- **summary:** (x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it. +- **summary:** (x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback. - **grounding:** source=investigation, confidence=high, noop_headless=True - **depends on:** 0x90 -- **evidence:** Ghidra /v2: op_0x97_bind_hotspot_key@0x41ff30 builds the same inclusive rect as op 0x90 and calls input_hotspot_bind_key_bit@0x403f50. That worker searches registered rects for exact equality and stores operand 5 in the record's key-bit array. SC0000 binds bits 0, 8, and 7 to its three 1x1 keyed records. +- **evidence:** Ghidra /v2: op_0x97_bind_hotspot_key@0x41ff30 builds the same inclusive rect as op 0x90 and calls input_hotspot_bind_key_bit@0x403f50. That worker searches registered rects for exact equality and stores operand 5 in the record's logical-action field. adv_input_service_poll@0x411230 passes the configured live action mask to input_hotspot_poll_bound_action_callback@0x403fb0, which returns the first matching record's activation PC. All 136 SC-family scripts bind action 7 to the callback that calls MENU.BIN; SYSTEM4 maps both right mouse and X to action 7. ### 0xcc `register-mouse-callback` (mouse_callback, argc 2) - **summary:** (poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script. @@ -969,10 +974,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x80 `u0041AF00` (u0041AF00, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x8c `jmp` (jmp, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=med diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index e112546..3c38ae7 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2206,3 +2206,21 @@ persistent-toggle survival across a physical press/release. **Next:** manually hold LeftCtrl through text reveal and multiple waits in SC0000, then confirm release returns to normal cadence and that the on-screen Skip toggle remains active if enabled during the test. + +### Slice B1 ADV right-click system-menu path investigated (2026-07-21) + +The native and corpus paths agree end to end. SYSTEM4 maps right mouse and X to logical action 7; every one +of the 136 SC-family scripts binds action 7 through op `0x97` to a dummy hotspot whose activation branch +calls `MENU.BIN`. Native `adv_input_service_poll` resolves these bindings against the live logical-action +mask before ordinary page advance. The port already stores the binding in `HotspotRegistry.Entry.InputBit`, +but nothing reads it, so action 7 stops after registration even though the physical input layer is correct. + +`MENU.BIN` is 48/49 handled, and its INFO selector is 30/30. The only MENU gap, op `0x80`, is a default +retained graphics-object-slot selector consumed by op `0x1d9`; it is straightforward state but not the +launch blocker. Larger character/info detail screens are about 91.5–95% handled and should be treated as +follow-up discrepancies reached through manual use. SAVE/CONFIG remain separate, storage-sensitive work. + +**Next:** implement one generic bound-action hotspot activation API, route pressed keyboard/mouse/controller +action masks through it before page advance, and implement op `0x80`'s engine-owned selector. Add a focused +synthetic bound-action test plus an SC0000 action-7 regression that proves the real `MENU.BIN` frame is +entered and the parent ADV wait is restored after return. diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 0ce9606..7ef07fd 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -9,6 +9,7 @@ INFERRED: dict[int, dict] = { 0x6c: dict(name='zero-int-range', category='compute', noop=False, confidence='high', source='investigation', summary='(destination)(count) — write logical integer zero to count consecutive VM cells beginning at the resolved destination.'), 0x7b: dict(name='coroutine-save-yield-handlers', category='control', noop=False, confidence='high', source='investigation', summary='(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator).'), 0x7c: dict(name='coroutine-resume', category='control', noop=False, confidence='high', source='investigation', summary='() — scene-coroutine RESUME point. Native requires run-state bit 0x2000000 (ctx[0x6dbc8]) set — THROWS (__CxxThrowException) if unset, so it is only ever reached on a scheduler-driven re-entry, NEVER on a cold first pass (cold flow jmps over it). Restores PC=ctx[0x53d28]+ctx[0x6dbcc]*4, clears the run-bit (ctx+0xa0ce4 &= ~0x2000000), resets input/line state. SC0000 0x443 (falls into the main loop label_444). See engine-re.md §Scene-coroutine framework.'), + 0x80: dict(name='set-default-gfx-object-slot', category='draw', noop=False, confidence='high', source='investigation', summary='(slot) - select the retained graphics-object slot used when op 0x1d9 receives explicit slot zero.'), 0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'), 0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'), 0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'), @@ -16,7 +17,7 @@ INFERRED: dict[int, dict] = { 0x90: dict(name='register-hotspot-callbacks', category='input', noop=True, confidence='high', source='investigation', summary="(x)(y)(w)(h)(on_enter_pc)(on_leave_pc)(on_activate_pc) - append an inclusive cursor rectangle and three callback PCs to the current script frame's native input registry. This opcode only registers; the ADV input service dispatches callbacks later."), 0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."), 0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'), - 0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it.'), + 0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback.'), 0xae: dict(name='continue-save-load-stack-restore', category='control', noop=False, confidence='high', source='investigation', summary='() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.'), 0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."), 0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index 942d388..4e3af07 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -73,6 +73,11 @@ name = "gfx_obj_ptr_table" type = "void*" note = "per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c)" [[field]] +offset = 0x14e08 +name = "gfx_default_object_slot" +type = "int" +note = "op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero" +[[field]] offset = 0x14ea0 name = "text_line_spacing" type = "int" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 3519759..3f78ca9 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1312,18 +1312,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041AF00" -category = "unknown" -summary = "" +name = "set-default-gfx-object-slot" +category = "draw" +summary = "(slot) - select the retained graphics-object slot used when op 0x1d9 receives explicit slot zero." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x80_set_default_gfx_object_slot@0x41ed40 writes operand 1 to EngineCtx+0x14e08. op_0x1d9_handler@0x420a30 reads that field only when its operand-1 object slot is zero, otherwise it indexes the explicit slot; both paths write object+0x70. MENU-family scripts select working slots 7/8/9 on entry and restore slot 1 during teardown." [[opcode.semantics.args]] i = 1 -role = "" +role = "default retained graphics-object slot" observed_types = ["imm"] [[opcode]] @@ -1577,12 +1577,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "bind-hotspot-key" category = "input" -summary = "(x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it." +summary = "(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback." noop_headless = true source = "investigation" confidence = "high" depends_on = [0x90] -evidence = "Ghidra /v2: op_0x97_bind_hotspot_key@0x41ff30 builds the same inclusive rect as op 0x90 and calls input_hotspot_bind_key_bit@0x403f50. That worker searches registered rects for exact equality and stores operand 5 in the record's key-bit array. SC0000 binds bits 0, 8, and 7 to its three 1x1 keyed records." +evidence = "Ghidra /v2: op_0x97_bind_hotspot_key@0x41ff30 builds the same inclusive rect as op 0x90 and calls input_hotspot_bind_key_bit@0x403f50. That worker searches registered rects for exact equality and stores operand 5 in the record's logical-action field. adv_input_service_poll@0x411230 passes the configured live action mask to input_hotspot_poll_bound_action_callback@0x403fb0, which returns the first matching record's activation PC. All 136 SC-family scripts bind action 7 to the callback that calls MENU.BIN; SYSTEM4 maps both right mouse and X to action 7." [[opcode.semantics.args]] i = 1 @@ -1606,7 +1606,7 @@ observed_types = ["imm"] [[opcode.semantics.args]] i = 5 -role = "input/key bit index" +role = "logical action index" observed_types = ["imm"] [[opcode]]