Correct ADV Hide Window restore behavior

This commit is contained in:
gamer147
2026-07-18 22:23:06 -04:00
parent d8ead3da6e
commit 56f8adfa2d
13 changed files with 177 additions and 35 deletions

View File

@@ -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`:

View File

@@ -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

View File

@@ -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.

View File

@@ -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) · `<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 |
| `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 |

View File

@@ -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<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}"))}");
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<Operand>()),
}, Array.Empty<string>());
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<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]
public void MouseCallback_UsesLivePointerAndButtonState()
{

View File

@@ -26,12 +26,15 @@ internal class RecordingHost : IHost
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
public readonly List<bool> MessageSkipChanges = new();
public readonly List<long> CursorResources = new();
public readonly List<bool> 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<bool> serviceInputCallback)
{

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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));

View File

@@ -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.'),

View File

@@ -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)

View File

@@ -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