Implement native ADV message skip
This commit is contained in:
@@ -10,6 +10,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x40c` | `sys4ini_count` | `int` | SYS4INI record count |
|
||||
| `0x410` | `archive_name_table` | `void*` | archive-name table base (arc_id*0x100 indexes it) |
|
||||
| `0x414` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50 |
|
||||
| `0x13dc` | `message_skip_enabled` | `int` | persistent all-message Skip flag; op 0x88 writes it and adv_interpreter_tick injects input bit 0x40 while nonzero |
|
||||
| `0x3028` | `alt_pack_table` | `int` | call-script high-byte alternate pack table (unused by corpus) |
|
||||
| `0xb558` | `gfx_dirty_a` | `int` | gfx dirty flag (anim set raises) |
|
||||
| `0xb560` | `gfx_dirty_b` | `int` | gfx dirty flag |
|
||||
@@ -26,6 +27,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)) |
|
||||
| `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 |
|
||||
| `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand |
|
||||
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
|
||||
@@ -33,12 +35,16 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) |
|
||||
| `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) |
|
||||
| `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) |
|
||||
| `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation |
|
||||
| `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero |
|
||||
| `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) |
|
||||
| `0x6db28` | `coroutine_yield_b` | `void*` | op 0x7b yield-state save (op2 -> +ctxidx*4) |
|
||||
| `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 |
|
||||
| `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active |
|
||||
| `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero |
|
||||
| `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) |
|
||||
|
||||
@@ -1156,6 +1156,43 @@ injection. Timer and VM regressions cover both paths, exact deadlines, disable/r
|
||||
fallback, `0x1b8`/`0x1b9`, and the `0xc4`/`0x1bc` voice-state lifecycle. The `/v2` handlers and input poller
|
||||
are named/commented and saved.
|
||||
|
||||
### ADV all-message Skip service (2026-07-18)
|
||||
|
||||
The x=728 control enables a persistent interpreter service; it is not a one-page advance and its active
|
||||
overlay is not the transient run-state bit. `op_0x88_set_message_skip@0x41f130` writes the requested value to
|
||||
`ctx+0x13dc` (`message_skip_enabled`) and `ctx+0x550fc` (`message_skip_display_enabled`). While the former is
|
||||
nonzero, `adv_interpreter_tick@0x410fb0` injects input bit `0x40` on every interpreter tick. The input service
|
||||
turns that into `run_state_flags & 0x08000000`, completing text/input waits and selecting the already-reversed
|
||||
skip endpoints for retained transitions.
|
||||
|
||||
The state queries are deliberately different:
|
||||
|
||||
- op `0x19a` returns persistent `message_skip_display_enabled` for SO001's active x=728 overlay;
|
||||
- op `0x1c7` returns the transient skip run-state bit, which may be driven by persistent op-`0x88` state or
|
||||
a physical fast-forward input such as Ctrl;
|
||||
- op `0x101` clears transient input/run-state fields after an ADV chrome action, but does not touch either
|
||||
persistent op-`0x88` field. The following interpreter tick therefore re-arms Skip.
|
||||
|
||||
All 301 ordinary ADV button callbacks call `0x88(1)`. `CALLBACK_LOAD.BIN` contains the corpus's only
|
||||
`0x88(0)` reset. The engine supports an optional click-cancel state machine, but
|
||||
`engine_settings_register_defaults@0x46be30` registers `set:CancelMesSkipOnClick=0`; a nonzero loaded engine
|
||||
configuration is required to enable its press/release cancellation path. The port preserves that default
|
||||
instead of unconditionally inventing click-to-cancel.
|
||||
|
||||
Voice playback also has a native Skip queue. `op_0xc4_handler@0x420610` plays immediately while the skip bit
|
||||
is clear. While it is set, the handler replaces `ctx+0x6dbf4/+0x6dbf8` with the latest requested voice instead
|
||||
of starting it. When Skip/read-skip input ends, `adv_interpreter_tick` starts that latest voice and clears the
|
||||
queue; skipped voices do not accumulate.
|
||||
|
||||
**Port implementation.** The VM owns persistent op-`0x88` state and keeps op `0x19a` separate from the
|
||||
combined op-`0x1c7` persistent/host-input query. `GodotAdvHost` completes text reveal and stable message waits
|
||||
while Skip is active; the existing `0x1c7` transition branches continue to publish their completed endpoints.
|
||||
Voice requests replace one host-side deferred payload during Skip and the latest payload starts when
|
||||
`0x88(0)` arrives. Enabling Skip through the real SC0000 hotspot callback wakes the parked wait without
|
||||
creating a synthetic pointer click. Focused tests cover `0x88` enable/disable, `0x19a`, `0x1c7`, transient
|
||||
`0x101`, retained host state, and the actual x=728 callback. The new EngineCtx fields are applied to `/v2`;
|
||||
the affected handlers, interpreter tick, and settings-default initializer 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`:
|
||||
|
||||
@@ -53,9 +53,9 @@
|
||||
- **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). Same rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). While all-message Skip is active, retain/replace the queued voice id instead of starting it; playback resumes from the latest queued id after Skip clears. Same resolver rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## control
|
||||
|
||||
@@ -334,9 +334,9 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
|
||||
## input
|
||||
|
||||
### 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.
|
||||
- **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
|
||||
- **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.
|
||||
- **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 on every tick while ctx+0x13dc is nonzero. engine_settings_register_defaults@0x46be30 initializes set:CancelMesSkipOnClick to 0; only a nonzero override enables its press/release cancellation path. All 301 ADV button callbacks pass 1; CALLBACK_LOAD.BIN is the sole corpus 0x88(0) reset.
|
||||
|
||||
### 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.
|
||||
@@ -381,9 +381,9 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **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.
|
||||
- **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. The Auto, Message-skip, Read-skip, and Hide-window click callbacks invoke it after their 100 ms cursor re-arm sequence.
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -1532,3 +1532,22 @@ 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.
|
||||
|
||||
### ADV all-message Skip implemented (2026-07-18)
|
||||
|
||||
The x=728 action now enables the native persistent Skip service through op `0x88`. The VM distinguishes
|
||||
persistent button/display state (op `0x19a`) from the live skip query (op `0x1c7`, persistent state OR the
|
||||
existing host fast-forward channel), and op `0x101` resets only transient input state. Godot completes text
|
||||
reveal and stable waits while Skip is active; the existing skip-aware transition branches publish their final
|
||||
endpoints. Voice calls follow the native latest-only queue: requests replace one deferred voice while Skip is
|
||||
active, and the most recent request starts when op `0x88(0)` clears the service.
|
||||
|
||||
Native `set:CancelMesSkipOnClick` defaults to zero, so this slice deliberately does not turn arbitrary clicks
|
||||
into a port-only cancel gesture. A future settings-profile implementation can expose the native nonzero
|
||||
press/release policy without changing the Skip state seam. Regressions exercise the opcode lifecycle and the
|
||||
real SC0000 x=728 callback. Validation: engine 167/167, zero-warning Godot build, opcode and EngineCtx
|
||||
generators/tests/lints clean, and threaded `SELFTEST OK`.
|
||||
|
||||
**Next:** manually validate that x=728 fast-forwards text and transitions and that a later `0x88(0)` boundary
|
||||
returns to normal pacing. Then implement Read-message Skip on the same service seam, adding the required
|
||||
per-script-offset read-history model rather than treating it as another global all-skip toggle.
|
||||
|
||||
Reference in New Issue
Block a user