diff --git a/docs/engine-re.md b/docs/engine-re.md index ae54e79..f6076ab 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 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) 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 89e04eb..eac92ed 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -82,6 +82,18 @@ - **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. +## 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 ### 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) - **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. +- **grounding:** source=investigation, confidence=high +- **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) - **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) - **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 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) - **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:** — - **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) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 02d5c8b..1316f5c 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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 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. + +**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. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 75a97f7..2da7d98 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -61,7 +61,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * | 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) · `` | corpus → stdout; `build/vm0-trace.json`; `build/settex-.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) · `` | corpus → stdout; `build/vm0-trace.json`; `build/settex-.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/.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 ` | `build/settex-.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 --git a/engine/Age.Engine.Tests/HotspotInputTests.cs b/engine/Age.Engine.Tests/HotspotInputTests.cs index efce646..892b0c7 100644 --- a/engine/Age.Engine.Tests/HotspotInputTests.cs +++ b/engine/Age.Engine.Tests/HotspotInputTests.cs @@ -95,6 +95,7 @@ public class HotspotInputTests public VirtualMachine Vm = null!; private long _now; public int HideLoopSleeps; + public bool HideReturned; public override long InputClockMilliseconds => _now; public override void Sleep(long duration) @@ -104,7 +105,17 @@ public class HotspotInputTests if (duration <= 1 && ++HideLoopSleeps == 2) { 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); Assert.True(Vm.TryActivatePointer(772, 572)); while (serviceInputCallback()) { } + HideReturned = true; throw new StopAtFirstWaitException(); } } @@ -341,10 +353,12 @@ public class HotspotInputTests Assert.Throws(() => 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}"))}"); Assert.Contains(trace.Events, e => e.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter && e.Name?.EndsWith("HIDEWIN.BIN", StringComparison.OrdinalIgnoreCase) == true); + Assert.True(host.HideReturned); + Assert.Equal(new[] { true, false }, host.AdvPagePresentationSuspended); } [Fact] @@ -383,7 +397,8 @@ public class HotspotInputTests (0x55, new[] { G(0x162), I(1) }), (0x7c, Array.Empty()), }, Array.Empty()); - var vm = new VirtualMachine(script, table, new RecordingHost()); + var host = new RecordingHost(); + var vm = new VirtualMachine(script, table, host); vm.Run(); @@ -391,9 +406,45 @@ public class HotspotInputTests Assert.Equal(1, vm.Globals.GetValueOrDefault(0x161)); Assert.Equal(1, vm.Globals.GetValueOrDefault(0x162)); Assert.Equal(1, vm.Globals.GetValueOrDefault(0x163)); + Assert.Equal(new[] { true, false }, host.AdvPagePresentationSuspended); 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()), + }, Array.Empty()); + 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()), + }, Array.Empty()); + 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] public void MouseCallback_UsesLivePointerAndButtonState() { diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index 43fb9f9..2ac44cc 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -26,12 +26,15 @@ internal class RecordingHost : IHost public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new(); public readonly List MessageSkipChanges = new(); public readonly List CursorResources = new(); + public readonly List AdvPagePresentationSuspended = new(); public int CursorClearCount; 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 DrawStringToSurface(int surfaceSlot, int x, int y, string text) => SurfaceStrings.Add((surfaceSlot, x, y, text)); public void ConfigureAdvWaitIndicator(AdvWaitIndicatorConfig config) => WaitIndicators.Add(config); + public void SetAdvPagePresentationSuspended(bool suspended) + => AdvPagePresentationSuspended.Add(suspended); public void WaitForInput() => Waits++; public virtual void WaitForInput(int layoutSlot, Func serviceInputCallback) { diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index a71f2b8..a05efa1 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -18,6 +18,10 @@ public interface IHost void SetAdvTextCursor(int layoutSlot, int x, int y) { } void DrawStringToSurface(int surfaceSlot, int x, int y, string text) { } 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(int layoutSlot) => WaitForInput(); // Interactive hosts service script callbacks on the VM thread while the enclosing ADV page remains diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 0095a60..2426011 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -330,8 +330,18 @@ public sealed class VirtualMachine LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1; case "lookup-array-2d": 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-reset": Write(a[0], Read(a[0]) & ~Read(a[1])); return pc + 1; + case "bit-set": + { + 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 "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); @@ -357,6 +367,7 @@ public sealed class VirtualMachine { _cur.CoroutineResumePc = pc + 1; _cur.CoroutineYieldActive = true; + _host.SetAdvPagePresentationSuspended(true); targetOffset = _cur.CoroutineYieldHandlerA; } else targetOffset = _cur.CoroutineYieldHandlerB; @@ -371,6 +382,7 @@ public sealed class VirtualMachine { _cur.CoroutineResumePc = null; _cur.CoroutineYieldActive = false; + _host.SetAdvPagePresentationSuspended(false); return resumePc; } return pc + 1; // cold bounded scene-entry path diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index c0e03d2..d0de92d 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -41,6 +41,7 @@ public sealed class GodotAdvHost : IHost private AudioPayload? _queuedSkippedVoice; private int _activeWaitLayout; private long _waitIndicatorStartedMs; + private volatile bool _advPagePresentationSuspended; private GfxState? _foregroundGfx; public volatile bool IsWaiting; public volatile bool IsTransitionWaiting; @@ -131,7 +132,7 @@ public sealed class GodotAdvHost : IHost public AdvWaitIndicatorSnapshot? SnapshotAdvWaitIndicator() { - if (!IsWaiting) return null; + if (!IsWaiting || _advPagePresentationSuspended) return null; AdvWaitIndicatorConfig config; long resourceId; lock (_textLock) @@ -250,6 +251,14 @@ public sealed class GodotAdvHost : IHost 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) => Interlocked.Exchange(ref _presentRequested, 1); diff --git a/godot/Main.cs b/godot/Main.cs index 9cffb08..542dfb9 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -350,15 +350,23 @@ public partial class Main : Godot.Control && (mb.ButtonIndex == MouseButton.Left || mb.ButtonIndex == MouseButton.Right)) { var p = ToNativeScreen(mb.Position); + bool advPageSuspended = _host.IsAdvPagePresentationSuspended; _vm.UpdatePointer(p.X, p.Y); int nativeButtonBit = mb.ButtonIndex == MouseButton.Left ? 0x1 : 0x2; _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)) { GetViewport().SetInputAsHandled(); 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; } UpdateAgeInputCallback(e, "ui_down", 0); @@ -515,6 +523,8 @@ public partial class Main : Godot.Control private void UpdateAdvTextPresentation() { + _text.Visible = !_host.IsAdvPagePresentationSuspended; + if (!_text.Visible) return; var t = _host.SnapshotAdvText(); _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)); diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index b6c8fc6..d11ddc8 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -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.'), 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.'), + 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.'), 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.'), diff --git a/tools/vm0.py b/tools/vm0.py index cbd0cfd..781582a 100644 --- a/tools/vm0.py +++ b/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]) self.lookup_store(a[0], addr); return pc + 1 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": - 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 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) diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 30578dd..f2ceba5 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -2268,9 +2268,9 @@ category = "input" summary = "Poll the current joy/input callback bitmask and initialize the per-dispatch scan state." noop_headless = false source = "investigation" -confidence = "med" +confidence = "high" 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]] op = 0x100 @@ -2344,7 +2344,7 @@ noop_headless = false source = "investigation" confidence = "high" 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]] i = 1 @@ -2737,13 +2737,13 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "bit-set" -category = "unknown" -summary = "" +category = "compute" +summary = "(value)(bit_index) - set the indexed bit in the destination integer." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" 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]] i = 1 @@ -2763,13 +2763,13 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "bit-reset" -category = "unknown" -summary = "" +category = "compute" +summary = "(value)(bit_index) - clear the indexed bit in the destination integer." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" 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]] i = 1