From 7d438a75749c0fec3c4fd30fef13d35a3fb5cb97 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 20 Jul 2026 16:41:44 -0400 Subject: [PATCH] Implement ADV skip lifecycle --- docs/engine-ctx-reference.md | 2 + docs/engine-re.md | 37 +++++++++++++++ docs/opcode-reference.md | 27 ++++++----- docs/phase-a-slice-plan.md | 28 ++++++++++++ engine/Age.Engine.Tests/HotspotInputTests.cs | 47 ++++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 20 +++++++-- tools/age_opcodes_himegari.py | 3 ++ vm-map/engine-ctx.toml | 10 +++++ vm-map/opcodes.toml | 36 +++++++-------- 9 files changed, 176 insertions(+), 34 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index e2f3bc6..9889dfc 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -33,12 +33,14 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x51b80` | `gfx_animation_service_flags` | `uint` | retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request | | `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate | | `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records | +| `0x53d24` | `save_load_stack_restore_active` | `int` | set while a deserialized numbered-save script stack is being reconstructed; op 0xae restores saved frame PCs/contexts and clears it at the terminal saved context | | `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` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key | | `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 | | `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay | +| `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip | | `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes | | `0x55110` | `text_history_recording_suppressed` | `uint` | high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable | | `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand | diff --git a/docs/engine-re.md b/docs/engine-re.md index 1690214..7602445 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1240,6 +1240,23 @@ 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 entry/exit lifecycle.** The script comments `savemesskip` and `loadmesskip` describe a temporary +suspension boundary, not a second saved preference. `op_0x19b_suspend_adv_skip_service@0x416560` clears +active `ctx+0x13dc`, run-state bit `0x08000000`, and `ctx+0x55100` (`adv_skip_service_enabled`) while +deliberately preserving `ctx+0x550fc`, the all-message Skip toggle returned by op `0x19a`. +`op_0x19c_resume_adv_skip_service@0x4165a0` sets the lifecycle gate again and reconstructs active +fast-forward from persistent all-message Skip or `adv_read_skip_state`; the separate startup guard at +`ctx+0x6f86c` can suppress that reactivation. SC0000 brackets ADV teardown/setup with this pair, as do +branch transitions and `CALLBACK_LOAD.BIN`. + +The port therefore keeps persistent Skip separate from currently active fast-forward. Opcode `0x19b` +deactivates the host service without changing the control-strip toggle, and `0x19c` recomputes the host +service from the persistent toggle plus the host's live read-skip channel. This also makes op `0x1c7` +correctly report inactive during the suspended interval. Focused regressions cover preservation across the +pair and read-skip-only reactivation. Implementing the `message:ReadTextSkip` preference itself remains +deferred with ops `0x1ca`/`0x1cb` and the profile-owned `ReadTextDB`; the lifecycle implementation does not +invent a storage backend. + ### Opcode `0x1ad` marks the numbered-save resume-frame boundary (2026-07-20) Opcode `0x1ad` is a zero-operand persistence marker, not an input reset or modal-UI synchronization call. @@ -1266,6 +1283,26 @@ real implementation belongs in the future unified save architecture, where the V `ExecFrame` chain and remember which frame is the resume boundary. This is the same architectural deferral as the already-deferred profile/read-state work, not a reason to invent a seed or offset-specific shortcut. +### Opcode `0xae` continues numbered-save stack restoration (2026-07-20) + +Opcode `0xae` is the load-side rendezvous paired with serialized script-frame state. Its handler, +`op_0xae_continue_save_load_stack_restore@0x416790`, normally returns after recording its one-dword length. +It only becomes effectful while `ctx+0x53d24` (`save_load_stack_restore_active`) is set by +`save_data_deserialize_and_begin_restore@0x40fd10`. That deserializer restores the selected save layout, +loads `CALLBACK_LOAD.BIN` or the saved entry script, and resets the current context so ordinary opcode +`0xae` sites can rebuild the saved stack. + +On an active restore, the handler reads `set:SaveVersion1`/`set:SaveVersion2`, selects the matching saved +frame layout, replaces the current frame PC with its saved resume or call target, and advances through the +serialized contexts. At the saved terminal context it clears the restore flag and reinstates the saved +context/return state. The corpus placement supports that control-flow role: 305 calls overwhelmingly follow +coroutine-resume or call boundaries, including SC0000's main-loop resume sequence. + +The opcode remains an effectful port gap. It is a no-op during every currently reachable port execution, +but its actual branch cannot be implemented until numbered saves serialize and restore the active +`ExecFrame` chain. Counting an unconditional no-op as coverage would conceal that dependency, so it stays +grouped with `0x1ad` rather than receiving a placeholder VM case. + ### 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 diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index b7a8027..03a197c 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -251,6 +251,11 @@ This also names the whole call graph statically (build/callscript-names.json). - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0xa3_handler@0x420060 formats operand 1, queries value_dispatch_lookup@0x419290, writes the matched or operand-2 default PC into the current frame, and clears the command type. Corpus pairs it with 0xa1/0xa2 in 12 generic switch sequences. +### 0xae `continue-save-load-stack-restore` (u00415130, argc 0) +- **summary:** () - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with FUN_0040f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack. + ### 0xc8 `sleep` (sleep, argc 1) - **summary:** Pause the current script for milliseconds while retained presentation continues. - **grounding:** source=investigation, confidence=high @@ -681,6 +686,16 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **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. +### 0x19b `suspend-adv-skip-service` (u00414E80, argc 0) +- **summary:** () - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. Corpus comments call it `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation. + +### 0x19c `resume-adv-skip-service` (u00414EC0, argc 0) +- **summary:** () - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x19c_resume_adv_skip_service@0x4165a0 sets ctx+0x55100, clears the click-cancel phase, and normally activates run-state bit 0x08000000 when ctx+0x550fc persistent all-message Skip or ctx+0x6dbd4 adv_read_skip_state is nonzero. Otherwise it clears ctx+0x13dc and the run-state bit. A separate startup fast-forward guard at ctx+0x6f86c suppresses reactivation while active. Corpus comments call it `loadmesskip`; ADV setup, branch, and CALLBACK_LOAD paths pair it with op 0x19b. + ### 0x1b6 `get-auto-message` (u00414F60, argc 1) - **summary:** (out) - return whether automatic message advance is enabled. - **grounding:** source=investigation, confidence=high @@ -895,10 +910,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 -### 0xae `u00415130` (u00415130, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0xb7 `u0041D0E0` (u0041D0E0, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -1031,14 +1042,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 -### 0x19b `u00414E80` (u00414E80, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x19c `u00414EC0` (u00414EC0, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x19d `u0041C680` (u0041C680, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index b5dd7e7..6faa4fa 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2104,3 +2104,31 @@ important than any one opcode's count; persistence-bound `0x1a2`, `0x1ad`, and ` Validation: all 211 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, the zero-warning Godot build, threaded self-test, and `git diff --check` are clean. + +### Slice A2b ADV skip lifecycle implemented; adjacent persistence gaps classified (2026-07-20) + +Native RE resolves script `savemesskip`/`loadmesskip` opcodes `0x19b`/`0x19c` as an ADV presentation +lifecycle pair. Suspend clears active fast-forward and the service gate while preserving the persistent +all-message Skip toggle. Resume re-enables the gate and recomputes active fast-forward from that toggle or +the live read-skip channel. The VM now models persistent and active skip state separately, and focused tests +prove both persistent-toggle preservation and read-skip-only reactivation. + +The same investigation classified the two adjacent one-call gaps without papering them over. Opcode `0x1ca` +is the already-known `message:ReadTextSkip` settings write and remains deferred with `0x1cb`, `RT.DAT`, and +the profile-owned read-state decision. Opcode `0xae` is normally inert but becomes the serialized save-stack +restoration rendezvous: it restores frame PCs and walks saved contexts while a flag set by the save-data +deserializer is active. Because the port has no numbered-save `ExecFrame` backend, an unconditional no-op +would be a false implementation; it remains grouped with `0x1ad`. + +The canonical opcode source and expanded 78-field EngineCtx source are regenerated. `/v2` names/comments +the three handlers and save deserializer, applies `adv_skip_service_enabled` and +`save_load_stack_restore_active`, and is saved. SC0000 is now **124/129 distinct opcodes handled (96.1%)** +and **16,041/16,257 instructions handled or safe-noop (98.7%)**. Its five remaining gaps total 216 +instructions: `0x1a2`×206, `0x1ad`×6, `0x1cb`×2, `0x1ca`×1, and `0xae`×1. + +**Next:** keep all five remaining gaps behind the unified profile/read-state/numbered-save ownership work. +Choose the next implementation slice from a different scene or runtime subsystem rather than claiming +normal-path no-ops for `0xae` or a transient-only `ReadTextSkip` setting. + +Validation: 213 engine tests pass; opcode and EngineCtx generators/tests/lints, SC0000 coverage, vm0 +RECOVER, the zero-warning Godot build, threaded self-test, and `git diff --check` are clean. diff --git a/engine/Age.Engine.Tests/HotspotInputTests.cs b/engine/Age.Engine.Tests/HotspotInputTests.cs index 892b0c7..3c10f96 100644 --- a/engine/Age.Engine.Tests/HotspotInputTests.cs +++ b/engine/Age.Engine.Tests/HotspotInputTests.cs @@ -302,6 +302,53 @@ public class HotspotInputTests Assert.Equal(new[] { true, false }, host.MessageSkipChanges); } + [Fact] + public void AdvSkipService_SuspendsAndRestoresPersistentMessageSkip() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var script = ScriptAssembler.Assemble(table, "MESSAGE_SKIP_LIFECYCLE", new List<(int, Operand[])> + { + (0x88, new[] { I(1) }), + (0x19b, Array.Empty()), + (0x19a, new[] { G(0x144) }), + (0x1c7, new[] { G(0x145) }), + (0x19c, Array.Empty()), + (0x1c7, new[] { G(0x146) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var host = new RecordingHost(); + var vm = new VirtualMachine(script, table, host); + + vm.Run(); + + Assert.Equal(1, vm.Globals.GetValueOrDefault(0x144)); + Assert.Equal(0, vm.Globals.GetValueOrDefault(0x145)); + Assert.Equal(1, vm.Globals.GetValueOrDefault(0x146)); + Assert.True(vm.MessageSkipEnabled); + Assert.Equal(new[] { true, false, true }, host.MessageSkipChanges); + } + + [Fact] + public void AdvSkipService_RestoreIncludesLiveReadSkipChannel() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var script = ScriptAssembler.Assemble(table, "READ_SKIP_LIFECYCLE", new List<(int, Operand[])> + { + (0x19b, Array.Empty()), + (0x19c, Array.Empty()), + (0x1c7, new[] { G(0x147) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var host = new RecordingHost { AdvReadSkip = true }; + var vm = new VirtualMachine(script, table, host); + + vm.Run(); + + Assert.Equal(1, vm.Globals.GetValueOrDefault(0x147)); + Assert.False(vm.MessageSkipEnabled); + Assert.Equal(new[] { false, true }, host.MessageSkipChanges); + } + [Fact] public void Sc0000MessageSkipButton_EnablesPersistentServiceState() { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index f67d072..4f45a0a 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -36,6 +36,7 @@ public sealed class VirtualMachine private long _autoMessageTime1Ms = 2000; private bool _autoVoicePending; private volatile bool _messageSkipEnabled; + private volatile bool _messageSkipServiceActive; private AdvTextStyle _advTextStyle = AdvTextStyle.Default; private readonly Dictionary _valueSwitchTargets = new(StringComparer.Ordinal); public long CallScriptDispatches { get; private set; } @@ -819,14 +820,25 @@ public sealed class VirtualMachine case "u0041B290": case "set-message-skip": // 0x88: persistent all-message fast-forward service state _messageSkipEnabled = Read(a[0]) != 0; - _host.SetMessageSkipActive(_messageSkipEnabled); + _messageSkipServiceActive = _messageSkipEnabled; + _host.SetMessageSkipActive(_messageSkipServiceActive); return pc + 1; case "u00414E50": // 0x19a: persistent state used by the SO001 active overlay Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1; + case "u00414E80": + case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation + _messageSkipServiceActive = false; + _host.SetMessageSkipActive(false); + return pc + 1; + case "u00414EC0": + case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry + _messageSkipServiceActive = _messageSkipEnabled || _host.IsAdvReadSkipActive; + _host.SetMessageSkipActive(_messageSkipServiceActive); + return pc + 1; case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward - // The native per-op tick continually re-arms the transient run-state bit while op 0x88's - // persistent flag is set. The host channel carries the physically held Ctrl/input source. - Write(a[0], _messageSkipEnabled || _host.IsMessageSkipActive ? 1 : 0); return pc + 1; + // The native per-op tick continually re-arms the transient run-state bit while the + // ADV service is enabled. The host channel also carries physical fast-forward input. + Write(a[0], _messageSkipServiceActive || _host.IsMessageSkipActive ? 1 : 0); return pc + 1; 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; diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 7561844..515d420 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -14,6 +14,7 @@ INFERRED: dict[int, dict] = { 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.'), + 0xae: dict(name='continue-save-load-stack-restore', category='control', noop=False, confidence='high', source='investigation', summary='() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.'), 0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(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.'), @@ -35,6 +36,8 @@ INFERRED: dict[int, dict] = { 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."), 0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', 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.'), + 0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."), + 0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'), 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.'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index 1711891..f04c18c 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -143,6 +143,11 @@ name = "cur_ctx_index" type = "uint" note = "current script-context index (curCtx); indexes 0x78-byte coroutine/frame records" [[field]] +offset = 0x53d24 +name = "save_load_stack_restore_active" +type = "int" +note = "set while a deserialized numbered-save script stack is being reconstructed; op 0xae restores saved frame PCs/contexts and clears it at the terminal saved context" +[[field]] offset = 0x53d28 name = "frame_codebase" type = "void*" @@ -173,6 +178,11 @@ name = "message_skip_display_enabled" type = "int" note = "persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay" [[field]] +offset = 0x55100 +name = "adv_skip_service_enabled" +type = "int" +note = "ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip" +[[field]] offset = 0x55104 name = "auto_message_enabled" type = "int" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index f16d77b..23f75cf 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1713,14 +1713,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00415130" -category = "unknown" -summary = "" +name = "continue-save-load-stack-restore" +category = "control" +summary = "() - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with FUN_0040f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack." [[opcode]] op = 0xb4 @@ -3293,14 +3293,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00414E80" -category = "unknown" -summary = "" +name = "suspend-adv-skip-service" +category = "input" +summary = "() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. Corpus comments call it `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation." [[opcode]] op = 0x19c @@ -3309,14 +3309,14 @@ argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00414EC0" -category = "unknown" -summary = "" +name = "resume-adv-skip-service" +category = "input" +summary = "() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x19c_resume_adv_skip_service@0x4165a0 sets ctx+0x55100, clears the click-cancel phase, and normally activates run-state bit 0x08000000 when ctx+0x550fc persistent all-message Skip or ctx+0x6dbd4 adv_read_skip_state is nonzero. Otherwise it clears ctx+0x13dc and the run-state bit. A separate startup fast-forward guard at ctx+0x6f86c suppresses reactivation while active. Corpus comments call it `loadmesskip`; ADV setup, branch, and CALLBACK_LOAD paths pair it with op 0x19b." [[opcode]] op = 0x19d