Implement ADV History interaction
This commit is contained in:
@@ -1350,9 +1350,24 @@ source rows therefore retain five independent speaker names, and compositor sour
|
|||||||
each one into its bound object. A real-script regression retains SC0000 page one, runs unmodified
|
each one into its bound object. A real-script regression retains SC0000 page one, runs unmodified
|
||||||
`HISTORY.BIN`, observes a non-empty rendered row, and reaches its `(0,60000)` presentation call.
|
`HISTORY.BIN`, observes a non-empty rendered row, and reaches its `(0,60000)` presentation call.
|
||||||
|
|
||||||
History's remaining work is interaction fidelity: `0x12e` rectangle hover/hit selection, stored voice replay
|
The fourth port slice implements History's basic pointer interaction and clean return. Op `0x12e` treats its
|
||||||
through `0x1bd`, and the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler. None changes backlog ownership or
|
addressable operands as arrays in their actual VM domain (HISTORY's are local integers), scans from
|
||||||
requires choosing a save/profile backend.
|
`incoming_index+1`, and tests inclusive intersection. Rectangle fields are `[left,right,top,bottom]`; each
|
||||||
|
candidate's x/y offset is subtracted from the pointer before comparison with the reference rectangle.
|
||||||
|
HISTORY's decoded arrays contain fourteen candidates: scrollbar/control regions, bottom-right close region
|
||||||
|
8, and five 650x130 text rows at indices 9..13. Moving the pointer updates the selected index and executes the
|
||||||
|
script's existing redraw, while left-button release on region 8 sets the script's exit state and runs its
|
||||||
|
normal surface/layout/recording cleanup.
|
||||||
|
|
||||||
|
The host input bridge now distinguishes an enclosing ADV page wait from a script-owned timed raw-input loop.
|
||||||
|
Registration through op `0xcc` marks the current frame as the raw-input owner until that frame returns;
|
||||||
|
Godot routes physical button state and configured callback indices to it without signaling the parked ADV
|
||||||
|
page. This is frame-scoped callback state, not a HISTORY name/offset special case, and also matches the
|
||||||
|
existing HIDEWIN scheduler family. A real regression activates x=684 in SC0000, runs unmodified HISTORY,
|
||||||
|
selects/closes region 8, observes retained text, and returns to the same single page wait.
|
||||||
|
|
||||||
|
History's remaining work is stored voice replay through `0x1bd` and the `0xd3/0xd4/0xd5` smooth-scroll
|
||||||
|
callback scheduler. None changes backlog ownership or requires choosing a save/profile backend.
|
||||||
|
|
||||||
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
|
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
|
||||||
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is
|
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is
|
||||||
|
|||||||
@@ -573,9 +573,9 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
- **evidence:** Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly.
|
- **evidence:** Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly.
|
||||||
|
|
||||||
### 0x12e `find-hit-rectangle` (find-hit-rectangle, argc 8)
|
### 0x12e `find-hit-rectangle` (find-hit-rectangle, argc 8)
|
||||||
- **summary:** (index_inout)(reference_rect)(pointer_x)(pointer_y)(rect_array)(x_offsets)(y_offsets)(count) - scan forward through encoded rectangles and return the next hit index, or -1.
|
- **summary:** (index_inout)(reference_rect)(pointer_x)(pointer_y)(rect_array)(x_offsets)(y_offsets)(count) - scan after the incoming index for the next inclusive rectangle intersection, or return -1.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra /v2: op_0x12e_find_hit_rectangle@0x428ff0 resolves the reference rectangle and three array operands as VM pointers, decodes rectangle/offset values with anti_tamper_a, scans candidates after the incoming index, applies per-entry x/y offsets to the pointer coordinates, performs rectangle overlap containment tests, and writes the matched array index or -1 to operand 1. HISTORY.BIN uses it for mouse hover selection over its literal-initialized row rectangles.
|
- **evidence:** Ghidra /v2: op_0x12e_find_hit_rectangle@0x428ff0 resolves the reference rectangle and three array operands as VM pointers, decodes rectangle/offset values with anti_tamper_a, and starts at incoming_index+1. Each candidate is [left,right,top,bottom]; the worker subtracts its per-entry x/y offsets from the pointer, inclusively intersects it with the reference rectangle, and writes the matched array index or -1 to operand 1. HISTORY.BIN uses decoded local arrays for scrollbar/control regions, close region 8, and visible text rows 9..13.
|
||||||
|
|
||||||
### 0x19a `get-message-skip` (u00414E50, argc 1)
|
### 0x19a `get-message-skip` (u00414E50, argc 1)
|
||||||
- **summary:** (out) - return the current all-message skip state set by op 0x88.
|
- **summary:** (out) - return the current all-message skip state set by op 0x88.
|
||||||
|
|||||||
@@ -1752,3 +1752,26 @@ opcodes and 840/854 instructions handled or safe-noop; its ten remaining effectf
|
|||||||
**Next:** implement the basic History interaction slice around rectangle hit/hover (`0x12e`) and clean exit
|
**Next:** implement the basic History interaction slice around rectangle hit/hover (`0x12e`) and clean exit
|
||||||
using the existing callback layer. Then add stored voice replay (`0x1bd`); leave `0xd3/0xd4/0xd5` smooth
|
using the existing callback layer. Then add stored voice replay (`0x1bd`); leave `0xd3/0xd4/0xd5` smooth
|
||||||
scroll interpolation as a separate fidelity slice.
|
scroll interpolation as a separate fidelity slice.
|
||||||
|
|
||||||
|
### ADV History basic interaction implemented (2026-07-19)
|
||||||
|
|
||||||
|
The fourth bounded slice is complete. Op `0x12e` now performs its native forward scan over addressable VM
|
||||||
|
arrays, including HISTORY's local rectangle and offset tables. It begins after the incoming index, applies
|
||||||
|
per-candidate offsets, and inclusively intersects `[left,right,top,bottom]` candidates with the supplied
|
||||||
|
reference rectangle. The shipped arrays resolve to scrollbar/control regions, bottom-right close candidate
|
||||||
|
8, and five 650x130 dialogue rows at candidates 9..13; the script's existing redraw owns hover visuals.
|
||||||
|
|
||||||
|
Raw input ownership is now frame-scoped. Registering a timed mouse callback through `0xcc` marks that script
|
||||||
|
frame as the input owner until return. Godot continues updating raw mouse bits and input callback indices,
|
||||||
|
but does not release the enclosing ADV page semaphore while that modal loop is active. This closes a real
|
||||||
|
scheduler hole without a HISTORY filename, scene offset, or synthetic button path.
|
||||||
|
|
||||||
|
Two regressions cover inclusive edges/forward scan and the complete live route. The latter reaches SC0000's
|
||||||
|
first wait, activates the real x=684 History hotspot, lets unmodified `HISTORY.BIN` render retained text,
|
||||||
|
presses/releases its real close region, and proves the script returns with recording restored while the same
|
||||||
|
single ADV wait remains parked. Validation: engine 196/196, zero-warning Godot build, threaded `SELFTEST OK`.
|
||||||
|
HISTORY is now 69/78 distinct opcodes and 841/854 instructions handled or safe-noop; its nine remaining
|
||||||
|
effectful gaps total 13 instructions.
|
||||||
|
|
||||||
|
**Next:** implement stored History voice replay (`0x1bd`) against the existing voice host path. Then take
|
||||||
|
`0xd3/0xd4/0xd5` smooth-scroll interpolation as a separate scheduler/fidelity slice.
|
||||||
|
|||||||
109
engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs
Normal file
109
engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using Age.Engine.Hosting;
|
||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Sys4;
|
||||||
|
using Age.Engine.Vm;
|
||||||
|
|
||||||
|
public class HistoryInteractionOpsTests
|
||||||
|
{
|
||||||
|
private const int T_IMM = 0, T_GINT = 3, T_LINT = 9;
|
||||||
|
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
private static Operand I(long value) => new(T_IMM, value);
|
||||||
|
private static Operand G(int address) => new(T_GINT, address);
|
||||||
|
private static Operand L(int address) => new(T_LINT, address);
|
||||||
|
|
||||||
|
private sealed class StopAfterHistoryReturnsException : Exception { }
|
||||||
|
|
||||||
|
private sealed class Sc0000HistoryCloseHost : RecordingHost
|
||||||
|
{
|
||||||
|
public VirtualMachine Vm = null!;
|
||||||
|
private long _now;
|
||||||
|
private int _modalSleeps;
|
||||||
|
public bool HistoryReturned;
|
||||||
|
public bool SawRenderedText;
|
||||||
|
public override long InputClockMilliseconds => _now;
|
||||||
|
|
||||||
|
public override void Sleep(long duration)
|
||||||
|
{
|
||||||
|
base.Sleep(duration);
|
||||||
|
_now += System.Math.Max(16, duration);
|
||||||
|
if (!Vm.IsRawInputCallbackActive) return;
|
||||||
|
_modalSleeps++;
|
||||||
|
if (_modalSleeps == 1)
|
||||||
|
{
|
||||||
|
Vm.UpdatePointer(790, 570); // HISTORY candidate 8: visible bottom-right close region
|
||||||
|
Vm.UpdateMouseButtonState(0x1, true);
|
||||||
|
Vm.QueueInputCallback(4);
|
||||||
|
}
|
||||||
|
else if (_modalSleeps == 3)
|
||||||
|
{
|
||||||
|
Vm.UpdateMouseButtonState(0x1, false);
|
||||||
|
Vm.QueueInputCallback(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
|
||||||
|
{
|
||||||
|
Waits++;
|
||||||
|
Vm.UpdatePointer(684, 572);
|
||||||
|
while (serviceInputCallback()) { }
|
||||||
|
Assert.True(Vm.TryActivatePointer(684, 572));
|
||||||
|
while (serviceInputCallback()) { }
|
||||||
|
HistoryReturned = !Vm.IsRawInputCallbackActive && !Vm.TextHistory.RecordingSuppressed;
|
||||||
|
SawRenderedText = HistoryRenders.Any(render => render.Text.Length > 0);
|
||||||
|
throw new StopAfterHistoryReturnsException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindHitRectangleScansAfterTheIncomingIndexWithInclusiveEdges()
|
||||||
|
{
|
||||||
|
var ops = new List<(int, Operand[])>();
|
||||||
|
void Set(int address, long value) => ops.Add((0x55, new[] { L(address), I(value) }));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) Set(i, 0); // point-sized reference rectangle
|
||||||
|
long[][] rectangles =
|
||||||
|
{
|
||||||
|
new long[] { 0, 10, 0, 10 },
|
||||||
|
new long[] { 0, 20, 0, 20 },
|
||||||
|
new long[] { 0, 20, 0, 20 },
|
||||||
|
};
|
||||||
|
for (int rectangle = 0; rectangle < rectangles.Length; rectangle++)
|
||||||
|
for (int field = 0; field < 4; field++) Set(100 + rectangle * 4 + field, rectangles[rectangle][field]);
|
||||||
|
foreach (var (address, value) in new[]
|
||||||
|
{
|
||||||
|
(200, 100L), (201, 200L), (202, 300L),
|
||||||
|
(210, 100L), (211, 200L), (212, 300L),
|
||||||
|
}) Set(address, value);
|
||||||
|
Set(50, 0); // skip candidate 0 and begin at candidate 1
|
||||||
|
ops.Add((0x12e, new[] { L(50), L(0), I(220), I(220), L(100), L(200), L(210), I(3) }));
|
||||||
|
ops.Add((0x55, new[] { G(0x100), L(50) }));
|
||||||
|
Set(50, 1);
|
||||||
|
ops.Add((0x12e, new[] { L(50), L(0), I(321), I(320), L(100), L(200), L(210), I(3) }));
|
||||||
|
ops.Add((0x55, new[] { G(0x101), L(50) }));
|
||||||
|
ops.Add((0x2, Array.Empty<Operand>()));
|
||||||
|
var script = ScriptAssembler.Assemble(Table, "HIT_RECT", ops, Array.Empty<string>());
|
||||||
|
var vm = new VirtualMachine(script, Table, new RecordingHost());
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal(1, vm.Globals[0x100]); // (220,220) is on candidate 1's inclusive edge
|
||||||
|
Assert.Equal(-1, vm.Globals[0x101]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RealSc0000HistoryButtonRendersAndClosesWithoutAdvancingThePageWait()
|
||||||
|
{
|
||||||
|
var scripts = Sys4ScriptProvider.Load(Table);
|
||||||
|
var host = new Sc0000HistoryCloseHost();
|
||||||
|
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
|
||||||
|
new VmOptions(MaxSteps: 2_000_000), scripts);
|
||||||
|
host.Vm = vm;
|
||||||
|
vm.Globals[0x6c1] = 1;
|
||||||
|
|
||||||
|
Assert.Throws<StopAfterHistoryReturnsException>(() => vm.Run());
|
||||||
|
|
||||||
|
Assert.True(host.SawRenderedText);
|
||||||
|
Assert.True(host.HistoryReturned);
|
||||||
|
Assert.Equal(1, host.Waits); // the enclosing ADV page was never released or re-entered
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ public sealed class VirtualMachine
|
|||||||
private readonly ITraceSink _sink;
|
private readonly ITraceSink _sink;
|
||||||
private readonly object _interactiveLock = new();
|
private readonly object _interactiveLock = new();
|
||||||
private ExecFrame? _interactiveFrame;
|
private ExecFrame? _interactiveFrame;
|
||||||
|
private ExecFrame? _rawInputFrame;
|
||||||
private int _pointerX = int.MinValue, _pointerY = int.MinValue;
|
private int _pointerX = int.MinValue, _pointerY = int.MinValue;
|
||||||
private int _mouseButtonState;
|
private int _mouseButtonState;
|
||||||
private int _heldInputCallbackMask;
|
private int _heldInputCallbackMask;
|
||||||
@@ -48,6 +49,11 @@ public sealed class VirtualMachine
|
|||||||
public long Steps { get; private set; }
|
public long Steps { get; private set; }
|
||||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||||
public bool MessageSkipEnabled => _messageSkipEnabled;
|
public bool MessageSkipEnabled => _messageSkipEnabled;
|
||||||
|
/// <summary>True while a script-owned timed mouse/input callback loop (HISTORY/HIDEWIN family) owns input.</summary>
|
||||||
|
public bool IsRawInputCallbackActive
|
||||||
|
{
|
||||||
|
get { lock (_interactiveLock) return _rawInputFrame != null; }
|
||||||
|
}
|
||||||
public AdvTextHistory TextHistory { get; }
|
public AdvTextHistory TextHistory { get; }
|
||||||
|
|
||||||
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null,
|
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null,
|
||||||
@@ -231,6 +237,28 @@ public sealed class VirtualMachine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long ReadAddressedCell(Operand operand, int offset)
|
||||||
|
{
|
||||||
|
return operand.Type switch
|
||||||
|
{
|
||||||
|
T_LINT => Gi(_cur.Locals.I, checked((int)operand.Value + offset)),
|
||||||
|
T_LFLOAT => Gi(_cur.Locals.F, checked((int)operand.Value + offset)),
|
||||||
|
T_GINT or T_GFLOAT => ReadGlobal(checked((int)operand.Value + offset)),
|
||||||
|
T_LPTR => Gi(Globals, checked((int)Gi(_cur.Locals.P, (int)operand.Value) + offset)),
|
||||||
|
T_GPTR => Gi(Globals, checked((int)Gi(Globals, (int)operand.Value) + offset)),
|
||||||
|
_ => Gi(Globals, checked((int)operand.Value + offset)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool IsLocal, int Address) AddressedCellIdentity(Operand operand, int offset)
|
||||||
|
=> operand.Type switch
|
||||||
|
{
|
||||||
|
T_LINT or T_LFLOAT => (true, checked((int)operand.Value + offset)),
|
||||||
|
T_LPTR => (false, checked((int)Gi(_cur.Locals.P, (int)operand.Value) + offset)),
|
||||||
|
T_GPTR => (false, checked((int)Gi(Globals, (int)operand.Value) + offset)),
|
||||||
|
_ => (false, checked((int)operand.Value + offset)),
|
||||||
|
};
|
||||||
|
|
||||||
private string FormatSwitchValue(Operand operand)
|
private string FormatSwitchValue(Operand operand)
|
||||||
=> IsStr(operand)
|
=> IsStr(operand)
|
||||||
? ReadStr(operand)
|
? ReadStr(operand)
|
||||||
@@ -256,7 +284,12 @@ public sealed class VirtualMachine
|
|||||||
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
||||||
{
|
{
|
||||||
ExecFrame? previousInteractiveFrame;
|
ExecFrame? previousInteractiveFrame;
|
||||||
lock (_interactiveLock) previousInteractiveFrame = _interactiveFrame;
|
ExecFrame? previousRawInputFrame;
|
||||||
|
lock (_interactiveLock)
|
||||||
|
{
|
||||||
|
previousInteractiveFrame = _interactiveFrame;
|
||||||
|
previousRawInputFrame = _rawInputFrame;
|
||||||
|
}
|
||||||
var prev = _cur; _cur = frame; _depth++;
|
var prev = _cur; _cur = frame; _depth++;
|
||||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||||
var outcome = FrameOutcome.RanOff;
|
var outcome = FrameOutcome.RanOff;
|
||||||
@@ -280,6 +313,7 @@ public sealed class VirtualMachine
|
|||||||
? previousInteractiveFrame : null;
|
? previousInteractiveFrame : null;
|
||||||
else if (ReferenceEquals(_interactiveFrame, frame))
|
else if (ReferenceEquals(_interactiveFrame, frame))
|
||||||
_interactiveFrame = null;
|
_interactiveFrame = null;
|
||||||
|
if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame;
|
||||||
}
|
}
|
||||||
_cur = prev; _depth--;
|
_cur = prev; _depth--;
|
||||||
return outcome;
|
return outcome;
|
||||||
@@ -372,6 +406,37 @@ public sealed class VirtualMachine
|
|||||||
WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i]));
|
WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i]));
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
|
case "find-hit-rectangle": // 0x12e: inclusive rectangle intersection over addressed arrays
|
||||||
|
case "u0041E940":
|
||||||
|
{
|
||||||
|
int previous = (int)Read(a[0]);
|
||||||
|
int count = System.Math.Max(0, (int)Read(a[7]));
|
||||||
|
long refLeft = ReadAddressedCell(a[1], 0);
|
||||||
|
long refRight = ReadAddressedCell(a[1], 1);
|
||||||
|
long refTop = ReadAddressedCell(a[1], 2);
|
||||||
|
long refBottom = ReadAddressedCell(a[1], 3);
|
||||||
|
int match = -1;
|
||||||
|
for (int index = previous + 1; index < count; index++)
|
||||||
|
{
|
||||||
|
long x = Read(a[2]) - ReadAddressedCell(a[5], index);
|
||||||
|
long y = Read(a[3]) - ReadAddressedCell(a[6], index);
|
||||||
|
long left = ReadAddressedCell(a[4], index * 4);
|
||||||
|
long right = ReadAddressedCell(a[4], index * 4 + 1);
|
||||||
|
long top = ReadAddressedCell(a[4], index * 4 + 2);
|
||||||
|
long bottom = ReadAddressedCell(a[4], index * 4 + 3);
|
||||||
|
bool isReferenceRectangle = AddressedCellIdentity(a[1], 0)
|
||||||
|
== AddressedCellIdentity(a[4], index * 4);
|
||||||
|
if (!isReferenceRectangle
|
||||||
|
&& x + refLeft <= right && x + refRight >= left
|
||||||
|
&& y + refTop <= bottom && y + refBottom >= top)
|
||||||
|
{
|
||||||
|
match = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write(a[0], match);
|
||||||
|
return pc + 1;
|
||||||
|
}
|
||||||
case "bit-set":
|
case "bit-set":
|
||||||
{
|
{
|
||||||
long bit = Read(a[1]);
|
long bit = Read(a[1]);
|
||||||
@@ -577,6 +642,7 @@ public sealed class VirtualMachine
|
|||||||
_cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0]));
|
_cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0]));
|
||||||
_cur.MouseCallbackTarget = (int)Read(a[1]);
|
_cur.MouseCallbackTarget = (int)Read(a[1]);
|
||||||
_cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs;
|
_cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs;
|
||||||
|
lock (_interactiveLock) _rawInputFrame = _cur;
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "get-input-type":
|
case "get-input-type":
|
||||||
case "dispatch-mouse-callback": // 0xcd
|
case "dispatch-mouse-callback": // 0xcd
|
||||||
|
|||||||
@@ -357,22 +357,24 @@ public partial class Main : Godot.Control
|
|||||||
{
|
{
|
||||||
var p = ToNativeScreen(mb.Position);
|
var p = ToNativeScreen(mb.Position);
|
||||||
bool advPageSuspended = _host.IsAdvPagePresentationSuspended;
|
bool advPageSuspended = _host.IsAdvPagePresentationSuspended;
|
||||||
|
bool rawInputCallbackActive = _vm.IsRawInputCallbackActive;
|
||||||
_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,
|
// 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
|
// and the configured primary action (default input callback index 4). Script-owned callback
|
||||||
// activation click's release arms the script; the next completed left click restores it.
|
// loops consume both channels without releasing the enclosing ADV page wait.
|
||||||
if (mb.ButtonIndex == MouseButton.Left && advPageSuspended)
|
if (mb.ButtonIndex == MouseButton.Left && (advPageSuspended || rawInputCallbackActive))
|
||||||
_vm.QueueInputCallback(mb.Pressed ? 4 : 10);
|
_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;
|
||||||
}
|
}
|
||||||
// A left click owned by the yielded page must return through HIDEWIN's callback/coroutine
|
// Modal callback scripts return through their own bytecode. Signaling the enclosing ADV wait
|
||||||
// path. Releasing the enclosing ADV wait here would also advance the restored dialogue page.
|
// here would also advance the restored dialogue page after HISTORY/HIDEWIN exits.
|
||||||
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && !advPageSuspended) _host.SignalInput();
|
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed
|
||||||
|
&& !advPageSuspended && !rawInputCallbackActive) _host.SignalInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UpdateAgeInputCallback(e, "ui_down", 0);
|
UpdateAgeInputCallback(e, "ui_down", 0);
|
||||||
@@ -381,7 +383,8 @@ public partial class Main : Godot.Control
|
|||||||
UpdateAgeInputCallback(e, "ui_right", 3);
|
UpdateAgeInputCallback(e, "ui_right", 3);
|
||||||
UpdateAgeInputCallback(e, "ui_accept", 4);
|
UpdateAgeInputCallback(e, "ui_accept", 4);
|
||||||
UpdateAgeInputCallback(e, "ui_cancel", 5);
|
UpdateAgeInputCallback(e, "ui_cancel", 5);
|
||||||
if (e.IsActionPressed("ui_accept")) _host.SignalInput();
|
if (e.IsActionPressed("ui_accept")
|
||||||
|
&& !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive) _host.SignalInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAgeInputCallback(InputEvent e, StringName action, int index)
|
private void UpdateAgeInputCallback(InputEvent e, StringName action, int index)
|
||||||
|
|||||||
@@ -2526,12 +2526,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "find-hit-rectangle"
|
name = "find-hit-rectangle"
|
||||||
category = "input"
|
category = "input"
|
||||||
summary = "(index_inout)(reference_rect)(pointer_x)(pointer_y)(rect_array)(x_offsets)(y_offsets)(count) - scan forward through encoded rectangles and return the next hit index, or -1."
|
summary = "(index_inout)(reference_rect)(pointer_x)(pointer_y)(rect_array)(x_offsets)(y_offsets)(count) - scan after the incoming index for the next inclusive rectangle intersection, or return -1."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra /v2: op_0x12e_find_hit_rectangle@0x428ff0 resolves the reference rectangle and three array operands as VM pointers, decodes rectangle/offset values with anti_tamper_a, scans candidates after the incoming index, applies per-entry x/y offsets to the pointer coordinates, performs rectangle overlap containment tests, and writes the matched array index or -1 to operand 1. HISTORY.BIN uses it for mouse hover selection over its literal-initialized row rectangles."
|
evidence = "Ghidra /v2: op_0x12e_find_hit_rectangle@0x428ff0 resolves the reference rectangle and three array operands as VM pointers, decodes rectangle/offset values with anti_tamper_a, and starts at incoming_index+1. Each candidate is [left,right,top,bottom]; the worker subtracts its per-entry x/y offsets from the pointer, inclusively intersects it with the reference rectangle, and writes the matched array index or -1 to operand 1. HISTORY.BIN uses decoded local arrays for scrollbar/control regions, close region 8, and visible text rows 9..13."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user