Implement ADV History interaction
This commit is contained in:
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 object _interactiveLock = new();
|
||||
private ExecFrame? _interactiveFrame;
|
||||
private ExecFrame? _rawInputFrame;
|
||||
private int _pointerX = int.MinValue, _pointerY = int.MinValue;
|
||||
private int _mouseButtonState;
|
||||
private int _heldInputCallbackMask;
|
||||
@@ -48,6 +49,11 @@ public sealed class VirtualMachine
|
||||
public long Steps { get; private set; }
|
||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||
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 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)
|
||||
=> IsStr(operand)
|
||||
? ReadStr(operand)
|
||||
@@ -256,7 +284,12 @@ public sealed class VirtualMachine
|
||||
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
||||
{
|
||||
ExecFrame? previousInteractiveFrame;
|
||||
lock (_interactiveLock) previousInteractiveFrame = _interactiveFrame;
|
||||
ExecFrame? previousRawInputFrame;
|
||||
lock (_interactiveLock)
|
||||
{
|
||||
previousInteractiveFrame = _interactiveFrame;
|
||||
previousRawInputFrame = _rawInputFrame;
|
||||
}
|
||||
var prev = _cur; _cur = frame; _depth++;
|
||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||
var outcome = FrameOutcome.RanOff;
|
||||
@@ -280,6 +313,7 @@ public sealed class VirtualMachine
|
||||
? previousInteractiveFrame : null;
|
||||
else if (ReferenceEquals(_interactiveFrame, frame))
|
||||
_interactiveFrame = null;
|
||||
if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame;
|
||||
}
|
||||
_cur = prev; _depth--;
|
||||
return outcome;
|
||||
@@ -372,6 +406,37 @@ public sealed class VirtualMachine
|
||||
WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i]));
|
||||
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":
|
||||
{
|
||||
long bit = Read(a[1]);
|
||||
@@ -577,6 +642,7 @@ public sealed class VirtualMachine
|
||||
_cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0]));
|
||||
_cur.MouseCallbackTarget = (int)Read(a[1]);
|
||||
_cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs;
|
||||
lock (_interactiveLock) _rawInputFrame = _cur;
|
||||
return pc + 1;
|
||||
case "get-input-type":
|
||||
case "dispatch-mouse-callback": // 0xcd
|
||||
|
||||
Reference in New Issue
Block a user