Implement ADV controls and native Auto timing

This commit is contained in:
gamer147
2026-07-18 17:16:06 -04:00
parent 8c7c7158a7
commit 4bf98cc7af
20 changed files with 1184 additions and 256 deletions

2
.gitattributes vendored
View File

@@ -1,2 +1,4 @@
docs/opcode-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol docs/opcode-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/global-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/engine-ctx-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
tools/age_opcodes_himegari.py whitespace=trailing-space,space-before-tab,cr-at-eol tools/age_opcodes_himegari.py whitespace=trailing-space,space-before-tab,cr-at-eol

View File

@@ -26,6 +26,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) | | `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) |
| `0x53d64` | `gfx_obj_record_array` | `void*` | gfx object-record array (field[0]=0xffffffff free; cmd-type at rec+0x24) | | `0x53d64` | `gfx_obj_record_array` | `void*` | gfx object-record array (field[0]=0xffffffff free; cmd-type at rec+0x24) |
| `0x53d88` | `cmd_type_table` | `int` | per-object cmd-type column base (write *(0x53d88 + curCtx*0x78)) | | `0x53d88` | `cmd_type_table` | `int` | per-object cmd-type column base (write *(0x53d88 + curCtx*0x78)) |
| `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes |
| `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand | | `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand |
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand | | `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
| `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) | | `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) |
@@ -37,6 +38,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) | | `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) |
| `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset | | `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset |
| `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history | | `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history |
| `0x6dbe4` | `adv_auto_voice_pending` | `int` | set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets |
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) | | `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
| `0xa0cc0` | `screen_w` | `int` | screen width (640) | | `0xa0cc0` | `screen_w` | `int` | screen width (640) |
| `0xa0cc4` | `screen_h` | `int` | screen height (480) | | `0xa0cc4` | `screen_h` | `int` | screen height (480) |

View File

@@ -1054,6 +1054,108 @@ NOT the predicted coroutine yield (`0x140`/`~0x50f`); the oracle reports whateve
surfaced an earlier state hole. **This is now the repeatable way to localize a mis-modeled op/state.** surfaced an earlier state hole. **This is now the repeatable way to localize a mis-modeled op/state.**
Phase-2 extension (deferred): effects-diff (global-bank / gfx-registry writes) for branchy scenes. Phase-2 extension (deferred): effects-diff (global-bank / gfx-registry writes) for branchy scenes.
### ADV control-strip buttons and native hotspot callbacks (2026-07-18)
The five controls at the lower right of ordinary ADV scenes are script-driven retained UI, backed by
`SO001.AGF` in system surface slot 17. They are not Godot-style widgets and opcode `0x90` is not an
immediate hit-test branch. The shared ADV routine copied into all 301 ADV scripts registers five rectangles
at `(684|706|728|750|772,572)`, nominally `20x20`, plus three keyboard/pad records. Native
`op_0x90_handler@0x41fc80` stores `(x,y,x+w,y+h)` and three callback PCs through
`input_hotspot_register_rect_callbacks@0x403d70`; `op 0x94` then arms the input service. The bounds are
compared inclusively.
`input_hotspot_update_cursor_hit@0x403e90` publishes the first matching record index.
`input_hotspot_poll_hover_callback@0x4040b0` maps the stored PCs exactly: operand 5 on pointer entry and
operand 6 on pointer exit. A direct move from one hotspot to another dispatches the old record's exit first
and the new record's entry on the next poll. `input_hotspot_take_click_callback@0x404330` resets the
registry and returns operand 7 on activation. The interpreter temporarily redirects the current script PC
to these callbacks; registration itself returns normally. Companion op `0x97` finds an identical registered
rectangle and binds its fifth operand as an input-bit index; SC0000 uses bits 0, 8, and 7 for its three
`1x1` keyed records.
The five entry callbacks set `G[0x6c9..0x6cd]` one at a time and call the common redraw routine at SC0000
`label_11ffa`; exit clears the corresponding flag and redraws. The redraw takes three pieces from SO001:
- the always-visible base strip from source `(0,254,114,25)` to screen `(681,570)`;
- one 132x27 textual tooltip from source y=227; and
- the same generic 20x20 hover overlay from source `(114,254)` over the selected icon, rendered through
mode-1 `0x203` at alpha `0x80`.
This gives the exact left-to-right behavior:
| x | Hover flag | SO001 tooltip source x | Label/action on activation |
|---:|---|---:|---|
| 684 | `G[0x6c9]` | 396 | **History** — cancel hotspot wait, call `HISTORY.BIN`, then rebuild chrome |
| 706 | `G[0x6ca]` | 264 | **Auto message** — `0x1b6` read, toggle, `0x1b7` write |
| 728 | `G[0x6cb]` | 0 | **Message skip** — `0x88(1)` enables all-message skip |
| 750 | `G[0x6cc]` | 132 | **Read-message skip** — `0x1cb` read, toggle, `0x1ca` write `message:ReadTextSkip` |
| 772 | `G[0x6cd]` | 528 | **Hide window** — `0x199` yields into the ADV/HIDEWIN coroutine flow |
The redraw also overlays persistent active-state cells for Auto, Message skip, and Read-message skip from
SO001 source x `154/174/194` at y=254. After the four state/coroutine actions, the script waits 100 ms,
reads the virtual cursor with `0x109`, alternates its y by one pixel, writes it with `0x10a`, and resets
transient skip/input state through `0x101`. That deliberate cursor jiggle re-arms entry/exit processing.
**No hover sound.** Manual correction on 2026-07-18 confirms these five ADV controls are silent on hover.
This matches the static evidence: none of the entry/exit callbacks executes `0xb4/0xb5` or another script
audio opcode, and the decoded native registration/hover-dispatch chain contains no audio call. The port
should not invent a sound asset or host audio event for this interaction.
The `/v2` Ghidra image names/comments the opcode handlers, registry/hit-test/dispatch helpers, message-skip,
auto-message, read-skip, cursor, and coroutine operations described above; saved 2026-07-18.
**Port implementation (2026-07-18).** The C# VM now models the per-frame registry and dispatches its three
local callbacks on the VM thread through a callback-only host wake channel, leaving the surrounding ADV input
wait parked. Godot feeds scaled native-screen pointer coordinates and consumes activation before ordinary
page advance. The blocking-host model retains the registered definitions across a normal action callback to
represent the native scheduler's subsequent shared-registration pass; explicit op `0x93` still clears them,
and callbacks such as History rebuild through their script path. The existing bytecode therefore owns the
SO001 hover/active redraw rather than a parallel widget layer; callback completion publishes one retained
frame even though the enclosing wait remains static. `0x1b6`/`0x1b7` are implemented as VM service
state for the first Auto action bridge; timed automatic page advance remains separate follow-up work.
**Port correction from manual validation (2026-07-18).** Merely implementing op `0x90` was insufficient in
the cold single-scene harness. The visible SO001 strip is drawn independently, while `jcc@0x8d` skips its five
rectangles when inherited `G[0x6c1]` is zero; the existing trace did exactly that. Even with the native value
one, `cancel-hotspot-wait@0x622` clears the early pass before page-one `wait-for-input@0x83c`, and the native
ADV coroutine later republishes it. Godot now seeds `adv_chrome_enabled=1` as part of the same bounded
SYSTEM4 chrome bootstrap as SO000/SO001. The blocking VM retains canceled definitions only as inactive
coroutine templates, replaces them if script registration runs first, and otherwise re-arms them at a stable
message wait. A full SC0000 regression proves the real History enter callback and retained-frame publication,
not just a synthetic registry path.
### ADV Auto-message timing and voice completion (2026-07-18)
Auto advance is a native input-service policy, not a script sleep or a fixed synthetic click. The native
state is split between `ctx+0x55104` (`auto_message_enabled`) and `ctx+0x6dbe4`
(`adv_auto_voice_pending`). `op_0xc4_handler@0x420610` sets the latter when it queues voice playback, while
`op_0x1bc_handler@0x416c20` clears it at the next message boundary.
`op_0x72_handler@0x41e690` arms `message:AutoMessageTime1` when Auto is enabled and the current message has
no queued voice. For a voiced message, `adv_input_service_poll@0x411230` instead waits until the native voice
service reports playback complete, clears `adv_auto_voice_pending`, and then arms
`message:AutoMessageTime0`. Expiry follows the same wait-release path as ordinary ADV input. Both branches
substitute 100 ms when their configuration getter returns zero.
The two settings are script-visible rather than constants embedded in the wait handler:
- op `0x1b8` reads selector 0 = post-voice `AutoMessageTime0`, selector 1 = unvoiced
`AutoMessageTime1`;
- op `0x1b9` writes the same selectors;
- `CONFIG.BIN` initializes them to 500 ms and 2000 ms respectively, and its UI adjusts either setting in
500-ms steps over 500..9500 ms.
**Port implementation.** The VM retains the enable bit, both configured delays, and per-message voice flag,
then supplies their live state to the blocking host wait. `GodotAdvHost` polls a small deterministic timer
from the existing monotonic frame clock. Unvoiced pages wait Time1; voiced pages remain parked through actual
`AudioStreamPlayer` playback and then wait Time0. A queued/started/completed generation counter closes the
deferred-call race between the VM thread queuing a voice and Godot beginning playback. Turning Auto off
cancels an armed deadline, and turning it back on starts a fresh one. This keeps timing in the host service
boundary and configuration in the VM, without scene offsets, wall-clock sleeps, or Auto-specific input
injection. Timer and VM regressions cover both paths, exact deadlines, disable/re-enable, the native zero
fallback, `0x1b8`/`0x1b9`, and the `0xc4`/`0x1bc` voice-state lifecycle. The `/v2` handlers and input poller
are named/commented and saved.
### ADV retained text — ops `0x7a` / `0x204` and show-text publication (2026-07-10) ### ADV retained text — ops `0x7a` / `0x204` and show-text publication (2026-07-10)
The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`: The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`:

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build --> <!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# Global Variable Reference (generated) # Global Variable Reference (generated)
16415 globals (70 curated, 16345 auto shape-inferred). Source of truth: `vm-map/globals.toml`. 16416 globals (71 curated, 16345 auto shape-inferred). Source of truth: `vm-map/globals.toml`.
## choice-output ## choice-output
@@ -3522,12 +3522,13 @@
| address | name | conf | source | usage | | address | name | conf | source | usage |
|---|---|---|---|---| |---|---|---|---|---|
| `0x6be` | — | low | inference | ADV message-window / text-render state in the 0x6bx-0x6cx chrome cluster (passed to render helpers u0041F9C0/u00415F70 alongside 0x6c3). NOT a story flag — high scene-reach is from the shared render chrome. Branch-read in 146 scenes. | | `0x6be` | — | low | inference | ADV message-window / text-render state in the 0x6bx-0x6cx chrome cluster (passed to render helpers u0041F9C0/u00415F70 alongside 0x6c3). NOT a story flag — high scene-reach is from the shared render chrome. Branch-read in 146 scenes. |
| `0x6c1` | adv_chrome_enabled | high | investigation | Inherited SYSTEM4 UI-boot flag. Standard ADV scripts register the five visible SO001 control-strip pointer rectangles only while nonzero; zero skips directly to the three off-screen keyboard/pad records. The Phase-A single-scene Godot bootstrap seeds the native observed value 1. |
| `0x6c3` | — | low | inference | ADV message-window / text-render state in the 0x6bx-0x6cx chrome cluster (`mov 0x6c3,<val>` then `u00415F70(0x6c3)` right after draw-texture; paired with 0x6be). NOT a story flag. Branch-read in 136 scenes. | | `0x6c3` | — | low | inference | ADV message-window / text-render state in the 0x6bx-0x6cx chrome cluster (`mov 0x6c3,<val>` then `u00415F70(0x6c3)` right after draw-texture; paired with 0x6be). NOT a story flag. Branch-read in 136 scenes. |
| `0x6c9` | ui_toggle_0 | med | investigation | ADV-chrome hotspot button toggle (op 0x90 site, near-universal across scenes). | | `0x6c9` | adv_hover_history | high | investigation | Pointer-hover flag for the standard ADV History button at (684,572). Its op 0x90 enter/leave callbacks set 1/0; the shared SO001 redraw shows the History tooltip plus generic hover overlay while set. |
| `0x6ca` | ui_toggle_1 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. | | `0x6ca` | adv_hover_auto_message | high | investigation | Pointer-hover flag for the standard ADV Auto-message button at (706,572). See adv_hover_history. |
| `0x6cb` | ui_toggle_2 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. | | `0x6cb` | adv_hover_message_skip | high | investigation | Pointer-hover flag for the standard ADV all-message Skip button at (728,572). See adv_hover_history. |
| `0x6cc` | ui_toggle_3 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. | | `0x6cc` | adv_hover_read_message_skip | high | investigation | Pointer-hover flag for the standard ADV read-message-only Skip button at (750,572). See adv_hover_history. |
| `0x6cd` | ui_toggle_4 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. | | `0x6cd` | adv_hover_hide_window | high | investigation | Pointer-hover flag for the standard ADV Hide-window button at (772,572). See adv_hover_history. |
## unknown ## unknown

View File

@@ -6,9 +6,9 @@
## adv ## adv
### 0x72 `wait-for-input` (wait-for-input, argc 1) ### 0x72 `wait-for-input` (wait-for-input, argc 1)
- **summary:** (layout_slot) - arm the ADV input wait after text reveal completes; activates the wait indicator configured for the selected text layout by op 0x73. - **summary:** (layout_slot) - arm the ADV input wait after text reveal completes; activate the configured wait indicator and, while Auto is enabled, arm the appropriate Auto-message timer.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached. - **evidence:** Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached. The handler also checks ctx+0x55104 (Auto enabled): when ctx+0x6dbe4 has no pending voice it arms the timer with message:AutoMessageTime1, substituting 100 ms for configuration value zero. adv_input_service_poll@0x411230 waits for an active voice to finish and then arms AutoMessageTime0, likewise with a 100-ms zero fallback.
### 0x73 `configure-adv-wait-indicator` (configure-adv-wait-indicator, argc 10) ### 0x73 `configure-adv-wait-indicator` (configure-adv-wait-indicator, argc 10)
- **summary:** (layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input. - **summary:** (layout_slot)(dst_x)(dst_y)(surface_slot)(src_x)(src_y)(cell_w)(cell_h)(terminal_frame)(frame_period_ms) - configure the animated marker shown while the selected ADV layout waits for input.
@@ -114,6 +114,11 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
- **grounding:** source=investigation, confidence=med - **grounding:** source=investigation, confidence=med
- **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Writes gfx cmd-type 9; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service. - **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Writes gfx cmd-type 9; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service.
### 0x199 `yield-adv-coroutine` (u00414D50, argc 0)
- **summary:** Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.
- **grounding:** source=investigation, confidence=med
- **evidence:** Ghidra /v2: op_0x199_yield_adv_coroutine@0x416440 selects the registered coroutine yield-A or yield-B PC according to ctx+0x6dbc8, saves the current resume offset/state, and redirects the current frame PC. SC0000's x=772 ADV button invokes it; the SO001 tooltip at source x=528 reads Window hide, and the surrounding coroutine calls HIDEWIN.BIN.
### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1) ### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1)
- **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c. - **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
@@ -328,31 +333,97 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
## input ## input
### 0x90 `hotspot-branch` (u0041BEB0, argc 7) ### 0x88 `set-message-skip` (u0041B290, argc 1)
- **summary:** cursor/input hotspot hit-test: rect (x,y,w,h) -> 3-way branch on interaction, else fall through to pc+1 - **summary:** (enabled) - set the all-message skip state. Nonzero arms ADV fast-forward; zero clears the associated native run-state bit.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x88_set_message_skip@0x41f130 writes operand 1 to ctx+0x13dc and ctx+0x550fc; nonzero also sets ctx+0x6da78=1, while zero clears run-state bit 0x08000000. adv_interpreter_tick injects input bit 0x40 while ctx+0x13dc is nonzero and cancels the state according to set:CancelMesSkipOnClick. SC0000's Message-skip button passes 1.
### 0x90 `register-hotspot-callbacks` (u0041BEB0, argc 7)
- **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.
- **grounding:** source=investigation, confidence=high, noop_headless=True - **grounding:** source=investigation, confidence=high, noop_headless=True
- **depends on:** 0x1f4, 0x1f5
- **depended on by:** 0x97 - **depended on by:** 0x97
- **evidence:** all 301 ADV scripts contain the identical 8 sites; enc.len 15 lands pc+1 on the fall-through stmt (design-confirmed); fall-through = correct headless no-input path, proven by 279 CLEAN dialogue scenes - **evidence:** Ghidra /v2: op_0x90_handler@0x41fc80 fetches x/y/w/h, forms x+w/y+h, fetches operands 5/6/7, and calls input_hotspot_register_rect_callbacks@0x403d70 on the current frame registry. The worker appends rect + callbacks to arrays and returns without changing PC. input_hotspot_poll_hover_callback@0x4040b0 returns operand 5 on entry and operand 6 on exit; input_hotspot_take_click_callback@0x404330 resets the registry and returns operand 7 on activation. All 301 ADV scripts contain the identical five control-strip records plus three keyed records.
op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "ukn" noting args op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "ukn" noting args
5-7 are code locations. Corpus analysis (all 301 ADV scripts) resolves it: 5-7 are code locations. Native RE plus corpus analysis resolves it:
- The opcode APPENDS a record; it is not an immediate branch. Rect bounds are stored as
`(x,y,x+w,y+h)` and the cursor hit-test compares them inclusively.
- The later input service maps targets exactly: target A = pointer enter, target B = pointer leave,
target C = activation/click. Moving directly between records emits leave first, then enter on the
next service poll. Activation consumes/resets the registry before dispatching target C.
- Two forms, both ONLY in one shared ADV-chrome subroutine copied into every ADV script: - Two forms, both ONLY in one shared ADV-chrome subroutine copied into every ADV script:
* Mode A (1505 = 5x301): immediate x,y,w,h with w=h=20; the five on-screen buttons at * Mode A (1505 = 5x301): immediate x,y,w,h with w=h=20; the five on-screen buttons at
(684|706|728|750|772, 572), each setting one of G[0x6c9..0x6cd] to 1 / 0 / 0+run-action (684|706|728|750|772, 572): History, Auto message, Message skip, Read-message skip, and
(reads as hover-enter / hover-leave / click). All 3 targets real. Hide window. Their enter/leave callbacks set G[0x6c9..0x6cd] to 1/0 and redraw SO001's
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real -- a keyed 2-way input test. tooltip + generic 20x20 translucent hover overlay; target C runs the per-button action.
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real. Companion op 0x97 binds
input-bit ids 0, 8, and 7 to these records for keyboard/pad activation.
- Every one of the 301 scripts has EXACTLY 8 sites (5 A + 3 B); zero scene-specific use. - Every one of the 301 scripts has EXACTLY 8 sites (5 A + 3 B); zero scene-specific use.
- Falls through (pc+1) when nothing matches -- design-confirmed (0xd0 + 15 dwords = 0xdf = label_df). - Registration returns normally to pc+1; later callbacks temporarily redirect execution.
- Headless (no cursor/input) => fall through => vm0 stub already correct; the 12 EMPTY sweep scenes - Headless (no cursor/input) can ignore registration; the 12 EMPTY sweep scenes
are gated by state + this input-wait chrome, NOT by unmodelled 0x90. Model live in A2. are gated by state + this input-wait chrome, NOT by unmodelled 0x90. Model live in A2.
### 0x97 `hotspot-reg?` (u0041C150, argc 5) ### 0x93 `cancel-hotspot-wait` (u00415040, argc 0)
- **summary:** companion register-hotspot / set-widget-action (argc5: v1 v2 1 1 <action-id>; NO code targets) - **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.
- **grounding:** source=inference, confidence=med, noop_headless=True - **grounding:** source=investigation, confidence=high, noop_headless=True
- **evidence:** Ghidra /v2: op_0x93_cancel_hotspot_wait@0x416670 clears run-state bit 0x00800000, calls input_hotspot_reset@0x404130, and clears the active flag at ctx+0xc6e4 (or sets the pending flag at +0xc6e0 when already inactive).
### 0x94 `arm-hotspot-wait` (u00415090, argc 0)
- **summary:** Arm native hotspot input processing after the script has registered its rectangles.
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **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.
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **depends on:** 0x90 - **depends on:** 0x90
- **evidence:** interleaves with 0x90 in the shared ADV-chrome subroutine; trailing imm = action id 0x0/0x7/0x8; same widget cluster as 0x90/0x91/0x92/0x95; confirm via frida - **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.
### 0x101 `reset-message-skip-input` (u00415BF0, argc 0)
- **summary:** Reset transient message-skip/input service state after an ADV chrome action.
- **grounding:** source=investigation, confidence=med
- **evidence:** Ghidra /v2: op_0x101_reset_message_skip_input@0x4170a0 resets the input state rooted at ctx+0xa0ce8, clears run-state bit 0x08000000, zeroes ctx+0xa0ce8, and writes ctx+0x6da74=1 / ctx+0x6da80=0. The Auto, Message-skip, Read-skip, and Hide-window click callbacks invoke it after their 100 ms cursor re-arm sequence.
### 0x109 `get-cursor-virtual` (u00415EC0, argc 2)
- **summary:** (out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates.
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **evidence:** Ghidra /v2: op_0x109_get_cursor_virtual@0x428bb0 calls the cursor-position helper, converts client/display coordinates through the active VirtualFullScreen transform, and writes x/y to operands 1/2. The ADV chrome callbacks preserve x and then move y by alternating -1/+1 before op 0x10a.
### 0x10a `set-cursor-virtual` (u0041E540, argc 2)
- **summary:** (x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor.
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **evidence:** Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly.
### 0x19a `get-message-skip` (u00414E50, argc 1)
- **summary:** (out) - return the current all-message skip state set by op 0x88.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19a_get_message_skip@0x4271b0 writes ctx+0x550fc to operand 1. The shared ADV redraw routine uses it to select SO001's active Message-skip icon at x=728.
### 0x1b6 `get-auto-message` (u00414F60, argc 1)
- **summary:** (out) - return whether automatic message advance is enabled.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1b6_get_auto_message@0x4271e0 writes `(ctx+0x55104 != 0)` to operand 1. FUN_00411230 consumes the same field to arm AutoMessageTime0/1 timers; the shared ADV redraw routine uses it for the active Auto icon.
### 0x1b7 `set-auto-message` (u0041B640, argc 1)
- **summary:** (enabled) - enable or disable automatic message advance.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1b7_set_auto_message@0x41f560 stores `(operand1 != 0)` at ctx+0x55104. SC0000's x=706 Auto button reads the current value through 0x1b6, toggles modulo 2, then writes it through this opcode.
### 0x1b8 `get-auto-message-time` (u0041B670, argc 2)
- **summary:** (selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1b8_handler@0x427210 dispatches selector 0 to config key message:AutoMessageTime0 and selector 1 to message:AutoMessageTime1, then writes the result to operand 2. CONFIG.BIN uses it to bound the two UI settings.
### 0x1b9 `set-auto-message-time` (u0041B710, argc 2)
- **summary:** (selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1b9_handler@0x41f5a0 dispatches selector 0/1 to config setters for message:AutoMessageTime0/1. CONFIG.BIN initializes them to 500/2000 ms and adjusts each in 500-ms steps within 500..9500.
### 0x1bc `reset-message-voice-state` (u00415670, argc 0)
- **summary:** Reset the per-message queued-voice flag used by ADV Auto timing.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1bc_handler@0x416c20 writes zero to ctx+0x6dbe4 (adv_auto_voice_pending). Opcode 0xc4 sets that field when voice playback is queued; adv_input_service_poll consumes it to select the post-voice AutoMessageTime0 path.
### 0x1c7 `get-message-skip` (get-message-skip, argc 1) ### 0x1c7 `get-message-skip` (get-message-skip, argc 1)
- **summary:** (out) - write 1 iff ADV message-skip run-state bit 0x08000000 is set, otherwise 0. - **summary:** (out) - write 1 iff ADV message-skip run-state bit 0x08000000 is set, otherwise 0.
@@ -360,11 +431,17 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **depended on by:** 0x20c, 0x20d, 0x21c, 0x223 - **depended on by:** 0x20c, 0x20d, 0x21c, 0x223
- **evidence:** Ghidra handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). SC0000 label_1235a ORs it with op 0x1cc. - **evidence:** Ghidra handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). SC0000 label_1235a ORs it with op 0x1cc.
## marker ### 0x1ca `set-read-message-skip` (u0041B9B0, argc 1)
- **summary:** (enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1ca_set_read_message_skip@0x41f880 calls the engine setting interface's setter for `message:ReadTextSkip` with operand 1. SC0000's x=750 Read-message-skip button toggles the value read by op 0x1cb.
### 0x1bc `block-mark` (u00415670, argc 0) ### 0x1cb `get-read-message-skip` (u00414FD0, argc 1)
- **summary:** zero-arg; follows jcc/mov, precedes mov/ret — block boundary - **summary:** (out) - read the engine setting `message:ReadTextSkip`.
- **grounding:** source=inference, confidence=high, noop_headless=True - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1cb_get_read_message_skip@0x4272f0 calls the engine setting interface's getter for `message:ReadTextSkip` and writes the result to operand 1. The shared ADV redraw routine uses it for the active Read-message-skip icon.
## marker
### 0x1bf `call-end` (u004156C0, argc 0) ### 0x1bf `call-end` (u004156C0, argc 0)
- **summary:** zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker - **summary:** zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker
@@ -381,12 +458,10 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
### 0x1f4 `stmt-begin` (u004160D0, argc 0) ### 0x1f4 `stmt-begin` (u004160D0, argc 0)
- **summary:** zero-arg; opens scripts, pairs with stmt-end 0x1f5 - **summary:** zero-arg; opens scripts, pairs with stmt-end 0x1f5
- **grounding:** source=investigation, confidence=high, noop_headless=True - **grounding:** source=investigation, confidence=high, noop_headless=True
- **depended on by:** 0x90
### 0x1f5 `stmt-end` (u00416120, argc 0) ### 0x1f5 `stmt-end` (u00416120, argc 0)
- **summary:** zero-arg; precedes exit/next-stmt, pairs with 0x1f4 - **summary:** zero-arg; precedes exit/next-stmt, pairs with 0x1f4
- **grounding:** source=investigation, confidence=high, noop_headless=True - **grounding:** source=investigation, confidence=high, noop_headless=True
- **depended on by:** 0x90
### 0x21b `line-id?` (u004213E0, argc 1) ### 0x21b `line-id?` (u004213E0, argc 1)
- **summary:** 1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control - **summary:** 1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control
@@ -581,10 +656,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x88 `u0041B290` (u0041B290, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x8b `u0041B3D0` (u0041B3D0, argc 1) ### 0x8b `u0041B3D0` (u0041B3D0, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
@@ -593,14 +664,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
### 0x93 `u00415040` (u00415040, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x94 `u00415090` (u00415090, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0xa0 `jcc` (jcc, argc 3) ### 0xa0 `jcc` (jcc, argc 3)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
@@ -693,10 +756,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x101 `u00415BF0` (u00415BF0, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x107 `u0041E500` (u0041E500, argc 2) ### 0x107 `u0041E500` (u0041E500, argc 2)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
@@ -705,14 +764,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x109 `u00415EC0` (u00415EC0, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x10a `u0041E540` (u0041E540, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x10b `u0041E5A0` (u0041E5A0, argc 2) ### 0x10b `u0041E5A0` (u0041E5A0, argc 2)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
@@ -829,14 +880,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x199 `u00414D50` (u00414D50, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x19a `u00414E50` (u00414E50, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x19b `u00414E80` (u00414E80, argc 0) ### 0x19b `u00414E80` (u00414E80, argc 0)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
@@ -933,22 +976,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x1b6 `u00414F60` (u00414F60, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1b7 `u0041B640` (u0041B640, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1b8 `u0041B670` (u0041B670, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1b9 `u0041B710` (u0041B710, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1ba `u0041D850` (u0041D850, argc 2) ### 0x1ba `u0041D850` (u0041D850, argc 2)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
@@ -969,14 +996,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
### 0x1ca `u0041B9B0` (u0041B9B0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1cb `u00414FD0` (u00414FD0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1ce `u0041B9F0` (u0041B9F0, argc 1) ### 0x1ce `u0041B9F0` (u0041B9F0, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -1469,3 +1469,66 @@ that lifecycle. The loader consequently builds base `(0,-500)`, so the retained
at `(0,50)`. Op `0x1ff` was also completed as the native immediate current-translation setter at obj+0x16c. at `(0,50)`. Op `0x1ff` was also completed as the native immediate current-translation setter at obj+0x16c.
Validation: engine **155/155**, zero-warning Godot and CLI builds, and a full synchronized run reaching the Validation: engine **155/155**, zero-warning Godot and CLI builds, and a full synchronized run reaching the
exact page-89 wait with `BG004D.AGF` drawn at `(0,50)` alongside the character sprite. exact page-89 wait with `BG004D.AGF` drawn at `(0,50)` alongside the character sprite.
### ADV control-strip button investigation complete (2026-07-18)
Native RE and the shared SC0000 routine now establish the implementation contract for the five visible
lower-right controls: op `0x90` registers enter/leave/activation callbacks, SO001 supplies the base icons,
tooltips, generic alpha-0x80 hover overlay, and persistent state overlays, and the activation callbacks map
left-to-right to History, Auto message, all-message Skip, read-message Skip, and Hide window. Manual
correction confirms the controls are silent on hover, matching the absence of any audio call in the path.
Relevant input/state opcodes and `G[0x6c9..0x6cd]` are named in their canonical registries; the `/v2`
Ghidra image is annotated and saved. Full native evidence is in
[`engine-re.md`](engine-re.md#adv-control-strip-buttons-and-native-hotspot-callbacks-2026-07-18).
### ADV control-strip pointer callbacks + Auto bridge implemented (2026-07-18)
The first bounded runtime slice is now live. `ExecFrame` owns the native-style hotspot registry populated by
ops `0x90`, `0x94`, and `0x97`; Godot routes 800x600 virtual pointer motion into it and consumes an armed
hotspot click before the ordinary ADV page-advance path. Enter, leave, and activation targets execute as
temporary local callbacks on the VM thread while the enclosing `wait-for-input` remains parked. A dedicated
host wake channel services those callbacks without releasing the page semaphore. Inclusive bounds, direct
enter/leave dispatch, activation/re-arm, nested local calls, click consumption, and registry lifetime have
focused regressions. Callback completion requests one retained-frame publication, so static waits show the
new tooltip/glow immediately without continuously recompositing the 800x600 canvas.
This does not introduce duplicate Godot widgets or hover assets: the callbacks already present in every ADV
script redraw the retained SO001 tooltip/glow/state layers. Ops `0x1b6`/`0x1b7` now retain Auto-message state,
so the x=706 action toggles and redraws its active overlay. Auto's timed page advance is not part of this
slice; nor are keyboard/pad activation and the remaining four action services. Validation: engine 157/157,
zero-warning Godot build, threaded `SELFTEST OK`, and SC0000 static coverage 100/129 handled (77.5%;
88 implemented + 12 safe no-ops, 29 GAP ops / 70 instructions).
**Next:** manually validate hover and repeated Auto clicks in a normal SC0000 run, then add Auto timing and
the History/all-skip/read-skip/Hide service behavior one bounded action at a time. No hover-audio work is
required.
**Manual-validation correction (2026-07-18).** The first build displayed the SO001 strip but did not hover.
Its cold profile state took `jcc@0x8d` with `G[0x6c1]==0`, skipping the five visible registrations at
`0x94..0xd0`; later op `0x93@0x622` also canceled the one early registration pass before the first dialogue
wait. Native SYSTEM4 supplies `G[0x6c1]=1`, and its ADV coroutine republishes the shared definitions at the
stable wait. The single-scene bootstrap now supplies that exact inherited flag, retains canceled definitions
as inactive coroutine templates, and re-arms them at `wait-for-input`. Called-script frame exit also restores
the parent's armed registry. A real-SC0000 regression reaches the first wait, moves to History `(684,572)`,
executes `label_1a0`, observes `G[0x6c9]=1`, and verifies callback-frame publication. Validation is now engine
159/159, zero-warning Godot build, and threaded `SELFTEST OK`; manual recheck remains the visual gate.
### ADV Auto timing implemented (2026-07-18)
The follow-up manual check confirms hover and the script-owned toggles work; Auto previously changed only its
SO001 active overlay because timed release had not yet been implemented. Native RE closes that gap without a
scene-specific shortcut. Auto now uses the engine's two configuration channels: unvoiced pages wait
`AutoMessageTime1` (CONFIG default 2000 ms), while voiced pages wait for real voice completion and then apply
`AutoMessageTime0` (default 500 ms). Ops `0x1b8`/`0x1b9` read/write those delays, op `0xc4` marks a queued
voice, and op `0x1bc` resets the per-message voice state. A configured zero retains the native 100-ms
fallback.
The implementation keeps VM-owned settings separate from host-owned clock/audio state. The Godot wait loop
polls the existing monotonic frame clock and an interrupt-safe voice generation counter; it does not enqueue
a fake click or sleep the VM thread for a hard-coded duration. Focused regressions cover unvoiced and voiced
deadlines, live Auto disable/re-enable, zero fallback, the time-setting ABI, and voice-state reset. Validation:
engine 165/165, opcode and EngineCtx generators/tests/lints clean, Godot build has no compiler/analyzer
warnings, and threaded `SELFTEST OK`.
**Next:** manually validate Auto advancing both an unvoiced page and a voiced page, then take the remaining
History/all-message-skip/read-message-skip/Hide actions as separate bounded services.

View File

@@ -0,0 +1,53 @@
using Age.Engine.Hosting;
using Xunit;
public class AdvAutoAdvanceTimerTests
{
[Fact]
public void UnvoicedWait_UsesAutoMessageTime1()
{
var timer = new AdvAutoAdvanceTimer();
var state = new AdvAutoWaitState(true, false, 500, 2000);
Assert.False(timer.Poll(state, false, 1000));
Assert.False(timer.Poll(state, false, 2999));
Assert.True(timer.Poll(state, false, 3000));
}
[Fact]
public void VoicedWait_WaitsForVoiceThenUsesAutoMessageTime0()
{
var timer = new AdvAutoAdvanceTimer();
var state = new AdvAutoWaitState(true, true, 500, 2000);
Assert.False(timer.Poll(state, true, 1000));
Assert.False(timer.Poll(state, true, 9000));
Assert.False(timer.Poll(state, false, 9000));
Assert.False(timer.Poll(state, false, 9499));
Assert.True(timer.Poll(state, false, 9500));
}
[Fact]
public void DisablingAuto_CancelsDeadlineAndReenableStartsFresh()
{
var timer = new AdvAutoAdvanceTimer();
var on = new AdvAutoWaitState(true, false, 500, 2000);
Assert.False(timer.Poll(on, false, 0));
Assert.False(timer.Poll(on with { Enabled = false }, false, 1500));
Assert.False(timer.Poll(on, false, 5000));
Assert.False(timer.Poll(on, false, 6999));
Assert.True(timer.Poll(on, false, 7000));
}
[Fact]
public void ZeroConfiguration_UsesNativeHundredMillisecondFallback()
{
var timer = new AdvAutoAdvanceTimer();
var state = new AdvAutoWaitState(true, false, 0, 0);
Assert.False(timer.Poll(state, false, 0));
Assert.False(timer.Poll(state, false, 99));
Assert.True(timer.Poll(state, false, 100));
}
}

View File

@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class HotspotInputTests
{
private sealed class StopAtFirstWaitException : Exception { }
private sealed class InteractiveHost : RecordingHost
{
public VirtualMachine Vm = null!;
public bool ClickConsumed;
public bool SecondClickConsumed;
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
Waits++;
// Width/height are added to x/y, so the native rectangle includes (30,40).
Vm.UpdatePointer(30, 40);
while (serviceInputCallback()) { }
Assert.Equal(1, Vm.Globals.GetValueOrDefault(0x100));
Vm.UpdatePointer(31, 40);
while (serviceInputCallback()) { }
Assert.Equal(1, Vm.Globals.GetValueOrDefault(0x101));
Vm.UpdatePointer(30, 40);
while (serviceInputCallback()) { }
ClickConsumed = Vm.TryActivatePointer(30, 40);
while (serviceInputCallback()) { }
SecondClickConsumed = Vm.TryActivatePointer(30, 40);
while (serviceInputCallback()) { }
}
}
private sealed class Sc0000HoverHost : RecordingHost
{
public VirtualMachine Vm = null!;
public bool SawHistoryHover;
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
Vm.UpdatePointer(684, 572);
while (serviceInputCallback()) { }
SawHistoryHover = Vm.Globals.GetValueOrDefault(0x6c9) == 1;
throw new StopAtFirstWaitException();
}
}
private sealed class AutoStateHost : RecordingHost
{
public AdvAutoWaitState State;
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
Func<AdvAutoWaitState> autoWaitState)
{
while (serviceInputCallback()) { }
State = autoWaitState();
Waits++;
}
}
[Fact]
public void ArmedHotspot_DispatchesHoverAndConsumesActivationWithoutAdvancingPage()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
// Main code ends at dword 20. Activation also exercises an ordinary local call beneath the
// VM's temporary callback return sentinel.
const int enterTarget = 20, leaveTarget = 26, activateTarget = 32;
var script = ScriptAssembler.Assemble(table, "HOTSPOT", new List<(int, Operand[])>
{
(0x90, new[] { I(10), I(20), I(20), I(20), I(enterTarget), I(leaveTarget), I(activateTarget) }),
(0x94, Array.Empty<Operand>()),
(0x72, new[] { I(1) }),
(0x2, Array.Empty<Operand>()),
(0x55, new[] { G(0x100), I(1) }),
(0x5, Array.Empty<Operand>()),
(0x55, new[] { G(0x101), I(1) }),
(0x5, Array.Empty<Operand>()),
(0x8f, new[] { I(36) }),
(0x5, Array.Empty<Operand>()),
(0x1b7, new[] { I(1) }),
(0x5, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new InteractiveHost();
var vm = new VirtualMachine(script, table, host);
host.Vm = vm;
vm.Run();
Assert.True(host.ClickConsumed);
Assert.True(host.SecondClickConsumed);
Assert.True(vm.AutoMessageEnabled);
Assert.Equal(1, host.Waits);
Assert.Equal(7, host.InputCallbackFrames);
Assert.Equal("exit", vm.HaltReason);
Assert.False(vm.TryActivatePointer(30, 40)); // the script frame has exited, so no hotspot remains active
}
[Fact]
public void Sc0000AdvChromeBootstrap_VisitsAllFiveVisibleButtonRegistrations()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table);
var trace = new RecordingTraceSink { TracingSteps = true };
var vm = new VirtualMachine(script, table, new RecordingHost(),
new VmOptions(HaltAtWaitForInput: true), sink: trace);
vm.Globals[0x6c1] = 1; // inherited SYSTEM4 adv_chrome_enabled state seeded by Godot Main
vm.Run();
Assert.Equal(new[] { 0x94, 0xa3, 0xb2, 0xc1, 0xd0 }, trace.Events
.Where(e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.Step
&& e.Opcode == 0x90 && e.Ins!.Offset < 0xdf)
.Select(e => e.Ins!.Offset).ToArray());
Assert.Equal("wait-for-input", vm.HaltReason);
}
[Fact]
public void Sc0000FirstWait_DispatchesRealHistoryHoverCallback()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table);
var host = new Sc0000HoverHost();
var vm = new VirtualMachine(script, table, host, new VmOptions(MaxSteps: 1_000_000));
host.Vm = vm;
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAtFirstWaitException>(() => vm.Run());
Assert.True(host.InputCallbackFrames > 0);
Assert.True(host.SawHistoryHover);
}
[Fact]
public void AutoMessageOpcodes_RoundTripVmServiceState()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "AUTO", new List<(int, Operand[])>
{
(0x1b7, new[] { I(1) }),
(0x1b6, new[] { G(0x120) }),
(0x1b7, new[] { I(0) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.Run();
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x120));
Assert.False(vm.AutoMessageEnabled);
}
[Fact]
public void AutoMessageTimeOpcodes_ConfigureWaitSchedulerState()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "AUTO_TIMES", new List<(int, Operand[])>
{
(0x1b9, new[] { I(0), I(750) }),
(0x1b9, new[] { I(1), I(2250) }),
(0x1b8, new[] { I(0), G(0x130) }),
(0x1b8, new[] { I(1), G(0x131) }),
(0x1b7, new[] { I(1) }),
(0x72, new[] { I(1) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new AutoStateHost();
var vm = new VirtualMachine(script, table, host);
vm.Run();
Assert.Equal(750, vm.Globals.GetValueOrDefault(0x130));
Assert.Equal(2250, vm.Globals.GetValueOrDefault(0x131));
Assert.Equal(new AdvAutoWaitState(true, false, 750, 2250), host.State);
Assert.Equal(1, host.Waits);
}
[Fact]
public void VoicePlayback_MarksAutoWaitUntilBlockMarkResetsIt()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var voiced = ScriptAssembler.Assemble(table, "AUTO_VOICE", new List<(int, Operand[])>
{
(0xc4, new[] { I(12) }),
(0x72, new[] { I(1) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var reset = ScriptAssembler.Assemble(table, "AUTO_VOICE_RESET", new List<(int, Operand[])>
{
(0xc4, new[] { I(12) }),
(0x1bc, Array.Empty<Operand>()),
(0x72, new[] { I(1) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var voicedHost = new AutoStateHost();
var resetHost = new AutoStateHost();
new VirtualMachine(voiced, table, voicedHost).Run();
new VirtualMachine(reset, table, resetHost).Run();
Assert.True(voicedHost.State.VoicePending);
Assert.False(resetHost.State.VoicePending);
}
private static Operand I(long value) => new(0, value);
private static Operand G(long address) => new(3, address);
}

View File

@@ -6,11 +6,12 @@ using Age.Engine.Model;
/// <summary>Shared test doubles: a host that records observable effects, and an in-memory script /// <summary>Shared test doubles: a host that records observable effects, and an in-memory script
/// provider for synthetic call-script targets.</summary> /// provider for synthetic call-script targets.</summary>
internal sealed class RecordingHost : IHost internal class RecordingHost : IHost
{ {
public int Waits; public int Waits;
public int Presents; public int Presents;
public int TransitionWaits; public int TransitionWaits;
public int InputCallbackFrames;
public bool MessageSkip; public bool MessageSkip;
public bool AdvReadSkip; public bool AdvReadSkip;
public readonly List<(int Offset, string Text)> Lines = new(); public readonly List<(int Offset, string Text)> Lines = new();
@@ -29,6 +30,15 @@ internal sealed class RecordingHost : IHost
=> SurfaceStrings.Add((surfaceSlot, x, y, text)); => SurfaceStrings.Add((surfaceSlot, x, y, text));
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config); public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
public void WaitForInput() => Waits++; public void WaitForInput() => Waits++;
public virtual void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
while (serviceInputCallback()) { }
WaitForInput();
}
public virtual void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
Func<AdvAutoWaitState> autoWaitState)
=> WaitForInput(layoutSlot, serviceInputCallback);
public void InputCallbackCompleted(GfxState gfx) => InputCallbackFrames++;
public void Sleep(long duration) => SleptDurations.Add(duration); public void Sleep(long duration) => SleptDurations.Add(duration);
public void FrameYield() { } public void FrameYield() { }
public bool IsMessageSkipActive => MessageSkip; public bool IsMessageSkipActive => MessageSkip;

View File

@@ -0,0 +1,50 @@
namespace Age.Engine.Hosting;
/// <summary>
/// Native ADV Auto timing policy. Unvoiced waits use AutoMessageTime1; voiced waits park until voice
/// completion and then use AutoMessageTime0. The host supplies a monotonic clock and live voice state.
/// </summary>
public sealed class AdvAutoAdvanceTimer
{
private bool _armed;
private bool _waitingForVoice;
private long _deadlineMs;
public void Reset()
{
_armed = false;
_waitingForVoice = false;
_deadlineMs = 0;
}
public bool Poll(AdvAutoWaitState state, bool voiceActive, long nowMs)
{
if (!state.Enabled)
{
Reset();
return false;
}
if (!_armed)
{
_armed = true;
_waitingForVoice = state.VoicePending && voiceActive;
if (!_waitingForVoice)
_deadlineMs = nowMs + EffectiveDelay(
state.VoicePending ? state.PostVoiceDelayMs : state.UnvoicedDelayMs);
}
if (_waitingForVoice)
{
if (voiceActive) return false;
_waitingForVoice = false;
_deadlineMs = nowMs + EffectiveDelay(state.PostVoiceDelayMs);
}
return nowMs >= _deadlineMs;
}
// Native callers substitute 100 ms when either configuration getter returns zero.
private static long EffectiveDelay(long configuredMs)
=> configuredMs == 0 ? 100 : System.Math.Max(1, configuredMs);
}

View File

@@ -7,6 +7,9 @@ public readonly record struct AdvWaitIndicatorConfig(
int SourceX, int SourceY, int CellWidth, int CellHeight, int SourceX, int SourceY, int CellWidth, int CellHeight,
int TerminalFrame, long FramePeriodMs); int TerminalFrame, long FramePeriodMs);
public readonly record struct AdvAutoWaitState(
bool Enabled, bool VoicePending, long PostVoiceDelayMs, long UnvoicedDelayMs);
public interface IHost public interface IHost
{ {
void ShowText(int offset, string text); void ShowText(int offset, string text);
@@ -17,6 +20,18 @@ public interface IHost
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { } void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
void WaitForInput(); void WaitForInput();
void WaitForInput(int layoutSlot) => WaitForInput(); void WaitForInput(int layoutSlot) => WaitForInput();
// Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains
// parked. The callback returns true while another queued input callback is ready to run.
void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
while (serviceInputCallback()) { }
WaitForInput(layoutSlot);
}
void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
Func<AdvAutoWaitState> autoWaitState)
=> WaitForInput(layoutSlot, serviceInputCallback);
void WakeInputCallbackService() { }
void InputCallbackCompleted(GfxState gfx) { }
void Sleep(long duration); void Sleep(long duration);
void FrameYield(); void FrameYield();
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive // Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive

View File

@@ -14,5 +14,6 @@ internal sealed class ExecFrame
public int? CoroutineYieldHandlerA; // op 0x7b: native per-frame handler PCs public int? CoroutineYieldHandlerA; // op 0x7b: native per-frame handler PCs
public int? CoroutineYieldHandlerB; public int? CoroutineYieldHandlerB;
public readonly Dictionary<int, int> CoroutineYieldVisits = new(); // instruction index -> visits public readonly Dictionary<int, int> CoroutineYieldVisits = new(); // instruction index -> visits
public readonly HotspotRegistry Hotspots = new();
public ExecFrame(Script script, int pc) { Script = script; Pc = pc; } public ExecFrame(Script script, int pc) { Script = script; Pc = pc; }
} }

View File

@@ -0,0 +1,109 @@
namespace Age.Engine.Vm;
/// <summary>Per-script-frame registry populated by SYS4 ops 0x90/0x94/0x97.</summary>
internal sealed class HotspotRegistry
{
private sealed class Entry
{
public int Left, Top, Right, Bottom;
public int EnterTarget, LeaveTarget, ActivateTarget;
public int? InputBit;
public bool Contains(int x, int y)
=> x >= Left && x <= Right && y >= Top && y <= Bottom;
}
private readonly List<Entry> _entries = new();
private readonly Queue<int> _pendingTargets = new();
private int _hovered = -1;
private bool _replaceOnNextRegister;
public bool Armed { get; private set; }
public bool HasDefinitions => _entries.Count != 0;
public void Register(int x, int y, int width, int height,
int enterTarget, int leaveTarget, int activateTarget)
{
if (_replaceOnNextRegister)
{
_entries.Clear();
_replaceOnNextRegister = false;
}
_entries.Add(new Entry
{
Left = x, Top = y, Right = x + width, Bottom = y + height,
EnterTarget = enterTarget, LeaveTarget = leaveTarget,
ActivateTarget = activateTarget,
});
}
public void BindKey(int x, int y, int width, int height, int inputBit)
{
int right = x + width, bottom = y + height;
var entry = _entries.FirstOrDefault(e => e.Left == x && e.Top == y
&& e.Right == right && e.Bottom == bottom);
if (entry != null) entry.InputBit = inputBit;
}
public bool Arm(int pointerX, int pointerY)
{
_replaceOnNextRegister = false;
Armed = true;
return UpdatePointer(pointerX, pointerY);
}
public bool UpdatePointer(int x, int y)
{
if (!Armed) return false;
int hit = FindHit(x, y);
if (hit == _hovered) return false;
if (_hovered >= 0) QueueTarget(_entries[_hovered].LeaveTarget);
_hovered = hit;
if (_hovered >= 0) QueueTarget(_entries[_hovered].EnterTarget);
return _pendingTargets.Count != 0;
}
public bool Activate(int x, int y)
{
if (!Armed) return false;
int hit = FindHit(x, y);
if (hit < 0) return false;
int target = _entries[hit].ActivateTarget;
// Native consumes the active input registration before dispatch. Its ADV scheduler revisits the
// shared registration routine afterward; retain the definitions so the blocking host can model
// that revisit without advancing the enclosing dialogue page.
_pendingTargets.Clear();
_hovered = -1;
Armed = false;
QueueTarget(target);
return true;
}
public bool RearmAfterCallback(int pointerX, int pointerY)
=> !Armed && _entries.Count != 0 && Arm(pointerX, pointerY);
public bool TryDequeue(out int target) => _pendingTargets.TryDequeue(out target);
public void Reset()
{
_pendingTargets.Clear();
_hovered = -1;
Armed = false;
// The native ADV coroutine can republish the shared definitions after a cancellation. If script
// code explicitly registers again first, replace this retained template instead of duplicating it.
_replaceOnNextRegister = _entries.Count != 0;
}
private int FindHit(int x, int y)
{
for (int i = 0; i < _entries.Count; i++)
if (_entries[i].Contains(x, y)) return i;
return -1;
}
private void QueueTarget(int target)
{
if (target >= 0 && (uint)target != uint.MaxValue) _pendingTargets.Enqueue(target);
}
}

View File

@@ -8,6 +8,7 @@ public sealed class VirtualMachine
private const long NoJump = 0xFFFFFFFF; private const long NoJump = 0xFFFFFFFF;
private const int HALT = int.MinValue; private const int HALT = int.MinValue;
private const int FRAME_RETURN = int.MinValue + 1; private const int FRAME_RETURN = int.MinValue + 1;
private const int HOTSPOT_RETURN = int.MinValue + 2;
private const int SceneEntryCoroutineGate = 0xaba5c; private const int SceneEntryCoroutineGate = 0xaba5c;
private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6, private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6,
T_GSTRPTR = 8, T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12, T_GSTRPTR = 8, T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12,
@@ -22,6 +23,13 @@ public sealed class VirtualMachine
private ExecFrame _cur = null!; private ExecFrame _cur = null!;
private int _depth; private int _depth;
private readonly ITraceSink _sink; private readonly ITraceSink _sink;
private readonly object _interactiveLock = new();
private ExecFrame? _interactiveFrame;
private int _pointerX = int.MinValue, _pointerY = int.MinValue;
private bool _autoMessageEnabled;
private long _autoMessageTime0Ms = 500;
private long _autoMessageTime1Ms = 2000;
private bool _autoVoicePending;
public long CallScriptDispatches { get; private set; } public long CallScriptDispatches { get; private set; }
public Dictionary<int, long> Globals { get; } = new(); public Dictionary<int, long> Globals { get; } = new();
@@ -32,12 +40,43 @@ public sealed class VirtualMachine
public List<(int Offset, string Text, string Script)> Emitted { get; } = new(); public List<(int Offset, string Text, string Script)> Emitted { get; } = new();
public string? HaltReason { get; private set; } public string? HaltReason { get; private set; }
public long Steps { get; private set; } public long Steps { get; private set; }
public bool AutoMessageEnabled => _autoMessageEnabled;
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null, public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null,
IScriptProvider? provider = null, ITraceSink? sink = null) IScriptProvider? provider = null, ITraceSink? sink = null)
{ _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider; { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider;
_sink = sink ?? NullTraceSink.Instance; } _sink = sink ?? NullTraceSink.Instance; }
/// <summary>Update the native 800x600 cursor coordinate without advancing the current ADV page.</summary>
public void UpdatePointer(int x, int y)
{
bool wake = false;
lock (_interactiveLock)
{
_pointerX = x; _pointerY = y;
if (_interactiveFrame != null)
wake = _interactiveFrame.Hotspots.UpdatePointer(x, y);
}
if (wake) _host.WakeInputCallbackService();
}
/// <summary>Queue an armed hotspot's activation callback. True means the click was consumed.</summary>
public bool TryActivatePointer(int x, int y)
{
bool consumed = false;
lock (_interactiveLock)
{
_pointerX = x; _pointerY = y;
if (_interactiveFrame != null)
{
_interactiveFrame.Hotspots.UpdatePointer(x, y);
consumed = _interactiveFrame.Hotspots.Activate(x, y);
}
}
if (consumed) _host.WakeInputCallbackService();
return consumed;
}
private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0; private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0;
private long ReadGlobal(int k) => ExternalGlobals.TryGetValue(k, out var v) ? v : Gi(Globals, k); private long ReadGlobal(int k) => ExternalGlobals.TryGetValue(k, out var v) ? v : Gi(Globals, k);
private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : ""; private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : "";
@@ -156,6 +195,8 @@ public sealed class VirtualMachine
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0) private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
{ {
ExecFrame? previousInteractiveFrame;
lock (_interactiveLock) previousInteractiveFrame = _interactiveFrame;
var prev = _cur; _cur = frame; _depth++; var prev = _cur; _cur = frame; _depth++;
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId)); _sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
var outcome = FrameOutcome.RanOff; var outcome = FrameOutcome.RanOff;
@@ -172,10 +213,49 @@ public sealed class VirtualMachine
pc = next; pc = next;
} }
_sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString())); _sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString()));
lock (_interactiveLock)
{
if (cause == FrameCause.CallScript)
_interactiveFrame = previousInteractiveFrame?.Hotspots.Armed == true
? previousInteractiveFrame : null;
else if (ReferenceEquals(_interactiveFrame, frame))
_interactiveFrame = null;
}
_cur = prev; _depth--; _cur = prev; _depth--;
return outcome; return outcome;
} }
private bool ServiceHotspotCallback()
{
int target;
lock (_interactiveLock)
{
if (_interactiveFrame == null || !_interactiveFrame.Hotspots.TryDequeue(out target)) return false;
}
if (_cur.Script.IndexByOffset.TryGetValue(target, out int pc))
{
_cur.CallStack.Add(HOTSPOT_RETURN);
while (pc >= 0 && pc < _cur.Script.Instructions.Count)
{
if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; break; }
Steps++;
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, _cur.Script.Instructions[pc], _depth));
int next = Step(_cur.Script.Instructions[pc], pc);
_host.FrameYield();
if (next == HOTSPOT_RETURN || next == FRAME_RETURN) break;
if (next == HALT) break;
pc = next;
}
// A malformed callback must not leave its sentinel in the page's ordinary local-call stack.
int sentinel = _cur.CallStack.LastIndexOf(HOTSPOT_RETURN);
if (sentinel >= 0) _cur.CallStack.RemoveAt(sentinel);
}
lock (_interactiveLock)
_interactiveFrame?.Hotspots.RearmAfterCallback(_pointerX, _pointerY);
_host.InputCallbackCompleted(Gfx);
return true;
}
private int Step(Instruction ins, int pc) private int Step(Instruction ins, int pc)
{ {
int op = ins.Opcode; int op = ins.Opcode;
@@ -295,7 +375,55 @@ public sealed class VirtualMachine
case "wait-for-input": case "wait-for-input":
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions). // Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; } if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
_host.WaitForInput((int)Read(a[0])); return pc + 1; // The native ADV chrome is a coroutine: after an earlier 0x93 cancellation its shared
// registration pass runs again before a stable message wait. Our blocking host models that
// scheduler boundary by re-arming the frame's retained definitions here.
bool wakeAtWait = false;
lock (_interactiveLock)
{
if (_cur.Hotspots.HasDefinitions && !_cur.Hotspots.Armed)
{
_interactiveFrame = _cur;
wakeAtWait = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
}
if (wakeAtWait) _host.WakeInputCallbackService();
_host.WaitForInput((int)Read(a[0]), ServiceHotspotCallback,
() => new AdvAutoWaitState(_autoMessageEnabled, _autoVoicePending,
_autoMessageTime0Ms, _autoMessageTime1Ms));
return pc + 1;
case "u0041BEB0":
case "register-hotspot-callbacks": // 0x90: inclusive rect + enter/leave/activate local callbacks
lock (_interactiveLock)
_cur.Hotspots.Register((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]));
return pc + 1;
case "u00415040":
case "cancel-hotspot-wait": // 0x93
lock (_interactiveLock)
{
_cur.Hotspots.Reset();
if (ReferenceEquals(_interactiveFrame, _cur)) _interactiveFrame = null;
}
return pc + 1;
case "u00415090":
case "arm-hotspot-wait": // 0x94
{
bool wake;
lock (_interactiveLock)
{
_interactiveFrame = _cur;
wake = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
if (wake) _host.WakeInputCallbackService();
return pc + 1;
}
case "u0041C150":
case "bind-hotspot-key": // 0x97: keyboard/pad routing is a later host-input slice
lock (_interactiveLock)
_cur.Hotspots.BindKey((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]),
(int)Read(a[3]), (int)Read(a[4]));
return pc + 1;
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
_host.Sleep(Read(a[0])); return pc + 1; _host.Sleep(Read(a[0])); return pc + 1;
case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit case "get-message-skip": // 0x1c7: Ctrl/message fast-forward run-state bit
@@ -303,6 +431,24 @@ public sealed class VirtualMachine
case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state
case "get-adv-service-state": // compatibility with pre-recovery generated tables case "get-adv-service-state": // compatibility with pre-recovery generated tables
Write(a[0], _host.IsAdvReadSkipActive ? 1 : 0); return pc + 1; Write(a[0], _host.IsAdvReadSkipActive ? 1 : 0); return pc + 1;
case "u00414F60":
case "get-auto-message": // 0x1b6: VM service state used by the ADV redraw callback
Write(a[0], _autoMessageEnabled ? 1 : 0); return pc + 1;
case "u0041B640":
case "set-auto-message": // 0x1b7
_autoMessageEnabled = Read(a[0]) != 0; return pc + 1;
case "u0041B670":
case "get-auto-message-time": // 0x1b8 (selector 0=post-voice Time0, 1=unvoiced Time1, out)
Write(a[1], Read(a[0]) == 0 ? _autoMessageTime0Ms : _autoMessageTime1Ms); return pc + 1;
case "u0041B710":
case "set-auto-message-time": // 0x1b9 (selector, milliseconds)
if (Read(a[0]) == 0) _autoMessageTime0Ms = Read(a[1]);
else if (Read(a[0]) == 1) _autoMessageTime1Ms = Read(a[1]);
return pc + 1;
case "u00415670":
case "block-mark":
case "reset-message-voice-state": // 0x1bc resets native per-message voice/queued-voice state
_autoVoicePending = false; return pc + 1;
case "end-text-line": case "set-font": case "end-text-line": case "set-font":
case "comment": case "display-furigana": case "dev_ukn": case "comment": case "display-furigana": case "dev_ukn":
return pc + 1; return pc + 1;
@@ -329,7 +475,9 @@ public sealed class VirtualMachine
return pc + 1; return pc + 1;
} }
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1; case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice": _host.PlayVoice(Read(a[0])); return pc + 1; case "play-voice":
_autoVoicePending = true;
_host.PlayVoice(Read(a[0])); return pc + 1;
case "play-sound-effect": // 0xb4 / semantics: sfx-load case "play-sound-effect": // 0xb4 / semantics: sfx-load
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1; _host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
case "u0041D050": // 0xb5 / semantics: sfx-start case "u0041D050": // 0xb5 / semantics: sfx-start

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using Age.Engine.Hosting; using Age.Engine.Hosting;
using Age.Engine.Model; using Age.Engine.Model;
using Age.Engine.Sys4; using Age.Engine.Sys4;
[SupportedOSPlatform("windows")]
public sealed class GodotAdvHost : IHost public sealed class GodotAdvHost : IHost
{ {
private readonly Main _main; private readonly Main _main;
@@ -20,6 +23,7 @@ public sealed class GodotAdvHost : IHost
// primary surface, but op 0x1fa releases it like any other slot; subsequent size queries must return 0x0. // primary surface, but op 0x1fa releases it like any other slot; subsequent size queries must return 0x0.
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } }; private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
private readonly SemaphoreSlim _gate = new(0, 1); private readonly SemaphoreSlim _gate = new(0, 1);
private readonly AutoResetEvent _inputCallbackSignal = new(false);
private readonly Age.Engine.Hosting.FrameClock _clock; private readonly Age.Engine.Hosting.FrameClock _clock;
private readonly GodotTimelineLog? _timeline; private readonly GodotTimelineLog? _timeline;
private readonly PageLocatorState _locator; private readonly PageLocatorState _locator;
@@ -147,6 +151,13 @@ public sealed class GodotAdvHost : IHost
public void WaitForInput() => WaitForInput(0); public void WaitForInput() => WaitForInput(0);
public void WaitForInput(int layoutSlot) public void WaitForInput(int layoutSlot)
=> WaitForInput(layoutSlot, static () => false, static () => default);
public void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
=> WaitForInput(layoutSlot, serviceInputCallback, static () => default);
public void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
Func<AdvAutoWaitState> autoWaitState)
{ {
Pages++; Pages++;
_locator.Wait(Pages); _locator.Wait(Pages);
@@ -158,9 +169,22 @@ public sealed class GodotAdvHost : IHost
_waitIndicatorStartedMs = _clock.NowMs; _waitIndicatorStartedMs = _clock.NowMs;
IsWaiting = true; IsWaiting = true;
_timeline?.State("input-wait", new() { ["page"] = Pages }); _timeline?.State("input-wait", new() { ["page"] = Pages });
_gate.Wait(); var autoTimer = new AdvAutoAdvanceTimer();
bool autoAdvanced = false;
while (!_stopping)
{
while (serviceInputCallback()) { }
if (autoTimer.Poll(autoWaitState(), _main.IsVoicePlaybackActive, _clock.NowMs))
{
autoAdvanced = true;
break;
}
if (_gate.Wait(0)) break;
WaitHandle.WaitAny(new[] { _gate.AvailableWaitHandle, _inputCallbackSignal, _frameSignal });
if (_gate.Wait(0)) break;
}
IsWaiting = false; IsWaiting = false;
_timeline?.State("running", new() { ["input"] = "auto-or-user" }); _timeline?.State("running", new() { ["input"] = autoAdvanced ? "auto" : "user" });
lock (_textLock) lock (_textLock)
{ {
_advText = ""; _advText = "";
@@ -194,6 +218,11 @@ public sealed class GodotAdvHost : IHost
if (IsWaiting && _gate.CurrentCount == 0) _gate.Release(); if (IsWaiting && _gate.CurrentCount == 0) _gate.Release();
} }
public void WakeInputCallbackService() => _inputCallbackSignal.Set();
public void InputCallbackCompleted(GfxState gfx)
=> Interlocked.Exchange(ref _presentRequested, 1);
public void WaitForForegroundTransition(GfxState gfx) public void WaitForForegroundTransition(GfxState gfx)
{ {
int started = gfx.StartForegroundTransitions(_clock.NowMs); int started = gfx.StartForegroundTransitions(_clock.NowMs);
@@ -251,6 +280,7 @@ public sealed class GodotAdvHost : IHost
_stopping = true; _stopping = true;
lock (_textLock) _advTextForceComplete = true; lock (_textLock) _advTextForceComplete = true;
if (_gate.CurrentCount == 0) _gate.Release(); if (_gate.CurrentCount == 0) _gate.Release();
_inputCallbackSignal.Set();
_frameSignal.Set(); _frameSignal.Set();
} }
@@ -440,7 +470,11 @@ public sealed class GodotAdvHost : IHost
{ {
var asset = _res.Resolve(_scene, id); var asset = _res.Resolve(_scene, id);
var audio = asset != null ? LoadAudio(asset) : null; var audio = asset != null ? LoadAudio(asset) : null;
if (audio != null) _main.CallDeferred("PlayVoice", audio.Bytes, audio.Name); if (audio != null)
{
int generation = _main.QueueVoicePlayback();
_main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation);
}
} }
public void LoadSoundEffect(long resourceId, int channel) public void LoadSoundEffect(long resourceId, int channel)

View File

@@ -30,6 +30,9 @@ public partial class Main : Godot.Control
private Label _locatorHud = null!; private Label _locatorHud = null!;
private AudioStreamPlayer _bgm = null!; // looping background music private AudioStreamPlayer _bgm = null!; // looping background music
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
private int _voiceQueuedGeneration;
private int _voiceStartedGeneration;
private int _voiceCompletedGeneration;
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9 private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
private VirtualMachine _vm = null!; private VirtualMachine _vm = null!;
private GodotAdvHost _host = null!; private GodotAdvHost _host = null!;
@@ -231,6 +234,10 @@ public partial class Main : Godot.Control
foreach (var kv in session.GlobalStrings) _vm.GlobalStrings[kv.Key] = kv.Value; foreach (var kv in session.GlobalStrings) _vm.GlobalStrings[kv.Key] = kv.Value;
GD.Print($"[boot] system boot done: {session.Globals.Count} globals seeded"); GD.Print($"[boot] system boot done: {session.Globals.Count} globals seeded");
} }
// The native SYSTEM4 UI boot enables standard ADV chrome after the data-only *INIT prefix above.
// Without this inherited value the visible SO001 strip is still drawn, but every ADV script skips
// its five pointer rectangles and registers only the off-screen keyboard/pad records.
if (!_selftest) _vm.Globals[0x6c1] = 1;
// Native AGE owns this transient secondary-SFX channel outside script-visible writes. // Native AGE owns this transient secondary-SFX channel outside script-visible writes.
// The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice. // The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice.
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase)) if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
@@ -265,6 +272,7 @@ public partial class Main : Godot.Control
_timelineFrame++; _timelineFrame++;
_timeline?.SetFrame(_timelineFrame, _clock.NowMs); _timeline?.SetFrame(_timelineFrame, _clock.NowMs);
_host?.PulseFrame(); _host?.PulseFrame();
UpdateVoicePlaybackState();
UpdateMovieFrames(); UpdateMovieFrames();
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx)) if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
Recomposite(); // native publishes retained mutations only at present/service boundaries Recomposite(); // native publishes retained mutations only at present/service boundaries
@@ -325,9 +333,32 @@ public partial class Main : Godot.Control
_locatorHud.Text = _locator.CurrentDisplay + " · copied"; _locatorHud.Text = _locator.CurrentDisplay + " · copied";
return; return;
} }
if (e.IsActionPressed("ui_accept") || if (e is InputEventMouseMotion motion)
(e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)) {
var p = ToNativeScreen(motion.Position);
_vm.UpdatePointer(p.X, p.Y);
return;
}
if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
{
var p = ToNativeScreen(mb.Position);
if (_vm.TryActivatePointer(p.X, p.Y))
{
GetViewport().SetInputAsHandled();
return;
}
_host.SignalInput(); _host.SignalInput();
return;
}
if (e.IsActionPressed("ui_accept")) _host.SignalInput();
}
private (int X, int Y) ToNativeScreen(Vector2 position)
{
Vector2 size = GetViewportRect().Size;
if (size.X <= 0 || size.Y <= 0) return (0, 0);
return ((int)System.Math.Floor(position.X * ScreenWidth / size.X),
(int)System.Math.Floor(position.Y * ScreenHeight / size.Y));
} }
public override void _ExitTree() public override void _ExitTree()
@@ -644,15 +675,47 @@ public partial class Main : Godot.Control
_bgm.Play(); _bgm.Play();
} }
public void PlayVoice(byte[] oggBytes, string assetName) public int QueueVoicePlayback()
=> System.Threading.Interlocked.Increment(ref _voiceQueuedGeneration);
public bool IsVoicePlaybackActive
=> System.Threading.Volatile.Read(ref _voiceCompletedGeneration)
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
public void PlayVoice(byte[] oggBytes, string assetName, int generation)
{ {
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes); var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; } if (stream == null)
{
GD.Print($"OGG load failed {assetName}");
CompleteVoiceGeneration(generation);
return;
}
stream.Loop = false; stream.Loop = false;
_voice.Stream = stream; _voice.Stream = stream;
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
_voice.Play(); _voice.Play();
} }
private void UpdateVoicePlaybackState()
{
int started = System.Threading.Volatile.Read(ref _voiceStartedGeneration);
if (started > System.Threading.Volatile.Read(ref _voiceCompletedGeneration) && !_voice.Playing)
CompleteVoiceGeneration(started);
}
private void CompleteVoiceGeneration(int generation)
{
int current;
do
{
current = System.Threading.Volatile.Read(ref _voiceCompletedGeneration);
if (current >= generation) return;
}
while (System.Threading.Interlocked.CompareExchange(
ref _voiceCompletedGeneration, generation, current) != current);
}
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel) public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
{ {
if ((uint)channel >= (uint)_sfx.Length) return; if ((uint)channel >= (uint)_sfx.Length) return;

View File

@@ -7,16 +7,30 @@ INFERRED: dict[int, dict] = {
0x71: dict(name='label-def', category='structural', noop=True, confidence='high', source='investigation', summary='1 imm; count == T1 table size -> the label/anchor T1 indexes. v1 no-op; revisit if menu/callback dispatch looks up by id'), 0x71: dict(name='label-def', category='structural', noop=True, confidence='high', source='investigation', summary='1 imm; count == T1 table size -> the label/anchor T1 indexes. v1 no-op; revisit if menu/callback dispatch looks up by id'),
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) + gfx cmd-type 5. 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).'), 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) + gfx cmd-type 5. 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.'), 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.'),
0x90: dict(name='hotspot-branch', category='input', noop=True, confidence='high', source='investigation', summary='cursor/input hotspot hit-test: rect (x,y,w,h) -> 3-way branch on interaction, else fall through to pc+1'), 0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the all-message skip state. Nonzero arms ADV fast-forward; zero clears the associated native run-state bit.'),
0x97: dict(name='hotspot-reg?', category='input', noop=True, confidence='med', source='inference', summary='companion register-hotspot / set-widget-action (argc5: v1 v2 1 1 <action-id>; NO code targets)'), 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.'),
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. Native manager supports channels 0..12; SC0000 uses 0..9."), 0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(resource_id)(channel) — synchronously resolve/open the scene-manifest asset and replace the channel's decoded sound buffer without starting playback. 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.'), 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.'),
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'), 0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'), 0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),
0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'), 0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'),
0x101: dict(name='reset-message-skip-input', category='input', noop=False, confidence='med', source='investigation', summary='Reset transient message-skip/input service state after an ADV chrome action.'),
0x109: dict(name='get-cursor-virtual', category='input', noop=True, confidence='high', source='investigation', summary="(out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates."),
0x10a: dict(name='set-cursor-virtual', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor.'),
0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework."), 0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the video service. See engine-re.md §Scene-coroutine framework."),
0x1bc: dict(name='block-mark', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; follows jcc/mov, precedes mov/ret — block boundary'), 0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='med', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'),
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'),
0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'),
0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1b9: dict(name='set-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1bc: dict(name='reset-message-voice-state', category='input', noop=False, confidence='high', source='investigation', summary='Reset the per-message queued-voice flag used by ADV Auto timing.'),
0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'), 0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'),
0x1ca: dict(name='set-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.'),
0x1cb: dict(name='get-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - read the engine setting `message:ReadTextSkip`.'),
0x1d2: dict(name='stmt-desc?', category='marker', noop=True, confidence='med', source='harness', summary='2 imm; immediately after stmt-begin 0x1f4 — statement descriptor?'), 0x1d2: dict(name='stmt-desc?', category='marker', noop=True, confidence='med', source='harness', summary='2 imm; immediately after stmt-begin 0x1f4 — statement descriptor?'),
0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'), 0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'),
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'), 0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),

View File

@@ -108,6 +108,11 @@ name = "cmd_type_table"
type = "int" type = "int"
note = "per-object cmd-type column base (write *(0x53d88 + curCtx*0x78))" note = "per-object cmd-type column base (write *(0x53d88 + curCtx*0x78))"
[[field]] [[field]]
offset = 0x55104
name = "auto_message_enabled"
type = "int"
note = "ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes"
[[field]]
offset = 0x55120 offset = 0x55120
name = "anti_tamper_a" name = "anti_tamper_a"
type = "int" type = "int"
@@ -163,6 +168,11 @@ name = "adv_read_skip_state"
type = "int" type = "int"
note = "current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history" note = "current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history"
[[field]] [[field]]
offset = 0x6dbe4
name = "adv_auto_voice_pending"
type = "int"
note = "set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets"
[[field]]
offset = 0x9b24c offset = 0x9b24c
name = "dispatch_table" name = "dispatch_table"
type = "void*" type = "void*"

View File

@@ -51,59 +51,70 @@ confidence = "high"
depends_on = ["0xa57", "0xa58"] depends_on = ["0xa57", "0xa58"]
[[global]] [[global]]
address = "0x6c9" address = "0x6c1"
name = "ui_toggle_0" name = "adv_chrome_enabled"
category = "ui-toggle" category = "ui-toggle"
type = "int" type = "int"
value_domain = "{0,1}" value_domain = "{0,1}"
usage = "ADV-chrome hotspot button toggle (op 0x90 site, near-universal across scenes)." usage = "Inherited SYSTEM4 UI-boot flag. Standard ADV scripts register the five visible SO001 control-strip pointer rectangles only while nonzero; zero skips directly to the three off-screen keyboard/pad records. The Phase-A single-scene Godot bootstrap seeds the native observed value 1."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] depends_on = ["0x6c9", "0x6ca", "0x6cb", "0x6cc", "0x6cd"]
[[global]]
address = "0x6c9"
name = "adv_hover_history"
category = "ui-toggle"
type = "int"
value_domain = "{0,1}"
usage = "Pointer-hover flag for the standard ADV History button at (684,572). Its op 0x90 enter/leave callbacks set 1/0; the shared SO001 redraw shows the History tooltip plus generic hover overlay while set."
source = "investigation"
confidence = "high"
depends_on = ["0x6ca", "0x6cb", "0x6cc", "0x6cd"]
[[global]] [[global]]
address = "0x6ca" address = "0x6ca"
name = "ui_toggle_1" name = "adv_hover_auto_message"
category = "ui-toggle" category = "ui-toggle"
type = "int" type = "int"
value_domain = "{0,1}" value_domain = "{0,1}"
usage = "ADV-chrome hotspot button toggle. See ui_toggle_0." usage = "Pointer-hover flag for the standard ADV Auto-message button at (706,572). See adv_hover_history."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] depends_on = ["0x6c9"]
[[global]] [[global]]
address = "0x6cb" address = "0x6cb"
name = "ui_toggle_2" name = "adv_hover_message_skip"
category = "ui-toggle" category = "ui-toggle"
type = "int" type = "int"
value_domain = "{0,1}" value_domain = "{0,1}"
usage = "ADV-chrome hotspot button toggle. See ui_toggle_0." usage = "Pointer-hover flag for the standard ADV all-message Skip button at (728,572). See adv_hover_history."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] depends_on = ["0x6c9"]
[[global]] [[global]]
address = "0x6cc" address = "0x6cc"
name = "ui_toggle_3" name = "adv_hover_read_message_skip"
category = "ui-toggle" category = "ui-toggle"
type = "int" type = "int"
value_domain = "{0,1}" value_domain = "{0,1}"
usage = "ADV-chrome hotspot button toggle. See ui_toggle_0." usage = "Pointer-hover flag for the standard ADV read-message-only Skip button at (750,572). See adv_hover_history."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] depends_on = ["0x6c9"]
[[global]] [[global]]
address = "0x6cd" address = "0x6cd"
name = "ui_toggle_4" name = "adv_hover_hide_window"
category = "ui-toggle" category = "ui-toggle"
type = "int" type = "int"
value_domain = "{0,1}" value_domain = "{0,1}"
usage = "ADV-chrome hotspot button toggle. See ui_toggle_0." usage = "Pointer-hover flag for the standard ADV Hide-window button at (772,572). See adv_hover_history."
source = "investigation" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] depends_on = ["0x6c9"]
[[global]] [[global]]
address = "0x62ccf" address = "0x62ccf"

View File

@@ -1015,12 +1015,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "wait-for-input" name = "wait-for-input"
category = "adv" category = "adv"
summary = "(layout_slot) - arm the ADV input wait after text reveal completes; activates the wait indicator configured for the selected text layout by op 0x73." summary = "(layout_slot) - arm the ADV input wait after text reveal completes; activate the configured wait indicator and, while Auto is enabled, arm the appropriate Auto-message timer."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached." evidence = "Ghidra /v2: op_0x72_handler@0x41e690 fetches operand 1 and calls FUN_00453120(text_manager, layout_slot, -1, &state), then sets the input-wait run-state flags. FUN_00453120 resolves layout slot 0 as current and consumes the indicator descriptor at layout+0x3c configured by op 0x73. SYSTEM4 layout 1 uses SO000's bat strip; the click that completes show-text is consumed before this opcode is reached. The handler also checks ctx+0x55104 (Auto enabled): when ctx+0x6dbe4 has no pending voice it arms the timer with message:AutoMessageTime1, substituting 100 ms for configuration value zero. adv_input_service_poll@0x411230 waits for an active voice to finish and then arms AutoMessageTime0, likewise with a 100-ms zero fallback."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
@@ -1383,18 +1383,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B290" name = "set-message-skip"
category = "unknown" category = "input"
summary = "" summary = "(enabled) - set the all-message skip state. Nonzero arms ADV fast-forward; zero clears the associated native run-state bit."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x88_set_message_skip@0x41f130 writes operand 1 to ctx+0x13dc and ctx+0x550fc; nonzero also sets ctx+0x6da78=1, while zero clears run-state bit 0x08000000. adv_interpreter_tick injects input bit 0x40 while ctx+0x13dc is nonzero and cancels the state according to set:CancelMesSkipOnClick. SC0000's Message-skip button passes 1."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "enabled"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -1468,62 +1468,69 @@ argc = 7
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "hotspot-branch" name = "register-hotspot-callbacks"
category = "input" category = "input"
summary = "cursor/input hotspot hit-test: rect (x,y,w,h) -> 3-way branch on interaction, else fall through to pc+1" 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."
noop_headless = true noop_headless = true
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [0x1f4, 0x1f5] depends_on = []
evidence = "all 301 ADV scripts contain the identical 8 sites; enc.len 15 lands pc+1 on the fall-through stmt (design-confirmed); fall-through = correct headless no-input path, proven by 279 CLEAN dialogue scenes" evidence = "Ghidra /v2: op_0x90_handler@0x41fc80 fetches x/y/w/h, forms x+w/y+h, fetches operands 5/6/7, and calls input_hotspot_register_rect_callbacks@0x403d70 on the current frame registry. The worker appends rect + callbacks to arrays and returns without changing PC. input_hotspot_poll_hover_callback@0x4040b0 returns operand 5 on entry and operand 6 on exit; input_hotspot_take_click_callback@0x404330 resets the registry and returns operand 7 on activation. All 301 ADV scripts contain the identical five control-strip records plus three keyed records."
confirm_by = "target->interaction-state mapping (hover-enter/leave/click) via live input capture or Frida in A2" confirm_by = ""
details = """ details = """
op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "ukn" noting args op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "ukn" noting args
5-7 are code locations. Corpus analysis (all 301 ADV scripts) resolves it: 5-7 are code locations. Native RE plus corpus analysis resolves it:
- The opcode APPENDS a record; it is not an immediate branch. Rect bounds are stored as
`(x,y,x+w,y+h)` and the cursor hit-test compares them inclusively.
- The later input service maps targets exactly: target A = pointer enter, target B = pointer leave,
target C = activation/click. Moving directly between records emits leave first, then enter on the
next service poll. Activation consumes/resets the registry before dispatching target C.
- Two forms, both ONLY in one shared ADV-chrome subroutine copied into every ADV script: - Two forms, both ONLY in one shared ADV-chrome subroutine copied into every ADV script:
* Mode A (1505 = 5x301): immediate x,y,w,h with w=h=20; the five on-screen buttons at * Mode A (1505 = 5x301): immediate x,y,w,h with w=h=20; the five on-screen buttons at
(684|706|728|750|772, 572), each setting one of G[0x6c9..0x6cd] to 1 / 0 / 0+run-action (684|706|728|750|772, 572): History, Auto message, Message skip, Read-message skip, and
(reads as hover-enter / hover-leave / click). All 3 targets real. Hide window. Their enter/leave callbacks set G[0x6c9..0x6cd] to 1/0 and redraw SO001's
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real -- a keyed 2-way input test. tooltip + generic 20x20 translucent hover overlay; target C runs the per-button action.
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real. Companion op 0x97 binds
input-bit ids 0, 8, and 7 to these records for keyboard/pad activation.
- Every one of the 301 scripts has EXACTLY 8 sites (5 A + 3 B); zero scene-specific use. - Every one of the 301 scripts has EXACTLY 8 sites (5 A + 3 B); zero scene-specific use.
- Falls through (pc+1) when nothing matches -- design-confirmed (0xd0 + 15 dwords = 0xdf = label_df). - Registration returns normally to pc+1; later callbacks temporarily redirect execution.
- Headless (no cursor/input) => fall through => vm0 stub already correct; the 12 EMPTY sweep scenes - Headless (no cursor/input) can ignore registration; the 12 EMPTY sweep scenes
are gated by state + this input-wait chrome, NOT by unmodelled 0x90. Model live in A2. are gated by state + this input-wait chrome, NOT by unmodelled 0x90. Model live in A2.
""" """
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "rectangle left x"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "rectangle top y"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "rectangle width added to x for inclusive right bound"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 4 i = 4
role = "" role = "rectangle height added to y for inclusive bottom bound"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 5 i = 5
role = "" role = "target:pointer enter"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 6 i = 6
role = "" role = "target:pointer leave"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 7 i = 7
role = "" role = "target:activate/click"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -1533,14 +1540,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00415040" name = "cancel-hotspot-wait"
category = "unknown" category = "input"
summary = "" 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."
noop_headless = false noop_headless = true
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x93_cancel_hotspot_wait@0x416670 clears run-state bit 0x00800000, calls input_hotspot_reset@0x404130, and clears the active flag at ctx+0xc6e4 (or sets the pending flag at +0xc6e0 when already inactive)."
[[opcode]] [[opcode]]
op = 0x94 op = 0x94
@@ -1549,14 +1556,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00415090" name = "arm-hotspot-wait"
category = "unknown" category = "input"
summary = "" summary = "Arm native hotspot input processing after the script has registered its rectangles."
noop_headless = false noop_headless = true
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" 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."
[[opcode]] [[opcode]]
op = 0x97 op = 0x97
@@ -1565,38 +1572,38 @@ argc = 5
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "hotspot-reg?" name = "bind-hotspot-key"
category = "input" category = "input"
summary = "companion register-hotspot / set-widget-action (argc5: v1 v2 1 1 <action-id>; NO code targets)" summary = "(x)(y)(w)(h)(input_bit) - find the already-registered rectangle with identical bounds and bind an input/key bit number to it."
noop_headless = true noop_headless = true
source = "inference" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [0x90] depends_on = [0x90]
evidence = "interleaves with 0x90 in the shared ADV-chrome subroutine; trailing imm = action id 0x0/0x7/0x8; same widget cluster as 0x90/0x91/0x92/0x95; confirm via frida" 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."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "rectangle left x"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "rectangle top y"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "rectangle width"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 4 i = 4
role = "" role = "rectangle height"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 5 i = 5
role = "" role = "input/key bit index"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -2288,14 +2295,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00415BF0" name = "reset-message-skip-input"
category = "unknown" category = "input"
summary = "" summary = "Reset transient message-skip/input service state after an ADV chrome action."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "med"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x101_reset_message_skip_input@0x4170a0 resets the input state rooted at ctx+0xa0ce8, clears run-state bit 0x08000000, zeroes ctx+0xa0ce8, and writes ctx+0x6da74=1 / ctx+0x6da80=0. The Auto, Message-skip, Read-skip, and Hide-window click callbacks invoke it after their 100 ms cursor re-arm sequence."
[[opcode]] [[opcode]]
op = 0x107 op = 0x107
@@ -2351,23 +2358,23 @@ argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00415EC0" name = "get-cursor-virtual"
category = "unknown" category = "input"
summary = "" summary = "(out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates."
noop_headless = false noop_headless = true
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x109_get_cursor_virtual@0x428bb0 calls the cursor-position helper, converts client/display coordinates through the active VirtualFullScreen transform, and writes x/y to operands 1/2. The ADV chrome callbacks preserve x and then move y by alternating -1/+1 before op 0x10a."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "output virtual x"
observed_types = ["g-int", "l-int"] observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "output virtual y"
observed_types = ["g-int", "l-int"] observed_types = ["g-int", "l-int"]
[[opcode]] [[opcode]]
@@ -2377,23 +2384,23 @@ argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041E540" name = "set-cursor-virtual"
category = "unknown" category = "input"
summary = "" summary = "(x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor."
noop_headless = false noop_headless = true
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "virtual x"
observed_types = ["g-int", "l-int"] observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "virtual y"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode]] [[opcode]]
@@ -3248,14 +3255,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00414D50" name = "yield-adv-coroutine"
category = "unknown" category = "control"
summary = "" summary = "Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "med"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x199_yield_adv_coroutine@0x416440 selects the registered coroutine yield-A or yield-B PC according to ctx+0x6dbc8, saves the current resume offset/state, and redirects the current frame PC. SC0000's x=772 ADV button invokes it; the SO001 tooltip at source x=528 reads Window hide, and the surrounding coroutine calls HIDEWIN.BIN."
[[opcode]] [[opcode]]
op = 0x19a op = 0x19a
@@ -3264,18 +3271,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00414E50" name = "get-message-skip"
category = "unknown" category = "input"
summary = "" summary = "(out) - return the current all-message skip state set by op 0x88."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x19a_get_message_skip@0x4271b0 writes ctx+0x550fc to operand 1. The shared ADV redraw routine uses it to select SO001's active Message-skip icon at x=728."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "output enabled"
observed_types = ["g-int"] observed_types = ["g-int"]
[[opcode]] [[opcode]]
@@ -3890,18 +3897,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00414F60" name = "get-auto-message"
category = "unknown" category = "input"
summary = "" summary = "(out) - return whether automatic message advance is enabled."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1b6_get_auto_message@0x4271e0 writes `(ctx+0x55104 != 0)` to operand 1. FUN_00411230 consumes the same field to arm AutoMessageTime0/1 timers; the shared ADV redraw routine uses it for the active Auto icon."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "output enabled"
observed_types = ["g-int"] observed_types = ["g-int"]
[[opcode]] [[opcode]]
@@ -3911,18 +3918,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B640" name = "set-auto-message"
category = "unknown" category = "input"
summary = "" summary = "(enabled) - enable or disable automatic message advance."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1b7_set_auto_message@0x41f560 stores `(operand1 != 0)` at ctx+0x55104. SC0000's x=706 Auto button reads the current value through 0x1b6, toggles modulo 2, then writes it through this opcode."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "enabled"
observed_types = ["g-int"] observed_types = ["g-int"]
[[opcode]] [[opcode]]
@@ -3932,23 +3939,23 @@ argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B670" name = "get-auto-message-time"
category = "unknown" category = "input"
summary = "" summary = "(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1b8_handler@0x427210 dispatches selector 0 to config key message:AutoMessageTime0 and selector 1 to message:AutoMessageTime1, then writes the result to operand 2. CONFIG.BIN uses it to bound the two UI settings."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "timer selector: 0=post-voice Time0, 1=unvoiced Time1"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "output milliseconds"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode]] [[opcode]]
@@ -3958,23 +3965,23 @@ argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B710" name = "set-auto-message-time"
category = "unknown" category = "input"
summary = "" summary = "(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1b9_handler@0x41f5a0 dispatches selector 0/1 to config setters for message:AutoMessageTime0/1. CONFIG.BIN initializes them to 500/2000 ms and adjusts each in 500-ms steps within 500..9500."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "timer selector: 0=post-voice Time0, 1=unvoiced Time1"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "milliseconds"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode]] [[opcode]]
@@ -4031,14 +4038,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "block-mark" name = "reset-message-voice-state"
category = "marker" category = "input"
summary = "zero-arg; follows jcc/mov, precedes mov/ret — block boundary" summary = "Reset the per-message queued-voice flag used by ADV Auto timing."
noop_headless = true noop_headless = false
source = "inference" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1bc_handler@0x416c20 writes zero to ctx+0x6dbe4 (adv_auto_voice_pending). Opcode 0xc4 sets that field when voice playback is queued; adv_input_service_poll consumes it to select the post-voice AutoMessageTime0 path."
[[opcode]] [[opcode]]
op = 0x1bd op = 0x1bd
@@ -4162,18 +4169,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B9B0" name = "set-read-message-skip"
category = "unknown" category = "input"
summary = "" summary = "(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1ca_set_read_message_skip@0x41f880 calls the engine setting interface's setter for `message:ReadTextSkip` with operand 1. SC0000's x=750 Read-message-skip button toggles the value read by op 0x1cb."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "enabled"
observed_types = ["imm", "g-int"] observed_types = ["imm", "g-int"]
[[opcode]] [[opcode]]
@@ -4183,18 +4190,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00414FD0" name = "get-read-message-skip"
category = "unknown" category = "input"
summary = "" summary = "(out) - read the engine setting `message:ReadTextSkip`."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x1cb_get_read_message_skip@0x4272f0 calls the engine setting interface's getter for `message:ReadTextSkip` and writes the result to operand 1. The shared ADV redraw routine uses it for the active Read-message-skip icon."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "output enabled"
observed_types = ["g-int"] observed_types = ["g-int"]
[[opcode]] [[opcode]]