Implement ADV controls and native Auto timing
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
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
|
||||
|
||||
@@ -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) |
|
||||
| `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)) |
|
||||
| `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 |
|
||||
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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) |
|
||||
| `0xa0cc0` | `screen_w` | `int` | screen width (640) |
|
||||
| `0xa0cc4` | `screen_h` | `int` | screen height (480) |
|
||||
|
||||
@@ -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.**
|
||||
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)
|
||||
|
||||
The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
|
||||
# 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
|
||||
|
||||
@@ -3522,12 +3522,13 @@
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `0x6c9` | ui_toggle_0 | med | investigation | ADV-chrome hotspot button toggle (op 0x90 site, near-universal across scenes). |
|
||||
| `0x6ca` | ui_toggle_1 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. |
|
||||
| `0x6cb` | ui_toggle_2 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. |
|
||||
| `0x6cc` | ui_toggle_3 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. |
|
||||
| `0x6cd` | ui_toggle_4 | med | investigation | ADV-chrome hotspot button toggle. See ui_toggle_0. |
|
||||
| `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` | adv_hover_auto_message | high | investigation | Pointer-hover flag for the standard ADV Auto-message button at (706,572). See adv_hover_history. |
|
||||
| `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` | 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` | adv_hover_hide_window | high | investigation | Pointer-hover flag for the standard ADV Hide-window button at (772,572). See adv_hover_history. |
|
||||
|
||||
## unknown
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
## adv
|
||||
|
||||
### 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
|
||||
- **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)
|
||||
- **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
|
||||
- **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)
|
||||
- **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
|
||||
@@ -328,31 +333,97 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
|
||||
|
||||
## input
|
||||
|
||||
### 0x90 `hotspot-branch` (u0041BEB0, argc 7)
|
||||
- **summary:** cursor/input hotspot hit-test: rect (x,y,w,h) -> 3-way branch on interaction, else fall through to pc+1
|
||||
### 0x88 `set-message-skip` (u0041B290, argc 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
|
||||
- **depends on:** 0x1f4, 0x1f5
|
||||
- **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
|
||||
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:
|
||||
* 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
|
||||
(reads as hover-enter / hover-leave / click). All 3 targets real.
|
||||
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real -- a keyed 2-way input test.
|
||||
(684|706|728|750|772, 572): History, Auto message, Message skip, Read-message skip, and
|
||||
Hide window. Their enter/leave callbacks set G[0x6c9..0x6cd] to 1/0 and redraw SO001's
|
||||
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.
|
||||
- Falls through (pc+1) when nothing matches -- design-confirmed (0xd0 + 15 dwords = 0xdf = label_df).
|
||||
- Headless (no cursor/input) => fall through => vm0 stub already correct; the 12 EMPTY sweep scenes
|
||||
- Registration returns normally to pc+1; later callbacks temporarily redirect execution.
|
||||
- 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.
|
||||
|
||||
|
||||
### 0x97 `hotspot-reg?` (u0041C150, argc 5)
|
||||
- **summary:** companion register-hotspot / set-widget-action (argc5: v1 v2 1 1 <action-id>; NO code targets)
|
||||
- **grounding:** source=inference, confidence=med, noop_headless=True
|
||||
### 0x93 `cancel-hotspot-wait` (u00415040, argc 0)
|
||||
- **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=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
|
||||
- **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)
|
||||
- **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
|
||||
- **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)
|
||||
- **summary:** zero-arg; follows jcc/mov, precedes mov/ret — block boundary
|
||||
- **grounding:** source=inference, confidence=high, noop_headless=True
|
||||
### 0x1cb `get-read-message-skip` (u00414FD0, argc 1)
|
||||
- **summary:** (out) - read the engine setting `message:ReadTextSkip`.
|
||||
- **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)
|
||||
- **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)
|
||||
- **summary:** zero-arg; opens scripts, pairs with stmt-end 0x1f5
|
||||
- **grounding:** source=investigation, confidence=high, noop_headless=True
|
||||
- **depended on by:** 0x90
|
||||
|
||||
### 0x1f5 `stmt-end` (u00416120, argc 0)
|
||||
- **summary:** zero-arg; precedes exit/next-stmt, pairs with 0x1f4
|
||||
- **grounding:** source=investigation, confidence=high, noop_headless=True
|
||||
- **depended on by:** 0x90
|
||||
|
||||
### 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
|
||||
@@ -581,10 +656,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x88 `u0041B290` (u0041B290, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x8b `u0041B3D0` (u0041B3D0, argc 1)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **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)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x101 `u00415BF0` (u00415BF0, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x107 `u0041E500` (u0041E500, argc 2)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **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)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **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)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **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)
|
||||
- **summary:** —
|
||||
- **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:** —
|
||||
- **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)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
53
engine/Age.Engine.Tests/AdvAutoAdvanceTimerTests.cs
Normal file
53
engine/Age.Engine.Tests/AdvAutoAdvanceTimerTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
214
engine/Age.Engine.Tests/HotspotInputTests.cs
Normal file
214
engine/Age.Engine.Tests/HotspotInputTests.cs
Normal 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);
|
||||
}
|
||||
@@ -6,11 +6,12 @@ using Age.Engine.Model;
|
||||
|
||||
/// <summary>Shared test doubles: a host that records observable effects, and an in-memory script
|
||||
/// provider for synthetic call-script targets.</summary>
|
||||
internal sealed class RecordingHost : IHost
|
||||
internal class RecordingHost : IHost
|
||||
{
|
||||
public int Waits;
|
||||
public int Presents;
|
||||
public int TransitionWaits;
|
||||
public int InputCallbackFrames;
|
||||
public bool MessageSkip;
|
||||
public bool AdvReadSkip;
|
||||
public readonly List<(int Offset, string Text)> Lines = new();
|
||||
@@ -29,6 +30,15 @@ internal sealed class RecordingHost : IHost
|
||||
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||
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 FrameYield() { }
|
||||
public bool IsMessageSkipActive => MessageSkip;
|
||||
|
||||
50
engine/Age.Engine/Hosting/AdvAutoAdvanceTimer.cs
Normal file
50
engine/Age.Engine/Hosting/AdvAutoAdvanceTimer.cs
Normal 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);
|
||||
}
|
||||
@@ -7,6 +7,9 @@ public readonly record struct AdvWaitIndicatorConfig(
|
||||
int SourceX, int SourceY, int CellWidth, int CellHeight,
|
||||
int TerminalFrame, long FramePeriodMs);
|
||||
|
||||
public readonly record struct AdvAutoWaitState(
|
||||
bool Enabled, bool VoicePending, long PostVoiceDelayMs, long UnvoicedDelayMs);
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
void ShowText(int offset, string text);
|
||||
@@ -17,6 +20,18 @@ public interface IHost
|
||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||
void 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 FrameYield();
|
||||
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
||||
|
||||
@@ -14,5 +14,6 @@ internal sealed class ExecFrame
|
||||
public int? CoroutineYieldHandlerA; // op 0x7b: native per-frame handler PCs
|
||||
public int? CoroutineYieldHandlerB;
|
||||
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; }
|
||||
}
|
||||
|
||||
109
engine/Age.Engine/Vm/HotspotRegistry.cs
Normal file
109
engine/Age.Engine/Vm/HotspotRegistry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public sealed class VirtualMachine
|
||||
private const long NoJump = 0xFFFFFFFF;
|
||||
private const int HALT = int.MinValue;
|
||||
private const int FRAME_RETURN = int.MinValue + 1;
|
||||
private const int HOTSPOT_RETURN = int.MinValue + 2;
|
||||
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,
|
||||
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 int _depth;
|
||||
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 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 string? HaltReason { get; private set; }
|
||||
public long Steps { get; private set; }
|
||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||
|
||||
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null,
|
||||
IScriptProvider? provider = null, ITraceSink? sink = null)
|
||||
{ _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider;
|
||||
_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 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 : "";
|
||||
@@ -156,6 +195,8 @@ public sealed class VirtualMachine
|
||||
|
||||
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
||||
{
|
||||
ExecFrame? previousInteractiveFrame;
|
||||
lock (_interactiveLock) previousInteractiveFrame = _interactiveFrame;
|
||||
var prev = _cur; _cur = frame; _depth++;
|
||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||
var outcome = FrameOutcome.RanOff;
|
||||
@@ -172,10 +213,49 @@ public sealed class VirtualMachine
|
||||
pc = next;
|
||||
}
|
||||
_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--;
|
||||
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)
|
||||
{
|
||||
int op = ins.Opcode;
|
||||
@@ -295,7 +375,55 @@ public sealed class VirtualMachine
|
||||
case "wait-for-input":
|
||||
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
|
||||
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.
|
||||
_host.Sleep(Read(a[0])); return pc + 1;
|
||||
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-service-state": // compatibility with pre-recovery generated tables
|
||||
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 "comment": case "display-furigana": case "dev_ukn":
|
||||
return pc + 1;
|
||||
@@ -329,7 +475,9 @@ public sealed class VirtualMachine
|
||||
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
|
||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
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.
|
||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||
private readonly SemaphoreSlim _gate = new(0, 1);
|
||||
private readonly AutoResetEvent _inputCallbackSignal = new(false);
|
||||
private readonly Age.Engine.Hosting.FrameClock _clock;
|
||||
private readonly GodotTimelineLog? _timeline;
|
||||
private readonly PageLocatorState _locator;
|
||||
@@ -147,6 +151,13 @@ public sealed class GodotAdvHost : IHost
|
||||
public void WaitForInput() => WaitForInput(0);
|
||||
|
||||
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++;
|
||||
_locator.Wait(Pages);
|
||||
@@ -158,9 +169,22 @@ public sealed class GodotAdvHost : IHost
|
||||
_waitIndicatorStartedMs = _clock.NowMs;
|
||||
IsWaiting = true;
|
||||
_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;
|
||||
_timeline?.State("running", new() { ["input"] = "auto-or-user" });
|
||||
_timeline?.State("running", new() { ["input"] = autoAdvanced ? "auto" : "user" });
|
||||
lock (_textLock)
|
||||
{
|
||||
_advText = "";
|
||||
@@ -194,6 +218,11 @@ public sealed class GodotAdvHost : IHost
|
||||
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)
|
||||
{
|
||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||
@@ -251,6 +280,7 @@ public sealed class GodotAdvHost : IHost
|
||||
_stopping = true;
|
||||
lock (_textLock) _advTextForceComplete = true;
|
||||
if (_gate.CurrentCount == 0) _gate.Release();
|
||||
_inputCallbackSignal.Set();
|
||||
_frameSignal.Set();
|
||||
}
|
||||
|
||||
@@ -440,7 +470,11 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
var asset = _res.Resolve(_scene, id);
|
||||
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)
|
||||
|
||||
@@ -30,6 +30,9 @@ public partial class Main : Godot.Control
|
||||
private Label _locatorHud = null!;
|
||||
private AudioStreamPlayer _bgm = null!; // looping background music
|
||||
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 VirtualMachine _vm = 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;
|
||||
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.
|
||||
// The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice.
|
||||
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
|
||||
@@ -265,6 +272,7 @@ public partial class Main : Godot.Control
|
||||
_timelineFrame++;
|
||||
_timeline?.SetFrame(_timelineFrame, _clock.NowMs);
|
||||
_host?.PulseFrame();
|
||||
UpdateVoicePlaybackState();
|
||||
UpdateMovieFrames();
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
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";
|
||||
return;
|
||||
}
|
||||
if (e.IsActionPressed("ui_accept") ||
|
||||
(e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left))
|
||||
if (e is InputEventMouseMotion motion)
|
||||
{
|
||||
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();
|
||||
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()
|
||||
@@ -644,15 +675,47 @@ public partial class Main : Godot.Control
|
||||
_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);
|
||||
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;
|
||||
_voice.Stream = stream;
|
||||
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
|
||||
_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)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||
|
||||
@@ -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'),
|
||||
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.'),
|
||||
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'),
|
||||
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)'),
|
||||
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.'),
|
||||
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."),
|
||||
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.'),
|
||||
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.'),
|
||||
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."),
|
||||
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'),
|
||||
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?'),
|
||||
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'),
|
||||
|
||||
@@ -108,6 +108,11 @@ name = "cmd_type_table"
|
||||
type = "int"
|
||||
note = "per-object cmd-type column base (write *(0x53d88 + curCtx*0x78))"
|
||||
[[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
|
||||
name = "anti_tamper_a"
|
||||
type = "int"
|
||||
@@ -163,6 +168,11 @@ name = "adv_read_skip_state"
|
||||
type = "int"
|
||||
note = "current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history"
|
||||
[[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
|
||||
name = "dispatch_table"
|
||||
type = "void*"
|
||||
|
||||
@@ -51,59 +51,70 @@ confidence = "high"
|
||||
depends_on = ["0xa57", "0xa58"]
|
||||
|
||||
[[global]]
|
||||
address = "0x6c9"
|
||||
name = "ui_toggle_0"
|
||||
address = "0x6c1"
|
||||
name = "adv_chrome_enabled"
|
||||
category = "ui-toggle"
|
||||
type = "int"
|
||||
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"
|
||||
confidence = "med"
|
||||
depends_on = []
|
||||
confidence = "high"
|
||||
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]]
|
||||
address = "0x6ca"
|
||||
name = "ui_toggle_1"
|
||||
name = "adv_hover_auto_message"
|
||||
category = "ui-toggle"
|
||||
type = "int"
|
||||
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"
|
||||
confidence = "med"
|
||||
depends_on = []
|
||||
confidence = "high"
|
||||
depends_on = ["0x6c9"]
|
||||
|
||||
[[global]]
|
||||
address = "0x6cb"
|
||||
name = "ui_toggle_2"
|
||||
name = "adv_hover_message_skip"
|
||||
category = "ui-toggle"
|
||||
type = "int"
|
||||
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"
|
||||
confidence = "med"
|
||||
depends_on = []
|
||||
confidence = "high"
|
||||
depends_on = ["0x6c9"]
|
||||
|
||||
[[global]]
|
||||
address = "0x6cc"
|
||||
name = "ui_toggle_3"
|
||||
name = "adv_hover_read_message_skip"
|
||||
category = "ui-toggle"
|
||||
type = "int"
|
||||
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"
|
||||
confidence = "med"
|
||||
depends_on = []
|
||||
confidence = "high"
|
||||
depends_on = ["0x6c9"]
|
||||
|
||||
[[global]]
|
||||
address = "0x6cd"
|
||||
name = "ui_toggle_4"
|
||||
name = "adv_hover_hide_window"
|
||||
category = "ui-toggle"
|
||||
type = "int"
|
||||
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"
|
||||
confidence = "med"
|
||||
depends_on = []
|
||||
confidence = "high"
|
||||
depends_on = ["0x6c9"]
|
||||
|
||||
[[global]]
|
||||
address = "0x62ccf"
|
||||
|
||||
@@ -1015,12 +1015,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "wait-for-input"
|
||||
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
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
@@ -1383,18 +1383,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041B290"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-message-skip"
|
||||
category = "input"
|
||||
summary = "(enabled) - set the all-message skip state. Nonzero arms ADV fast-forward; zero clears the associated native run-state bit."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "enabled"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -1468,62 +1468,69 @@ argc = 7
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "hotspot-branch"
|
||||
name = "register-hotspot-callbacks"
|
||||
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
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = [0x1f4, 0x1f5]
|
||||
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"
|
||||
confirm_by = "target->interaction-state mapping (hover-enter/leave/click) via live input capture or Frida in A2"
|
||||
depends_on = []
|
||||
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 = ""
|
||||
details = """
|
||||
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:
|
||||
* 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
|
||||
(reads as hover-enter / hover-leave / click). All 3 targets real.
|
||||
* Mode B (903 = 3x301): local-int operands, w=h=1, only tgt_c real -- a keyed 2-way input test.
|
||||
(684|706|728|750|772, 572): History, Auto message, Message skip, Read-message skip, and
|
||||
Hide window. Their enter/leave callbacks set G[0x6c9..0x6cd] to 1/0 and redraw SO001's
|
||||
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.
|
||||
- Falls through (pc+1) when nothing matches -- design-confirmed (0xd0 + 15 dwords = 0xdf = label_df).
|
||||
- Headless (no cursor/input) => fall through => vm0 stub already correct; the 12 EMPTY sweep scenes
|
||||
- Registration returns normally to pc+1; later callbacks temporarily redirect execution.
|
||||
- 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.
|
||||
"""
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "rectangle left x"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "rectangle top y"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "rectangle width added to x for inclusive right bound"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "rectangle height added to y for inclusive bottom bound"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "target:pointer enter"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 6
|
||||
role = ""
|
||||
role = "target:pointer leave"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 7
|
||||
role = ""
|
||||
role = "target:activate/click"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -1533,14 +1540,14 @@ argc = 0
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00415040"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
name = "cancel-hotspot-wait"
|
||||
category = "input"
|
||||
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 = true
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
op = 0x94
|
||||
@@ -1549,14 +1556,14 @@ argc = 0
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00415090"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
name = "arm-hotspot-wait"
|
||||
category = "input"
|
||||
summary = "Arm native hotspot input processing after the script has registered its rectangles."
|
||||
noop_headless = true
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
op = 0x97
|
||||
@@ -1565,38 +1572,38 @@ argc = 5
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "hotspot-reg?"
|
||||
name = "bind-hotspot-key"
|
||||
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
|
||||
source = "inference"
|
||||
confidence = "med"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "rectangle left x"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "rectangle top y"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "rectangle width"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "rectangle height"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "input/key bit index"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -2288,14 +2295,14 @@ argc = 0
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00415BF0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "reset-message-skip-input"
|
||||
category = "input"
|
||||
summary = "Reset transient message-skip/input service state after an ADV chrome action."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "med"
|
||||
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]]
|
||||
op = 0x107
|
||||
@@ -2351,23 +2358,23 @@ argc = 2
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00415EC0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
name = "get-cursor-virtual"
|
||||
category = "input"
|
||||
summary = "(out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates."
|
||||
noop_headless = true
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "output virtual x"
|
||||
observed_types = ["g-int", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "output virtual y"
|
||||
observed_types = ["g-int", "l-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -2377,23 +2384,23 @@ argc = 2
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041E540"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
name = "set-cursor-virtual"
|
||||
category = "input"
|
||||
summary = "(x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor."
|
||||
noop_headless = true
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "virtual x"
|
||||
observed_types = ["g-int", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "virtual y"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -3248,14 +3255,14 @@ argc = 0
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00414D50"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "yield-adv-coroutine"
|
||||
category = "control"
|
||||
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
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "med"
|
||||
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]]
|
||||
op = 0x19a
|
||||
@@ -3264,18 +3271,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00414E50"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "get-message-skip"
|
||||
category = "input"
|
||||
summary = "(out) - return the current all-message skip state set by op 0x88."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "output enabled"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -3890,18 +3897,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00414F60"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "get-auto-message"
|
||||
category = "input"
|
||||
summary = "(out) - return whether automatic message advance is enabled."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "output enabled"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -3911,18 +3918,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041B640"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-auto-message"
|
||||
category = "input"
|
||||
summary = "(enabled) - enable or disable automatic message advance."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "enabled"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -3932,23 +3939,23 @@ argc = 2
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041B670"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "get-auto-message-time"
|
||||
category = "input"
|
||||
summary = "(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "timer selector: 0=post-voice Time0, 1=unvoiced Time1"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "output milliseconds"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -3958,23 +3965,23 @@ argc = 2
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041B710"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-auto-message-time"
|
||||
category = "input"
|
||||
summary = "(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "timer selector: 0=post-voice Time0, 1=unvoiced Time1"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "milliseconds"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -4031,14 +4038,14 @@ argc = 0
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "block-mark"
|
||||
category = "marker"
|
||||
summary = "zero-arg; follows jcc/mov, precedes mov/ret — block boundary"
|
||||
noop_headless = true
|
||||
source = "inference"
|
||||
name = "reset-message-voice-state"
|
||||
category = "input"
|
||||
summary = "Reset the per-message queued-voice flag used by ADV Auto timing."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
op = 0x1bd
|
||||
@@ -4162,18 +4169,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u0041B9B0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-read-message-skip"
|
||||
category = "input"
|
||||
summary = "(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "enabled"
|
||||
observed_types = ["imm", "g-int"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -4183,18 +4190,18 @@ argc = 1
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00414FD0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "get-read-message-skip"
|
||||
category = "input"
|
||||
summary = "(out) - read the engine setting `message:ReadTextSkip`."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
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]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "output enabled"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode]]
|
||||
|
||||
Reference in New Issue
Block a user