From 6255f42794684ce7ba8b0783108fd57a60ccc5fb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 18 Jul 2026 20:28:36 -0400 Subject: [PATCH] Document ADV history and remaining button paths --- docs/engine-ctx-reference.md | 2 +- docs/engine-re.md | 76 +++++++++++++ docs/opcode-reference.md | 126 ++++++++++++--------- docs/phase-a-slice-plan.md | 41 +++++++ tools/age_opcodes_himegari.py | 15 ++- vm-map/engine-ctx.toml | 6 +- vm-map/opcodes.toml | 206 +++++++++++++++++----------------- 7 files changed, 308 insertions(+), 164 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index dfe7502..c29592b 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -25,7 +25,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) | | `0x53d2c` | `frame_pc` | `int` | current frame PC column (op = *(0x53d2c + curCtx*0x78)) | | `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) | -| `0x53d64` | `gfx_obj_record_array` | `void*` | gfx object-record array (field[0]=0xffffffff free; cmd-type at rec+0x24) | +| `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key | | `0x53d88` | `cmd_type_table` | `int` | per-object cmd-type column base (write *(0x53d88 + curCtx*0x78)) | | `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay | | `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes | diff --git a/docs/engine-re.md b/docs/engine-re.md index e780fb1..1f2fb4e 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1203,6 +1203,82 @@ the missing native governor and remains fast without teleporting between blockin op `0x88` state reaches the host before the following cadence yields. Validation is engine 168/168, zero-warning Godot build, and threaded `SELFTEST OK`. +### ADV read-message Skip and shared `RT.DAT` history (2026-07-18) + +Read-message Skip is backed by an engine-owned `ReadTextDB`, not a VM-global flag and not ordinary numbered +slot data. Each script frame supplies its raw packed SYS4/AAI resource id, a table of message code offsets, +and its count. `script_frame_load_resource@0x40e980` stores the same id used by +`asset_open_indexed_entry` at frame field `+0x04` (`EngineCtx+0x53d64`); base Himegari scripts therefore use +their SYS4INI file index, while append resources retain the high-byte pack selector. +`read_text_db_find_message_index@0x468f50` maps the current code dword offset through that table to a +per-script message index. `read_text_db_is_message_read@0x469930` formats the script id as an eight-digit +lowercase hexadecimal key, looks up that script's record, bounds-checks the index, and returns the stored +dword flag. `adv_refresh_read_skip_state@0x406cd0` and ops `0x6e/0x71/0x72` combine that result with +`message:ReadTextSkip`; a read page sets `run_state_flags & 0x08000000` and +`ctx->adv_read_skip_state`, which op `0x1cc` exposes to the scripts. + +The write side records completion, rather than merely displaying text. Ordinary click/wheel advance and +Auto expiry queue `{script_id, message_index, message_count}` through +`read_text_db_queue_message@0x469340`; an op-`0x72` wait already being passed by Skip queues the same tuple +directly. Opcode `0x71` remains the structural T1 label/anchor, but it is not a pure runtime no-op: +`op_0x71_handler@0x41e540` snapshots the current code position and calls +`read_text_db_commit_pending@0x46ae20`, which grows or creates the per-script flag array and sets the queued +indices to one. This queued/commit seam lets the port reproduce native read eligibility without scene +offset lists or synthetic VM globals. + +Persistence is shared across numbered save slots. `shared_profile_save@0x40c950` atomically rewrites +`SAVE.DAT`, then serializes `ReadTextDB` through `$$RT.DAT` to `RT.DAT`, with `RT.BAK` handling. +`shared_profile_load@0x40ccd0` loads `SAVE.DAT` (falling back to `SAVE.BAK`) and then independently loads +`RT.DAT` when present. Numbered saves use the separate `SAVE%2.2d.DAT` pattern. A successful context/slot +save calls the shared-profile writer, and shutdown also calls it unless `set:NoSaveDat` suppresses shared +data writes. + +The `RT.DAT` header is `0x114` bytes: magic `0x54523353` (bytes `S3RT`), a compatibility id, a 256-byte +game id, version pair `1,0`, and script-record count. It is followed by 12-byte script records containing +`{script_id, message_count, pointer_placeholder}` and the corresponding `message_count` dword flag arrays. +The loader validates the header compatibility fields, allocates fresh arrays, and rebuilds the in-memory +hashtable. The port should own an equivalent profile-level model; matching the original raw pointer-bearing +file layout is optional compatibility work, not a prerequisite for native runtime semantics. + +The `/v2` Ghidra image now names/comments the lookup, queue, commit, mark, file read/write, and shared-profile +save/load chain and corrects the relevant function prototypes; saved 2026-07-18. + +### Remaining ADV control-strip actions and implementation cost (2026-07-18) + +The five standard controls are now fully inventoried. Auto message and all-message Skip have working host +services; Read-message Skip is the profile-wide `RT.DAT`/ReadTextDB slice above. The two other actions are +History and Hide Window, and they exercise different engine subsystems rather than variations of Skip. + +| Control | Native action | Current port boundary | Relative cost | +|---|---|---|---| +| History (`x=684`) | Cancel the ADV hotspot wait and run `HISTORY.BIN` over the text manager's retained record stream | Hotspot and nested `call-script` work, but retained text-history records, history query/render opcodes, generic mouse/joy callback dispatch, and several UI/font operations do not | High | +| Auto (`x=706`) | Toggle the Auto service | Implemented, including timed wait completion | Done | +| Message Skip (`x=728`) | Enable persistent all-message fast-forward | Implemented; pacing discrepancies remain a later fidelity adjustment | Done | +| Read-message Skip (`x=750`) | Toggle `message:ReadTextSkip`; gate advancement through shared ReadTextDB state | Native persistence and queue/commit/query flow investigated; service not implemented | Medium-high, bounded | +| Hide Window (`x=772`) | Op `0x199` enters the saved ADV coroutine handler, removes chrome, and runs `HIDEWIN.BIN` | Saved handler metadata and retained scene transforms exist, but op `0x199` and generic callback/input/cursor services do not | Medium | + +`HIDEWIN.BIN` is primarily an input/scheduler slice, not a new renderer. Static coverage is 29/37 distinct +opcodes handled (251/286 instructions); its eight effectful gaps are cursor selection (`0x86/0x87`), mouse +callback registration/dispatch (`0xcc/0xcd`), mouse-button state (`0x108`), and joy callback +registration/poll/dispatch (`0xfb/0xff/0x100`). The script saves retained-object translations, hides the ADV +chrome through the parent coroutine, allows the scene to be viewed/panned, then restores state. A clean port +slice therefore needs real op-`0x199` frame redirection plus a per-frame callback table and host input +snapshot; the existing retained renderer supplies the visual state. + +`HISTORY.BIN` is a substantially larger subsystem. Static coverage is 41/78 distinct opcodes handled +(761/854 instructions), leaving 37 effectful opcode kinds. AGE does not build the backlog from `RT.DAT`. +It retains a session text-manager stream of 0x48-byte records plus an 8-byte logical-message index. Op +`0x1d0` navigates message groups, `0x1d1` rerenders stored records with their font/color/layout metadata, +`0x1d3/0x1d4` recover typed metadata and voice pairs, and `0x1bd` replays a selected stored voice. History +also needs the generic callback/input layer used by Hide Window, local literal-array copies, and remaining +menu/text-surface operations. ReadTextDB can share the point where a message completes, but it cannot serve +as the backlog data model because it stores only read flags, not text, styling, names, or voice metadata. + +Implementation order by dependency and risk is therefore: **Hide Window first**, because it establishes +the reusable callback/coroutine input layer on an otherwise well-covered script; then Read-message Skip; +then History after both the input layer and message-completion seam exist. The `/v2` image names/comments +the cursor, callback dispatch, retained-history navigation/render/metadata, and history-voice opcode paths. + ### 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`: diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 431f743..7767ca0 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -8,7 +8,7 @@ ### 0x72 `wait-for-input` (wait-for-input, argc 1) - **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. 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. +- **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. The same click/Auto completion path calls read_text_db_queue_message@0x469340 with the current script id, resolved per-script message index, and message count; an already-skipped wait queues it directly in op 0x72. Op 0x71 later commits the pending records. ### 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. @@ -20,6 +20,26 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x7a_handler@0x41eba0 fetches operands 3,2,1 and calls adv_text_set_cursor@0x4530f0 on text manager ctx+0x14940. Slot 0 resolves manager+0x4c8; manager+0x414[slot] selects the layout; text_layout_set_cursor@0x452530 writes x/y to +4/+8 of its last 0x14-byte record. SC0000 0x9d3 computes slot 1, x=75, y=47 before voiced show-text. +### 0x1d0 `step-text-history` (u0041BA80, argc 3) +- **summary:** (out_group)(out_record)(delta) - move through retained ADV text-history groups and return the resolved group/record indices, or -1 at a boundary. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1d0_step_text_history@0x427360 calls text_history_step_group@0x4537c0 with operand 3 and mode mask 2, then writes two outputs. The helper walks the text manager's 8-byte history index vector over its 0x48-byte retained text records, skipping masked records and returning -1 outputs at a boundary. HISTORY.BIN uses negative deltas to count and page backward and positive deltas to page forward. + +### 0x1d1 `render-text-history` (u0041BAE0, argc 5) +- **summary:** (layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1d1_render_text_history@0x41f950 forwards all five operands plus the text manager at ctx+0x14508 to text_history_render_records@0x4526c0. That helper walks 0x48-byte retained text records, applies record flags/colors/font state, measures strings, and rasterizes/binds them to the chosen layout. HISTORY.BIN uses it to draw each visible backlog line. + +### 0x1d3 `find-text-history-value` (u0041BB90, argc 5) +- **summary:** (out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1d3_find_text_history_value@0x4273c0 calls text_history_find_typed_value@0x450840 and writes its boolean result plus returned value. The helper scans the 0x48-byte text records within a logical group for flag 0x20000000 and matching type in record+0x18, returning record+0x14. HISTORY.BIN queries types 1 and 2 for line decoration/name metadata. + +### 0x1d4 `find-text-history-pair` (u0041BC00, argc 4) +- **summary:** (out_a)(out_b)(direction)(record_index) - find paired metadata within one retained ADV message group. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1d4_find_text_history_pair@0x427430 calls text_history_find_pair@0x4509f0 and writes two outputs. The helper scans the logical group for a record flagged 0x40000000 and returns its +0x14/+0x18 pair. HISTORY.BIN uses the pair to expose voice replay for a backlog entry. + ### 0x204 `draw-string` (draw-string, argc 4) - **summary:** (surface_slot)(x)(y)(string) - rasterize a CP932 string immediately into a numbered graphics surface using current font/color/effect state. - **grounding:** source=investigation, confidence=high @@ -57,6 +77,11 @@ - **grounding:** source=investigation, confidence=high - **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it starts the voice immediately; while set it stores the latest id/zero arg at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active. +### 0x1bd `play-history-voice` (u0041D910, argc 1) +- **summary:** (voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice, starts operand 1 through the native voice service when Skip is inactive (or queues it while Skip is active), records the replay in the message voice state when enabled, and sets adv_auto_voice_pending when playback exists. HISTORY.BIN obtains the id from retained text-record metadata before invoking this opcode. + ## control ### 0x3 `call-script` (call-script, argc 1) @@ -123,7 +148,7 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t - **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 - **depended on by:** 0x20c, 0x20d, 0x21c, 0x223 -- **evidence:** Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Producer recovered 2026-07-10: adv_update_read_text_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message_ReadTextSkip plus current-PC read-history lookup; adv_interpreter_tick consumes it in click/read-skip control. It is not the 0x223 surface-transition progress flag. +- **evidence:** Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). adv_refresh_read_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message:ReadTextSkip plus read_text_db_find_message_index@0x468f50 and read_text_db_is_message_read@0x469930. The database is engine-owned shared RT.DAT state keyed by raw packed script resource id and per-script message index, not VM globals or slot-local SAVE##.DAT data. adv_interpreter_tick consumes the result in click/read-skip control; it is not op 0x223 surface-transition progress. ### 0x21c `mark-frame-yield` (mark-frame-yield, argc 0) - **summary:** Set native run-state bit 0x400; in normal ADV playback this is the retained-presentation render/wait/resume boundary. @@ -333,6 +358,16 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b ## input +### 0x86 `set-cursor-resource` (u0041B210, argc 1) +- **summary:** (resource_id) - load an indexed cursor asset and install it as the active custom cursor. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x86_set_cursor_resource@0x41f0a0 opens operand 1 through asset_open_indexed_entry, extracts the asset payload, passes it to the cursor installer at 0x485ce0, releases the asset, and refreshes the OS cursor when the window is active. HIDEWIN.BIN selects resources 0x3318..0x331f according to the pointer's screen-edge region. + +### 0x87 `clear-cursor-resource` (u00414D10, argc 0) +- **summary:** Clear the active custom cursor and refresh the OS cursor when the game window is active. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x87_clear_cursor_resource@0x416400 calls the cursor clear/release helper at 0x4856b0 and then the same active-window cursor refresh used by op 0x86. HIDEWIN.BIN calls it when leaving an edge region and on exit. + ### 0x88 `set-message-skip` (u0041B290, argc 1) - **summary:** (enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit. - **grounding:** source=investigation, confidence=high @@ -380,11 +415,41 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **depends on:** 0x90 - **evidence:** Ghidra /v2: op_0x97_bind_hotspot_key@0x41ff30 builds the same inclusive rect as op 0x90 and calls input_hotspot_bind_key_bit@0x403f50. That worker searches registered rects for exact equality and stores operand 5 in the record's key-bit array. SC0000 binds bits 0, 8, and 7 to its three 1x1 keyed records. +### 0xcc `register-mouse-callback` (mouse_callback, argc 2) +- **summary:** (poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0xcc_register_mouse_callback@0x420f70 stores operand 2 as the callback PC and the current frame_script_resource_id as its owner, then arms operand 1 as the poll interval. HIDEWIN.BIN and HISTORY.BIN both register a 0x10-ms callback. Op 0xcd performs the timed dispatch. + +### 0xcd `dispatch-mouse-callback` (get-input-type, argc 0) +- **summary:** Dispatch the registered mouse callback when its polling interval elapses. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0xcd_dispatch_mouse_callback@0x417e10 compares timeGetTime against the registered interval, pushes the following PC, verifies the callback's saved script resource id matches the current frame, and jumps to the registered callback target. It writes no operand, so the upstream get-input-type label was incorrect. + +### 0xfb `register-joy-callback` (joy_callback, argc 2) +- **summary:** (input_index)(target_pc) - register one of 32 per-frame joy/input callback targets. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0xfb_register_joy_callback@0x421270 bounds-checks operand 1 to 0..31 and stores operand 2 in the current script frame's 33-entry callback table. Ops 0xff/0x100 poll and dispatch this table; HISTORY.BIN and HIDEWIN.BIN register indices 0..10. + +### 0xff `poll-joy-callback-input` (u00415A10, argc 0) +- **summary:** Poll the current joy/input callback bitmask and initialize the per-dispatch scan state. +- **grounding:** source=investigation, confidence=med +- **evidence:** Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through the input poller at 0x4608b0, resets the scan index, and snapshots the current input selector. It pairs with op 0x100. + +### 0x100 `dispatch-joy-callbacks` (u00415A60, argc 0) +- **summary:** Dispatch registered callbacks for the current or pending joy/input selection. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff (or uses the current selector when no mask is present), pushes a return PC, and jumps through the current frame's callback table populated by op 0xfb. + ### 0x101 `reset-message-skip-input` (u00415BF0, argc 0) - **summary:** Reset transient message-skip/input service state after an ADV chrome action without clearing op 0x88's persistent all-message Skip flag. - **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. It does not touch ctx+0x13dc or ctx+0x550fc, so adv_interpreter_tick re-injects Skip on the following tick while persistent state remains enabled. The Auto, Message-skip, Read-skip, and Hide-window callbacks invoke it after their 100 ms cursor re-arm sequence. +### 0x108 `get-mouse-button-state` (u00415E70, argc 1) +- **summary:** (out) - return the current mouse-button state bitmask. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x108_get_mouse_button_state@0x428b60 fills a local through the mouse-state helper at 0x4602e0 and writes it to operand 1. HIDEWIN.BIN and HISTORY.BIN test individual bits to detect press/release transitions. + ### 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 @@ -479,8 +544,9 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " ## structural ### 0x71 `label-def` (u0041A7B0, argc 1) -- **summary:** 1 imm; count == T1 table size -> the label/anchor T1 indexes. v1 no-op; revisit if menu/callback dispatch looks up by id -- **grounding:** source=investigation, confidence=high, noop_headless=True +- **summary:** (anchor_id) - define a T1 label/anchor, snapshot the current code position, and commit pending read-message records to the shared ReadTextDB. +- **grounding:** source=investigation, confidence=high +- **evidence:** Corpus: count exactly matches each script's T1 table and T1 entries target these records, preserving the structural label role. Ghidra /v2: op_0x71_handler@0x41e540 records (frame_pc-frame_codebase)/4 at the current frame's +0x20 field, snapshots text state, and calls read_text_db_commit_pending@0x46ae20. That drains queued {script_resource_id,message_index,message_count} records into ReadTextDB. script_frame_load_resource@0x40e980 stores the raw packed SYS4/AAI resource id at frame+0x04 (EngineCtx+0x53d64), establishing the persistence key. Therefore 0x71 is not a pure runtime no-op when persistent read history is modeled. ## unknown @@ -648,14 +714,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 -### 0x86 `u0041B210` (u0041B210, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x87 `u00414D10` (u00414D10, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x8b `u0041B3D0` (u0041B3D0, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -716,14 +774,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 -### 0xcc `mouse_callback` (mouse_callback, argc 2) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - -### 0xcd `get-input-type` (get-input-type, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0xd0 `u00415830` (u00415830, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -740,30 +790,14 @@ 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 -### 0xfb `joy_callback` (joy_callback, argc 2) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0xfe `u0041E360` (u0041E360, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0xff `u00415A10` (u00415A10, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x100 `u00415A60` (u00415A60, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x107 `u0041E500` (u0041E500, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x108 `u00415E70` (u00415E70, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x10b `u0041E5A0` (u0041E5A0, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -984,10 +1018,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 -### 0x1bd `u0041D910` (u0041D910, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1c1 `u0041B820` (u0041B820, argc 3) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -1004,22 +1034,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 -### 0x1d0 `u0041BA80` (u0041BA80, argc 3) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x1d1 `u0041BAE0` (u0041BAE0, argc 5) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x1d3 `u0041BB90` (u0041BB90, argc 5) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x1d4 `u0041BC00` (u0041BC00, argc 4) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1f6 `u00416170` (u00416170, argc 0) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 912f736..d7cdd8d 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1558,3 +1558,44 @@ again. Native still executes one opcode per interpreter tick during Skip. Godot frame pulse at each `FrameYield` only while persistent message Skip is enabled, retaining normal free-running bursts outside Skip. Validation: engine 168/168, zero-warning Godot build, threaded `SELFTEST OK`; repeat the x=728 pacing check before starting Read-message Skip. + +### ADV Read-message Skip persistence investigation (2026-07-18) + +Native RE confirms that this service cannot be implemented as a second all-message toggle. AGE resolves the +current script code offset through its per-frame message-offset table, then tests a dword flag in an +engine-owned `ReadTextDB` keyed by the raw packed script resource id and per-script message index. Click, +Auto, and active-Skip completion queue `{script_resource_id,index,count}`; opcode `0x71`, while still the +structural T1 label/anchor, +commits those pending records and therefore is not a pure runtime no-op. + +The database is profile-wide rather than slot-local. AGE loads and writes `RT.DAT` beside shared +`SAVE.DAT`, using `$$RT.DAT` and `RT.BAK` for replacement/backup; numbered game slots remain +`SAVE%2.2d.DAT`. Successful slot/context saves and normal shutdown persist the shared database. The full +native chain and file layout are recorded in +[`engine-re.md`](engine-re.md#adv-read-message-skip-and-shared-rtdat-history-2026-07-18), and `/v2` is +renamed/commented/saved. + +**Next:** implement a profile-level read-history service behind the VM/host boundary, make click/Auto/Skip +completion queue the native tuple, make op `0x71` commit it, and use the same resolver for op `0x1cc`. +Persist it in a port-owned shared format first; raw `RT.DAT` import/export can remain a separate compatibility +slice. + +### ADV remaining-button difficulty inventory (2026-07-18) + +The remaining action buttons beyond Read-message Skip are History (`x=684`) and Hide Window (`x=772`). +They are not additional toggle services. `HIDEWIN.BIN` is 29/37 distinct opcodes covered (251/286 +instructions) and mainly needs the real op-`0x199` coroutine transition plus AGE's per-frame mouse/joy +callback tables, button-state polling, and cursor-resource operations. Existing retained-object translation +and presentation support covers its scene-view/pan behavior, making this a medium-sized infrastructure slice. + +`HISTORY.BIN` is 41/78 distinct opcodes covered (761/854 instructions) and needs a separate session-level +retained text-record model, navigation/render/metadata ops `0x1d0/0x1d1/0x1d3/0x1d4`, stored-voice replay, +the same generic input-callback layer, literal local-array initialization, and additional menu/text-surface +operations. The shared ReadTextDB is not a backlog: it stores only per-message read flags. History is +therefore the largest remaining control-strip action. + +**Recommended next slice:** implement Hide Window first as the smallest clean remaining action and use it to +land the generic coroutine/input-callback service that History will later reuse. Read-message Skip remains +the next bounded persistence slice; History should follow after both foundations exist. Full native evidence +and the difficulty table live in +[`engine-re.md`](engine-re.md#remaining-adv-control-strip-actions-and-implementation-cost-2026-07-18). diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 9be94c9..ab1dc20 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -4,9 +4,11 @@ from __future__ import annotations INFERRED: dict[int, dict] = { - 0x71: dict(name='label-def', category='structural', noop=True, confidence='high', source='investigation', summary='1 imm; count == T1 table size -> the label/anchor T1 indexes. v1 no-op; revisit if menu/callback dispatch looks up by id'), + 0x71: dict(name='label-def', category='structural', noop=False, confidence='high', source='investigation', summary='(anchor_id) - define a T1 label/anchor, snapshot the current code position, and commit pending read-message records to the shared ReadTextDB.'), 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.'), + 0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'), + 0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'), 0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'), 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."), @@ -16,8 +18,14 @@ INFERRED: dict[int, dict] = { 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.'), + 0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'), + 0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'), 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.'), + 0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'), + 0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='med', source='investigation', summary='Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.'), + 0x100: dict(name='dispatch-joy-callbacks', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch registered callbacks for the current or pending joy/input selection.'), 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 without clearing op 0x88's persistent all-message Skip flag."), + 0x108: dict(name='get-mouse-button-state', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current mouse-button state bitmask.'), 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."), @@ -28,10 +36,15 @@ INFERRED: dict[int, dict] = { 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.'), + 0x1bd: dict(name='play-history-voice', category='audio', noop=False, confidence='high', source='investigation', summary='(voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior.'), 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`.'), + 0x1d0: dict(name='step-text-history', category='adv', noop=False, confidence='high', source='investigation', summary='(out_group)(out_record)(delta) - move through retained ADV text-history groups and return the resolved group/record indices, or -1 at a boundary.'), + 0x1d1: dict(name='render-text-history', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface.'), 0x1d2: dict(name='stmt-desc?', category='marker', noop=True, confidence='med', source='harness', summary='2 imm; immediately after stmt-begin 0x1f4 — statement descriptor?'), + 0x1d3: dict(name='find-text-history-value', category='adv', noop=False, confidence='high', source='investigation', summary='(out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group.'), + 0x1d4: dict(name='find-text-history-pair', category='adv', noop=False, confidence='high', source='investigation', summary='(out_a)(out_b)(direction)(record_index) - find paired metadata within one retained ADV message group.'), 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'), 0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index cba9bcb..53e93e2 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -104,9 +104,9 @@ type = "void*" note = "0x78-byte context-record array base (coroutine/script contexts)" [[field]] offset = 0x53d64 -name = "gfx_obj_record_array" -type = "void*" -note = "gfx object-record array (field[0]=0xffffffff free; cmd-type at rec+0x24)" +name = "frame_script_resource_id" +type = "uint" +note = "raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key" [[field]] offset = 0x53d88 name = "cmd_type_table" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 16e20ce..a562b4c 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -994,16 +994,16 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "label-def" category = "structural" -summary = "1 imm; count == T1 table size -> the label/anchor T1 indexes. v1 no-op; revisit if menu/callback dispatch looks up by id" -noop_headless = true +summary = "(anchor_id) - define a T1 label/anchor, snapshot the current code position, and commit pending read-message records to the shared ReadTextDB." +noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "" +evidence = "Corpus: count exactly matches each script's T1 table and T1 entries target these records, preserving the structural label role. Ghidra /v2: op_0x71_handler@0x41e540 records (frame_pc-frame_codebase)/4 at the current frame's +0x20 field, snapshots text state, and calls read_text_db_commit_pending@0x46ae20. That drains queued {script_resource_id,message_index,message_count} records into ReadTextDB. script_frame_load_resource@0x40e980 stores the raw packed SYS4/AAI resource id at frame+0x04 (EngineCtx+0x53d64), establishing the persistence key. Therefore 0x71 is not a pure runtime no-op when persistent read history is modeled." [[opcode.semantics.args]] i = 1 -role = "" +role = "label/anchor id" observed_types = ["imm", "l-int"] [[opcode]] @@ -1020,7 +1020,7 @@ 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. 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." +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. The same click/Auto completion path calls read_text_db_queue_message@0x469340 with the current script id, resolved per-script message index, and message count; an already-skipped wait queues it directly in op 0x72. Op 0x71 later commits the pending records." [[opcode.semantics.args]] i = 1 @@ -1346,18 +1346,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041B210" -category = "unknown" -summary = "" +name = "set-cursor-resource" +category = "input" +summary = "(resource_id) - load an indexed cursor asset and install it as the active custom cursor." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x86_set_cursor_resource@0x41f0a0 opens operand 1 through asset_open_indexed_entry, extracts the asset payload, passes it to the cursor installer at 0x485ce0, releases the asset, and refreshes the OS cursor when the window is active. HIDEWIN.BIN selects resources 0x3318..0x331f according to the pointer's screen-edge region." [[opcode.semantics.args]] i = 1 -role = "" +role = "cursor resource id" observed_types = ["imm"] [[opcode]] @@ -1367,14 +1367,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00414D10" -category = "unknown" -summary = "" +name = "clear-cursor-resource" +category = "input" +summary = "Clear the active custom cursor and refresh the OS cursor when the game window is active." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x87_clear_cursor_resource@0x416400 calls the cursor clear/release helper at 0x4856b0 and then the same active-window cursor refresh used by op 0x86. HIDEWIN.BIN calls it when leaving an edge region and on exit." [[opcode]] op = 0x88 @@ -2064,23 +2064,23 @@ argc = 2 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "mouse_callback" -category = "unknown" -summary = "" +name = "register-mouse-callback" +category = "input" +summary = "(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0xcc_register_mouse_callback@0x420f70 stores operand 2 as the callback PC and the current frame_script_resource_id as its owner, then arms operand 1 as the poll interval. HIDEWIN.BIN and HISTORY.BIN both register a 0x10-ms callback. Op 0xcd performs the timed dispatch." [[opcode.semantics.args]] i = 1 -role = "" +role = "poll interval (ms)" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "" +role = "callback code target" observed_types = ["imm"] [[opcode]] @@ -2090,14 +2090,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "get-input-type" -category = "unknown" -summary = "" +name = "dispatch-mouse-callback" +category = "input" +summary = "Dispatch the registered mouse callback when its polling interval elapses." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0xcd_dispatch_mouse_callback@0x417e10 compares timeGetTime against the registered interval, pushes the following PC, verifies the callback's saved script resource id matches the current frame, and jumps to the registered callback target. It writes no operand, so the upstream get-input-type label was incorrect." [[opcode]] op = 0xd0 @@ -2216,23 +2216,23 @@ argc = 2 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "joy_callback" -category = "unknown" -summary = "" +name = "register-joy-callback" +category = "input" +summary = "(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0xfb_register_joy_callback@0x421270 bounds-checks operand 1 to 0..31 and stores operand 2 in the current script frame's 33-entry callback table. Ops 0xff/0x100 poll and dispatch this table; HISTORY.BIN and HIDEWIN.BIN register indices 0..10." [[opcode.semantics.args]] i = 1 -role = "" +role = "input index" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "" +role = "callback code target" observed_types = ["imm"] [[opcode]] @@ -2263,14 +2263,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00415A10" -category = "unknown" -summary = "" +name = "poll-joy-callback-input" +category = "input" +summary = "Poll the current joy/input callback bitmask and initialize the per-dispatch scan state." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "med" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through the input poller at 0x4608b0, resets the scan index, and snapshots the current input selector. It pairs with op 0x100." [[opcode]] op = 0x100 @@ -2279,14 +2279,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00415A60" -category = "unknown" -summary = "" +name = "dispatch-joy-callbacks" +category = "input" +summary = "Dispatch registered callbacks for the current or pending joy/input selection." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff (or uses the current selector when no mask is present), pushes a return PC, and jumps through the current frame's callback table populated by op 0xfb." [[opcode]] op = 0x101 @@ -2337,18 +2337,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00415E70" -category = "unknown" -summary = "" +name = "get-mouse-button-state" +category = "input" +summary = "(out) - return the current mouse-button state bitmask." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x108_get_mouse_button_state@0x428b60 fills a local through the mouse-state helper at 0x4602e0 and writes it to operand 1. HIDEWIN.BIN and HISTORY.BIN test individual bits to detect press/release transitions." [[opcode.semantics.args]] i = 1 -role = "" +role = "mouse button bitmask out" observed_types = ["l-int"] [[opcode]] @@ -4054,18 +4054,18 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041D910" -category = "unknown" -summary = "" +name = "play-history-voice" +category = "audio" +summary = "(voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice, starts operand 1 through the native voice service when Skip is inactive (or queues it while Skip is active), records the replay in the message voice state when enabled, and sets adv_auto_voice_pending when playback exists. HISTORY.BIN obtains the id from retained text-record metadata before invoking this opcode." [[opcode.semantics.args]] i = 1 -role = "" +role = "voice id" observed_types = ["l-int"] [[opcode]] @@ -4218,7 +4218,7 @@ noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). Producer recovered 2026-07-10: adv_update_read_text_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message_ReadTextSkip plus current-PC read-history lookup; adv_interpreter_tick consumes it in click/read-skip control. It is not the 0x223 surface-transition progress flag." +evidence = "Ghidra handler 0x427330 calls vm_operand_write(1, ctx+0x6dbd4). adv_refresh_read_skip_state@0x406cd0 and op 0x6e/0x71/0x72 maintain the field from message:ReadTextSkip plus read_text_db_find_message_index@0x468f50 and read_text_db_is_message_read@0x469930. The database is engine-owned shared RT.DAT state keyed by raw packed script resource id and per-script message index, not VM globals or slot-local SAVE##.DAT data. adv_interpreter_tick consumes the result in click/read-skip control; it is not op 0x223 surface-transition progress." [[opcode.semantics.args]] i = 1 @@ -4274,28 +4274,28 @@ argc = 3 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041BA80" -category = "unknown" -summary = "" +name = "step-text-history" +category = "adv" +summary = "(out_group)(out_record)(delta) - move through retained ADV text-history groups and return the resolved group/record indices, or -1 at a boundary." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1d0_step_text_history@0x427360 calls text_history_step_group@0x4537c0 with operand 3 and mode mask 2, then writes two outputs. The helper walks the text manager's 8-byte history index vector over its 0x48-byte retained text records, skipping masked records and returning -1 outputs at a boundary. HISTORY.BIN uses negative deltas to count and page backward and positive deltas to page forward." [[opcode.semantics.args]] i = 1 -role = "" +role = "history group out" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "history record out" observed_types = ["l-int"] [[opcode.semantics.args]] i = 3 -role = "" +role = "group delta" observed_types = ["l-int"] [[opcode]] @@ -4305,23 +4305,23 @@ argc = 5 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041BAE0" -category = "unknown" -summary = "" +name = "render-text-history" +category = "adv" +summary = "(layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1d1_render_text_history@0x41f950 forwards all five operands plus the text manager at ctx+0x14508 to text_history_render_records@0x4526c0. That helper walks 0x48-byte retained text records, applies record flags/colors/font state, measures strings, and rasterizes/binds them to the chosen layout. HISTORY.BIN uses it to draw each visible backlog line." [[opcode.semantics.args]] i = 1 -role = "" +role = "layout slot" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "history record index" observed_types = ["l-int"] [[opcode.semantics.args]] @@ -4372,23 +4372,23 @@ argc = 5 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041BB90" -category = "unknown" -summary = "" +name = "find-text-history-value" +category = "adv" +summary = "(out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1d3_find_text_history_value@0x4273c0 calls text_history_find_typed_value@0x450840 and writes its boolean result plus returned value. The helper scans the 0x48-byte text records within a logical group for flag 0x20000000 and matching type in record+0x18, returning record+0x14. HISTORY.BIN queries types 1 and 2 for line decoration/name metadata." [[opcode.semantics.args]] i = 1 -role = "" +role = "found out" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "value out" observed_types = ["l-int"] [[opcode.semantics.args]] @@ -4398,12 +4398,12 @@ observed_types = ["imm"] [[opcode.semantics.args]] i = 4 -role = "" +role = "history record index" observed_types = ["l-int"] [[opcode.semantics.args]] i = 5 -role = "" +role = "metadata type" observed_types = ["imm"] [[opcode]] @@ -4413,23 +4413,23 @@ argc = 4 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041BC00" -category = "unknown" -summary = "" +name = "find-text-history-pair" +category = "adv" +summary = "(out_a)(out_b)(direction)(record_index) - find paired metadata within one retained ADV message group." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1d4_find_text_history_pair@0x427430 calls text_history_find_pair@0x4509f0 and writes two outputs. The helper scans the logical group for a record flagged 0x40000000 and returns its +0x14/+0x18 pair. HISTORY.BIN uses the pair to expose voice replay for a backlog entry." [[opcode.semantics.args]] i = 1 -role = "" +role = "first value out" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "second value out" observed_types = ["l-int"] [[opcode.semantics.args]] @@ -4439,7 +4439,7 @@ observed_types = ["imm"] [[opcode.semantics.args]] i = 4 -role = "" +role = "history record index" observed_types = ["l-int"] [[opcode]]