Correct ADV Hide Window restore behavior
This commit is contained in:
@@ -1308,6 +1308,30 @@ pointer/button reads, cursor host forwarding, and real CUR decoding.
|
|||||||
Validation is engine 175/175, opcode/global generator tests and lints clean, zero-warning Godot build, and
|
Validation is engine 175/175, opcode/global generator tests and lints clean, zero-warning Godot build, and
|
||||||
threaded `SELFTEST OK`.
|
threaded `SELFTEST OK`.
|
||||||
|
|
||||||
|
**Manual-validation correction (2026-07-18).** Native `bit-set`/`bit-reset` operands are bit indices, not
|
||||||
|
literal masks: HIDEWIN sets index 1 at `0x13d`, tests mask `0x2` at `0x146`, and clears index 1 at `0x154`.
|
||||||
|
The old VM interpretation wrote mask `0x1`, so the right-button release edge could never reach the restore
|
||||||
|
path. `/v2` confirms the generic semantics in `op_0x135_handler@0x4296c0` and
|
||||||
|
`op_0x136_handler@0x429730`: `value |= 1 << index` and `value &= ~(1 << index)`, with indices 0..31 valid.
|
||||||
|
The raw op-`0x108` channel therefore supplies `0x1` for `VK_LBUTTON` and `0x2` for `VK_RBUTTON`, making a
|
||||||
|
right-button release one direct restore route. That is not the complete input model, however. Native
|
||||||
|
`input_poll_mouse_action_bits@0x460240` also maps the physical left/right buttons through configurable
|
||||||
|
logical actions (defaults 0/1) to callback indices 4/5, and HIDEWIN registers both indices to its restore
|
||||||
|
callback. A completed ordinary left click therefore restores the textbox as observed in the original game.
|
||||||
|
The raw left-button edge branch only supports moving an oversized retained display object while held; it is
|
||||||
|
not evidence that dungeon gameplay camera panning is enabled during an ordinary 800x600 VN scene.
|
||||||
|
|
||||||
|
The manual run also exposed that ADV text and the op-`0x72` wait indicator are currently Godot presentation
|
||||||
|
overlays rather than retained texture objects. The shipped handler correctly faded the textbox/chrome, but
|
||||||
|
those overlays ignored the op-`0x199` coroutine lifecycle and remained above the scene. The host now suspends
|
||||||
|
both when the first `0x199` saves/yields the active ADV page and restores them only when op `0x7c` restores
|
||||||
|
that saved page PC. The enclosing input wait remains parked throughout, so HIDEWIN continues to own mouse and
|
||||||
|
mapped action input instead of an overlay click accidentally advancing dialogue.
|
||||||
|
The Godot input adapter now queues the native primary-action callback while the ADV page is suspended and
|
||||||
|
does not release the enclosing dialogue wait; otherwise the restoring click would also advance the page.
|
||||||
|
The `/v2` bit and mouse-input helpers are annotated and saved. Validation: engine 177/177, opcode tests/lints
|
||||||
|
and vm0 RECOVER clean, zero-warning Godot build, and threaded `SELFTEST OK`.
|
||||||
|
|
||||||
### ADV retained text — ops `0x7a` / `0x204` and show-text publication (2026-07-10)
|
### ADV retained text — ops `0x7a` / `0x204` and show-text publication (2026-07-10)
|
||||||
|
|
||||||
The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`:
|
The SC0000 textbox uses two related native paths under the text manager at `ctx+0x14940`:
|
||||||
|
|||||||
@@ -82,6 +82,18 @@
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
## compute
|
||||||
|
|
||||||
|
### 0x135 `bit-set` (bit-set, argc 2)
|
||||||
|
- **summary:** (value)(bit_index) - set the indexed bit in the destination integer.
|
||||||
|
- **grounding:** source=investigation, confidence=high
|
||||||
|
- **evidence:** Ghidra /v2: op_0x135_handler@0x4296c0 fetches operand 2 as an unsigned bit index, rejects values >=32 through the native script-error path, fetches operand 1, and writes value | (1 << index). HIDEWIN.BIN sets index 1 at 0x13d and later tests mask 0x2 at 0x146.
|
||||||
|
|
||||||
|
### 0x136 `bit-reset` (bit-reset, argc 2)
|
||||||
|
- **summary:** (value)(bit_index) - clear the indexed bit in the destination integer.
|
||||||
|
- **grounding:** source=investigation, confidence=high
|
||||||
|
- **evidence:** Ghidra /v2: op_0x136_handler@0x429730 fetches operand 2 as an unsigned bit index, rejects values >=32 through the native script-error path, fetches operand 1, and writes value & ~(1 << index). HIDEWIN.BIN clears index 1 at 0x154 after testing mask 0x2.
|
||||||
|
|
||||||
## control
|
## control
|
||||||
|
|
||||||
### 0x3 `call-script` (call-script, argc 1)
|
### 0x3 `call-script` (call-script, argc 1)
|
||||||
@@ -432,8 +444,8 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
|
|
||||||
### 0xff `poll-joy-callback-input` (u00415A10, argc 0)
|
### 0xff `poll-joy-callback-input` (u00415A10, argc 0)
|
||||||
- **summary:** Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.
|
- **summary:** Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.
|
||||||
- **grounding:** source=investigation, confidence=med
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **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.
|
- **evidence:** Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets the scan index, and snapshots the current input selector. That poller combines configured keyboard, mouse-action, and joystick bits. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON to logical bit mouse_map[0]+4 (default index 4) and VK_RBUTTON to mouse_map[1]+4 (default index 5). It pairs with op 0x100.
|
||||||
|
|
||||||
### 0x100 `dispatch-joy-callbacks` (u00415A60, argc 0)
|
### 0x100 `dispatch-joy-callbacks` (u00415A60, argc 0)
|
||||||
- **summary:** Dispatch registered callbacks for the current or pending joy/input selection.
|
- **summary:** Dispatch registered callbacks for the current or pending joy/input selection.
|
||||||
@@ -448,7 +460,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
### 0x108 `get-mouse-button-state` (u00415E70, argc 1)
|
### 0x108 `get-mouse-button-state` (u00415E70, argc 1)
|
||||||
- **summary:** (out) - return the current mouse-button state bitmask.
|
- **summary:** (out) - return the current mouse-button state bitmask.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **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 uses bit 0x1 for left-click edge interaction and bit 0x2 for its close/restore gesture; HISTORY.BIN likewise tests individual bits for transitions.
|
- **evidence:** Ghidra /v2: op_0x108_get_mouse_button_state@0x428b60 fills a local through input_poll_raw_mouse_buttons@0x4602e0 and writes it to operand 1. The raw mapping is VK_LBUTTON -> 0x1 and VK_RBUTTON -> 0x2. This is distinct from op 0xff's logical action mask: the default mouse mapping also exposes left/right as callback indices 4/5, both registered by HIDEWIN to its close/restore callback.
|
||||||
|
|
||||||
### 0x109 `get-cursor-virtual` (u00415EC0, argc 2)
|
### 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.
|
- **summary:** (out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates.
|
||||||
@@ -842,14 +854,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|
||||||
### 0x135 `bit-set` (bit-set, argc 2)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=med
|
|
||||||
|
|
||||||
### 0x136 `bit-reset` (bit-reset, argc 2)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=med
|
|
||||||
|
|
||||||
### 0x137 `u0041F1C0` (u0041F1C0, argc 1)
|
### 0x137 `u0041F1C0` (u0041F1C0, argc 1)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|||||||
@@ -1620,3 +1620,20 @@ threaded `SELFTEST OK`.
|
|||||||
**Next:** manually validate x=772 hide/restore and edge cursors in a normal SC0000 run. The next development
|
**Next:** manually validate x=772 hide/restore and edge cursors in a normal SC0000 run. The next development
|
||||||
slice should be Read-message Skip's profile-wide ReadTextDB model; History can then reuse both that
|
slice should be Read-message Skip's profile-wide ReadTextDB model; History can then reuse both that
|
||||||
message-completion seam and the generic callback/input layer landed here.
|
message-completion seam and the generic callback/input layer landed here.
|
||||||
|
|
||||||
|
**Manual-validation correction (2026-07-18).** The textbox faded through the shipped retained-graphics path,
|
||||||
|
but the port's separate ADV text Label and wait-indicator TextureRect stayed visible. Op `0x199` now suspends
|
||||||
|
those two page overlays for the lifetime of the yielded ADV coroutine, and op `0x7c` restores them with the
|
||||||
|
saved page. The failed restore interaction was a shared opcode error: `bit-set`/`bit-reset` take a bit index,
|
||||||
|
not a literal mask, so HIDEWIN's set-index-1/test-`0x2` raw right-button release never matched. Both the C# VM
|
||||||
|
and the Python oracle now use indexed-bit semantics. Follow-up original-game validation corrected an
|
||||||
|
over-narrow input conclusion: AGE separately maps physical left/right mouse buttons into logical callback
|
||||||
|
indices 4/5, and HIDEWIN registers those actions to restore as well. Godot now queues the primary action for
|
||||||
|
a left click while the ADV page is suspended and keeps that click from advancing the enclosing dialogue
|
||||||
|
wait. The real-script regression restores through the left-action callback without hitting the step limit
|
||||||
|
and records the overlay suspend/restore sequence.
|
||||||
|
Validation: engine 177/177, opcode tests/lints and vm0 RECOVER clean, zero-warning Godot build, and threaded
|
||||||
|
`SELFTEST OK`; `/v2` bit handlers are annotated and saved.
|
||||||
|
|
||||||
|
**Next:** manually recheck x=772 with the original left-click restore gesture, then continue with the
|
||||||
|
profile-wide ReadTextDB slice if the visual lifecycle now matches.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
|
|||||||
|
|
||||||
| Tool | Purpose | Run | Reads → Writes |
|
| Tool | Purpose | Run | Reads → Writes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `vm0.py` | Headless Python bytecode VM (Phase A0 execution-model prototype; reuses `sys4load`). | `--test` (RECOVER unit test) · `--sweep [N]` (oracle coverage) · `--scene NAME` · `--settex NAME` (set-texture resId trace + exec trace) · `<file.BIN>` | corpus → stdout; `build/vm0-trace.json`; `build/settex-<NAME>.json` |
|
| `vm0.py` | Headless Python bytecode VM (Phase A0 execution-model prototype; reuses `sys4load`; native `bit-set`/`bit-reset` operands are bounded bit indices). | `--test` (RECOVER unit test) · `--sweep [N]` (oracle coverage) · `--scene NAME` · `--settex NAME` (set-texture resId trace + exec trace) · `<file.BIN>` | corpus → stdout; `build/vm0-trace.json`; `build/settex-<NAME>.json` |
|
||||||
| `scene_opcode_coverage.py` | Per-scene opcode completeness gauge: histograms a scene's static opcodes and classifies each **impl** / **safe-noop** / **GAP** (effectful op the VM silently stubs). Implemented set parsed from `VirtualMachine.cs` `case` arms; metadata from `opcodes.json`. Surfaces the concrete rendering/feature holes so a half-drawn scene reads as "N ops still stubbed", not "mystery". | `scene_opcode_coverage.py [SCENE …]` (default SC0000) | corpus, `build/opcodes.json`, `engine/…/VirtualMachine.cs`, `build/callscript-names.json` → ⚙ `build/scene-opcode-coverage/<SCENE>.md` + stdout |
|
| `scene_opcode_coverage.py` | Per-scene opcode completeness gauge: histograms a scene's static opcodes and classifies each **impl** / **safe-noop** / **GAP** (effectful op the VM silently stubs). Implemented set parsed from `VirtualMachine.cs` `case` arms; metadata from `opcodes.json`. Surfaces the concrete rendering/feature holes so a half-drawn scene reads as "N ops still stubbed", not "mystery". | `scene_opcode_coverage.py [SCENE …]` (default SC0000) | corpus, `build/opcodes.json`, `engine/…/VirtualMachine.cs`, `build/callscript-names.json` → ⚙ `build/scene-opcode-coverage/<SCENE>.md` + stdout |
|
||||||
| `correlate_scope.py` | Align the VM's `set-texture(resId)` trace with the game's Frida load order → tag each load's DATA2 package, flag package transitions, dump the significant ops in each transition span (the **scope selector** hunt). | `correlate_scope.py <SCENE>` | `build/settex-<SCENE>.json` + `build/frida-load-order-result.json` + index → stdout |
|
| `correlate_scope.py` | Align the VM's `set-texture(resId)` trace with the game's Frida load order → tag each load's DATA2 package, flag package transitions, dump the significant ops in each transition span (the **scope selector** hunt). | `correlate_scope.py <SCENE>` | `build/settex-<SCENE>.json` + `build/frida-load-order-result.json` + index → stdout |
|
||||||
| `diff_optrace.py` | **Differential offset-path oracle** (`docs/engine-re.md`): diff the engine's executed offset path (`trace_engine_ops.py`) against the VM's (`Age.Cli trace --trace-json`) → first divergence = the mis-modeled branch/op/state, with opcode + ±3 ops of context. Identifies the scene's codebase by longest-common-prefix; filters the VM trace to argc≥1 (operand-capture parity). Pure core unit-tested (`test_diff_optrace.py`). | `py -3.11 -X utf8 tools/diff_optrace.py SC0000 [--full]` | `build/engine-optrace.jsonl` + `build/vm-optrace.json` + disasm → stdout |
|
| `diff_optrace.py` | **Differential offset-path oracle** (`docs/engine-re.md`): diff the engine's executed offset path (`trace_engine_ops.py`) against the VM's (`Age.Cli trace --trace-json`) → first divergence = the mis-modeled branch/op/state, with opcode + ±3 ops of context. Identifies the scene's codebase by longest-common-prefix; filters the VM trace to argc≥1 (operand-capture parity). Pure core unit-tested (`test_diff_optrace.py`). | `py -3.11 -X utf8 tools/diff_optrace.py SC0000 [--full]` | `build/engine-optrace.jsonl` + `build/vm-optrace.json` + disasm → stdout |
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ public class HotspotInputTests
|
|||||||
public VirtualMachine Vm = null!;
|
public VirtualMachine Vm = null!;
|
||||||
private long _now;
|
private long _now;
|
||||||
public int HideLoopSleeps;
|
public int HideLoopSleeps;
|
||||||
|
public bool HideReturned;
|
||||||
public override long InputClockMilliseconds => _now;
|
public override long InputClockMilliseconds => _now;
|
||||||
|
|
||||||
public override void Sleep(long duration)
|
public override void Sleep(long duration)
|
||||||
@@ -104,7 +105,17 @@ public class HotspotInputTests
|
|||||||
if (duration <= 1 && ++HideLoopSleeps == 2)
|
if (duration <= 1 && ++HideLoopSleeps == 2)
|
||||||
{
|
{
|
||||||
Vm.UpdateMouseButtonState(0x1, false); // release the x=772 activation click
|
Vm.UpdateMouseButtonState(0x1, false); // release the x=772 activation click
|
||||||
Vm.UpdateMouseButtonState(0x2, true); // native right-click close/restore gesture
|
Vm.QueueInputCallback(10); // common release callback arms HIDEWIN input
|
||||||
|
}
|
||||||
|
else if (duration <= 1 && HideLoopSleeps == 3)
|
||||||
|
{
|
||||||
|
Vm.UpdateMouseButtonState(0x1, true); // next primary click is generic action index 4
|
||||||
|
Vm.QueueInputCallback(4);
|
||||||
|
}
|
||||||
|
else if (duration <= 1 && HideLoopSleeps == 4)
|
||||||
|
{
|
||||||
|
Vm.UpdateMouseButtonState(0x1, false);
|
||||||
|
Vm.QueueInputCallback(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +126,7 @@ public class HotspotInputTests
|
|||||||
Vm.UpdateMouseButtonState(0x1, true);
|
Vm.UpdateMouseButtonState(0x1, true);
|
||||||
Assert.True(Vm.TryActivatePointer(772, 572));
|
Assert.True(Vm.TryActivatePointer(772, 572));
|
||||||
while (serviceInputCallback()) { }
|
while (serviceInputCallback()) { }
|
||||||
|
HideReturned = true;
|
||||||
throw new StopAtFirstWaitException();
|
throw new StopAtFirstWaitException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +353,12 @@ public class HotspotInputTests
|
|||||||
|
|
||||||
Assert.Throws<StopAtFirstWaitException>(() => vm.Run());
|
Assert.Throws<StopAtFirstWaitException>(() => vm.Run());
|
||||||
|
|
||||||
Assert.True(host.HideLoopSleeps >= 2,
|
Assert.True(host.HideLoopSleeps >= 4,
|
||||||
$"hide sleeps={host.HideLoopSleeps}; halt={vm.HaltReason}; frames={string.Join(',', trace.Events.Where(e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter).Select(e => e.Name))}; tail={string.Join(',', trace.Events.Where(e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.Step).TakeLast(30).Select(e => $"{e.Ins!.Offset:x}:{e.Opcode:x}"))}");
|
$"hide sleeps={host.HideLoopSleeps}; halt={vm.HaltReason}; frames={string.Join(',', trace.Events.Where(e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter).Select(e => e.Name))}; tail={string.Join(',', trace.Events.Where(e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.Step).TakeLast(30).Select(e => $"{e.Ins!.Offset:x}:{e.Opcode:x}"))}");
|
||||||
Assert.Contains(trace.Events, e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
Assert.Contains(trace.Events, e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||||
&& e.Name?.EndsWith("HIDEWIN.BIN", StringComparison.OrdinalIgnoreCase) == true);
|
&& e.Name?.EndsWith("HIDEWIN.BIN", StringComparison.OrdinalIgnoreCase) == true);
|
||||||
|
Assert.True(host.HideReturned);
|
||||||
|
Assert.Equal(new[] { true, false }, host.AdvPagePresentationSuspended);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -383,7 +397,8 @@ public class HotspotInputTests
|
|||||||
(0x55, new[] { G(0x162), I(1) }),
|
(0x55, new[] { G(0x162), I(1) }),
|
||||||
(0x7c, Array.Empty<Operand>()),
|
(0x7c, Array.Empty<Operand>()),
|
||||||
}, Array.Empty<string>());
|
}, Array.Empty<string>());
|
||||||
var vm = new VirtualMachine(script, table, new RecordingHost());
|
var host = new RecordingHost();
|
||||||
|
var vm = new VirtualMachine(script, table, host);
|
||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
@@ -391,9 +406,45 @@ public class HotspotInputTests
|
|||||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x161));
|
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x161));
|
||||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x162));
|
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x162));
|
||||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x163));
|
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x163));
|
||||||
|
Assert.Equal(new[] { true, false }, host.AdvPagePresentationSuspended);
|
||||||
Assert.Equal("exit", vm.HaltReason);
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BitSetAndReset_UseBitIndicesRatherThanLiteralMasks()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
var script = ScriptAssembler.Assemble(table, "BIT_INDEX", new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x135, new[] { G(0x168), I(1) }),
|
||||||
|
(0x135, new[] { G(0x168), I(4) }),
|
||||||
|
(0x136, new[] { G(0x168), I(1) }),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var vm = new VirtualMachine(script, table, new RecordingHost());
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal(0x10, vm.Globals.GetValueOrDefault(0x168));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BitSet_RejectsNativeOutOfRangeIndex()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
var script = ScriptAssembler.Assemble(table, "BIT_RANGE", new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x135, new[] { G(0x169), I(32) }),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var vm = new VirtualMachine(script, table, new RecordingHost());
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal("bit-index-out-of-range:32", vm.HaltReason);
|
||||||
|
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x169));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MouseCallback_UsesLivePointerAndButtonState()
|
public void MouseCallback_UsesLivePointerAndButtonState()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,12 +26,15 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
||||||
public readonly List<bool> MessageSkipChanges = new();
|
public readonly List<bool> MessageSkipChanges = new();
|
||||||
public readonly List<long> CursorResources = new();
|
public readonly List<long> CursorResources = new();
|
||||||
|
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||||
public int CursorClearCount;
|
public int CursorClearCount;
|
||||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||||
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
=> SurfaceStrings.Add((surfaceSlot, x, y, text));
|
||||||
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config);
|
||||||
|
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||||
|
=> AdvPagePresentationSuspended.Add(suspended);
|
||||||
public void WaitForInput() => Waits++;
|
public void WaitForInput() => Waits++;
|
||||||
public virtual void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
|
public virtual void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public interface IHost
|
|||||||
void SetAdvTextCursor(int layoutSlot, int x, int y) { }
|
void SetAdvTextCursor(int layoutSlot, int x, int y) { }
|
||||||
void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { }
|
void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { }
|
||||||
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) { }
|
||||||
|
// Op 0x199 temporarily yields the active ADV page into its registered hide-window coroutine.
|
||||||
|
// The retained scene continues to render, but the text layout and its wait marker are suspended
|
||||||
|
// until op 0x7c restores the saved page PC.
|
||||||
|
void SetAdvPagePresentationSuspended(bool suspended) { }
|
||||||
void WaitForInput();
|
void WaitForInput();
|
||||||
void WaitForInput(int layoutSlot) => WaitForInput();
|
void WaitForInput(int layoutSlot) => WaitForInput();
|
||||||
// Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains
|
// Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains
|
||||||
|
|||||||
@@ -330,8 +330,18 @@ public sealed class VirtualMachine
|
|||||||
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1;
|
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1;
|
||||||
case "lookup-array-2d":
|
case "lookup-array-2d":
|
||||||
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2]) * Read(a[3]) + Read(a[4])); return pc + 1;
|
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2]) * Read(a[3]) + Read(a[4])); return pc + 1;
|
||||||
case "bit-set": Write(a[0], Read(a[0]) | Read(a[1])); return pc + 1;
|
case "bit-set":
|
||||||
case "bit-reset": Write(a[0], Read(a[0]) & ~Read(a[1])); return pc + 1;
|
{
|
||||||
|
long bit = Read(a[1]);
|
||||||
|
if ((ulong)bit >= 32) { HaltReason ??= $"bit-index-out-of-range:{bit}"; return HALT; }
|
||||||
|
Write(a[0], Read(a[0]) | (1L << (int)bit)); return pc + 1;
|
||||||
|
}
|
||||||
|
case "bit-reset":
|
||||||
|
{
|
||||||
|
long bit = Read(a[1]);
|
||||||
|
if ((ulong)bit >= 32) { HaltReason ??= $"bit-index-out-of-range:{bit}"; return HALT; }
|
||||||
|
Write(a[0], Read(a[0]) & ~(1L << (int)bit)); return pc + 1;
|
||||||
|
}
|
||||||
case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1;
|
case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1;
|
||||||
case "copy-to-global": Write(a[0], Read(a[1])); return pc + 1;
|
case "copy-to-global": Write(a[0], Read(a[1])); return pc + 1;
|
||||||
case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
|
case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
|
||||||
@@ -357,6 +367,7 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
_cur.CoroutineResumePc = pc + 1;
|
_cur.CoroutineResumePc = pc + 1;
|
||||||
_cur.CoroutineYieldActive = true;
|
_cur.CoroutineYieldActive = true;
|
||||||
|
_host.SetAdvPagePresentationSuspended(true);
|
||||||
targetOffset = _cur.CoroutineYieldHandlerA;
|
targetOffset = _cur.CoroutineYieldHandlerA;
|
||||||
}
|
}
|
||||||
else targetOffset = _cur.CoroutineYieldHandlerB;
|
else targetOffset = _cur.CoroutineYieldHandlerB;
|
||||||
@@ -371,6 +382,7 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
_cur.CoroutineResumePc = null;
|
_cur.CoroutineResumePc = null;
|
||||||
_cur.CoroutineYieldActive = false;
|
_cur.CoroutineYieldActive = false;
|
||||||
|
_host.SetAdvPagePresentationSuspended(false);
|
||||||
return resumePc;
|
return resumePc;
|
||||||
}
|
}
|
||||||
return pc + 1; // cold bounded scene-entry path
|
return pc + 1; // cold bounded scene-entry path
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private AudioPayload? _queuedSkippedVoice;
|
private AudioPayload? _queuedSkippedVoice;
|
||||||
private int _activeWaitLayout;
|
private int _activeWaitLayout;
|
||||||
private long _waitIndicatorStartedMs;
|
private long _waitIndicatorStartedMs;
|
||||||
|
private volatile bool _advPagePresentationSuspended;
|
||||||
private GfxState? _foregroundGfx;
|
private GfxState? _foregroundGfx;
|
||||||
public volatile bool IsWaiting;
|
public volatile bool IsWaiting;
|
||||||
public volatile bool IsTransitionWaiting;
|
public volatile bool IsTransitionWaiting;
|
||||||
@@ -131,7 +132,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator()
|
||||||
{
|
{
|
||||||
if (!IsWaiting) return null;
|
if (!IsWaiting || _advPagePresentationSuspended) return null;
|
||||||
AdvWaitIndicatorConfig config;
|
AdvWaitIndicatorConfig config;
|
||||||
long resourceId;
|
long resourceId;
|
||||||
lock (_textLock)
|
lock (_textLock)
|
||||||
@@ -250,6 +251,14 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void WakeInputCallbackService() => _inputCallbackSignal.Set();
|
public void WakeInputCallbackService() => _inputCallbackSignal.Set();
|
||||||
|
|
||||||
|
public bool IsAdvPagePresentationSuspended => _advPagePresentationSuspended;
|
||||||
|
|
||||||
|
public void SetAdvPagePresentationSuspended(bool suspended)
|
||||||
|
{
|
||||||
|
_advPagePresentationSuspended = suspended;
|
||||||
|
_timeline?.State(suspended ? "adv-page-suspended" : "adv-page-restored", new());
|
||||||
|
}
|
||||||
|
|
||||||
public void InputCallbackCompleted(GfxState gfx)
|
public void InputCallbackCompleted(GfxState gfx)
|
||||||
=> Interlocked.Exchange(ref _presentRequested, 1);
|
=> Interlocked.Exchange(ref _presentRequested, 1);
|
||||||
|
|
||||||
|
|||||||
@@ -350,15 +350,23 @@ public partial class Main : Godot.Control
|
|||||||
&& (mb.ButtonIndex == MouseButton.Left || mb.ButtonIndex == MouseButton.Right))
|
&& (mb.ButtonIndex == MouseButton.Left || mb.ButtonIndex == MouseButton.Right))
|
||||||
{
|
{
|
||||||
var p = ToNativeScreen(mb.Position);
|
var p = ToNativeScreen(mb.Position);
|
||||||
|
bool advPageSuspended = _host.IsAdvPagePresentationSuspended;
|
||||||
_vm.UpdatePointer(p.X, p.Y);
|
_vm.UpdatePointer(p.X, p.Y);
|
||||||
int nativeButtonBit = mb.ButtonIndex == MouseButton.Left ? 0x1 : 0x2;
|
int nativeButtonBit = mb.ButtonIndex == MouseButton.Left ? 0x1 : 0x2;
|
||||||
_vm.UpdateMouseButtonState(nativeButtonBit, mb.Pressed);
|
_vm.UpdateMouseButtonState(nativeButtonBit, mb.Pressed);
|
||||||
|
// AGE exposes the physical left button twice: raw mask 0x1 for the timed mouse callback,
|
||||||
|
// and the configured primary action (default input callback index 4). During HIDEWIN the
|
||||||
|
// activation click's release arms the script; the next completed left click restores it.
|
||||||
|
if (mb.ButtonIndex == MouseButton.Left && advPageSuspended)
|
||||||
|
_vm.QueueInputCallback(mb.Pressed ? 4 : 10);
|
||||||
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && _vm.TryActivatePointer(p.X, p.Y))
|
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && _vm.TryActivatePointer(p.X, p.Y))
|
||||||
{
|
{
|
||||||
GetViewport().SetInputAsHandled();
|
GetViewport().SetInputAsHandled();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed) _host.SignalInput();
|
// A left click owned by the yielded page must return through HIDEWIN's callback/coroutine
|
||||||
|
// path. Releasing the enclosing ADV wait here would also advance the restored dialogue page.
|
||||||
|
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && !advPageSuspended) _host.SignalInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UpdateAgeInputCallback(e, "ui_down", 0);
|
UpdateAgeInputCallback(e, "ui_down", 0);
|
||||||
@@ -515,6 +523,8 @@ public partial class Main : Godot.Control
|
|||||||
|
|
||||||
private void UpdateAdvTextPresentation()
|
private void UpdateAdvTextPresentation()
|
||||||
{
|
{
|
||||||
|
_text.Visible = !_host.IsAdvPagePresentationSuspended;
|
||||||
|
if (!_text.Visible) return;
|
||||||
var t = _host.SnapshotAdvText();
|
var t = _host.SnapshotAdvText();
|
||||||
_text.Position = new Vector2(t.X, 430 + t.Y);
|
_text.Position = new Vector2(t.X, 430 + t.Y);
|
||||||
_text.Size = new Vector2(System.Math.Max(1, 720 - t.X), System.Math.Max(1, 147 - t.Y));
|
_text.Size = new Vector2(System.Math.Max(1, 720 - t.X), System.Math.Max(1, 147 - t.Y));
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ INFERRED: dict[int, dict] = {
|
|||||||
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.'),
|
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.'),
|
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.'),
|
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.'),
|
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', 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.'),
|
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."),
|
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.'),
|
0x108: dict(name='get-mouse-button-state', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current mouse-button state bitmask.'),
|
||||||
|
|||||||
12
tools/vm0.py
12
tools/vm0.py
@@ -177,9 +177,17 @@ class VM:
|
|||||||
addr = self.base_addr(a[1]) + self.read(a[2]) * self.read(a[3]) + self.read(a[4])
|
addr = self.base_addr(a[1]) + self.read(a[2]) * self.read(a[3]) + self.read(a[4])
|
||||||
self.lookup_store(a[0], addr); return pc + 1
|
self.lookup_store(a[0], addr); return pc + 1
|
||||||
if lbl == "bit-set":
|
if lbl == "bit-set":
|
||||||
self.write(a[0], self.read(a[0]) | self.read(a[1])); return pc + 1
|
bit = self.read(a[1])
|
||||||
|
if not 0 <= bit < 32:
|
||||||
|
self.halt_reason = f"bit-index-out-of-range:{bit}"
|
||||||
|
return None
|
||||||
|
self.write(a[0], self.read(a[0]) | (1 << bit)); return pc + 1
|
||||||
if lbl == "bit-reset":
|
if lbl == "bit-reset":
|
||||||
self.write(a[0], self.read(a[0]) & ~self.read(a[1])); return pc + 1
|
bit = self.read(a[1])
|
||||||
|
if not 0 <= bit < 32:
|
||||||
|
self.halt_reason = f"bit-index-out-of-range:{bit}"
|
||||||
|
return None
|
||||||
|
self.write(a[0], self.read(a[0]) & ~(1 << bit)); return pc + 1
|
||||||
if lbl == "check-bit": # p1 = (p2 >> p3) & 1
|
if lbl == "check-bit": # p1 = (p2 >> p3) & 1
|
||||||
self.write(a[0], (self.read(a[1]) >> (self.read(a[2]) & 31)) & 1); return pc + 1
|
self.write(a[0], (self.read(a[1]) >> (self.read(a[2]) & 31)) & 1); return pc + 1
|
||||||
if lbl == "copy-to-global": # best-effort: p1 = p2 (single cell)
|
if lbl == "copy-to-global": # best-effort: p1 = p2 (single cell)
|
||||||
|
|||||||
@@ -2268,9 +2268,9 @@ category = "input"
|
|||||||
summary = "Poll the current joy/input callback bitmask and initialize the per-dispatch scan state."
|
summary = "Poll the current joy/input callback bitmask and initialize the per-dispatch scan state."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "med"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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."
|
evidence = "Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets the scan index, and snapshots the current input selector. That poller combines configured keyboard, mouse-action, and joystick bits. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON to logical bit mouse_map[0]+4 (default index 4) and VK_RBUTTON to mouse_map[1]+4 (default index 5). It pairs with op 0x100."
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x100
|
op = 0x100
|
||||||
@@ -2344,7 +2344,7 @@ noop_headless = false
|
|||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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 uses bit 0x1 for left-click edge interaction and bit 0x2 for its close/restore gesture; HISTORY.BIN likewise tests individual bits for transitions."
|
evidence = "Ghidra /v2: op_0x108_get_mouse_button_state@0x428b60 fills a local through input_poll_raw_mouse_buttons@0x4602e0 and writes it to operand 1. The raw mapping is VK_LBUTTON -> 0x1 and VK_RBUTTON -> 0x2. This is distinct from op 0xff's logical action mask: the default mouse mapping also exposes left/right as callback indices 4/5, both registered by HIDEWIN to its close/restore callback."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -2737,13 +2737,13 @@ abi_source = "kelebek+decode-validated"
|
|||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
category = "unknown"
|
category = "compute"
|
||||||
summary = ""
|
summary = "(value)(bit_index) - set the indexed bit in the destination integer."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "med"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
evidence = "Ghidra /v2: op_0x135_handler@0x4296c0 fetches operand 2 as an unsigned bit index, rejects values >=32 through the native script-error path, fetches operand 1, and writes value | (1 << index). HIDEWIN.BIN sets index 1 at 0x13d and later tests mask 0x2 at 0x146."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -2763,13 +2763,13 @@ abi_source = "kelebek+decode-validated"
|
|||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "bit-reset"
|
name = "bit-reset"
|
||||||
category = "unknown"
|
category = "compute"
|
||||||
summary = ""
|
summary = "(value)(bit_index) - clear the indexed bit in the destination integer."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "med"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
evidence = "Ghidra /v2: op_0x136_handler@0x429730 fetches operand 2 as an unsigned bit index, rejects values >=32 through the native script-error path, fetches operand 1, and writes value & ~(1 << index). HIDEWIN.BIN clears index 1 at 0x154 after testing mask 0x2."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user