Implement ADV layout cursor and bounds
This commit is contained in:
@@ -45,7 +45,7 @@ public class AdvTextHistoryTests
|
||||
Assert.Equal(123, metadata.Value);
|
||||
Assert.Equal(1, metadata.AuxValue);
|
||||
Assert.Equal((24, 0xf0e0d0L), (metadata.Style.PrimaryFontSize, metadata.Style.TextColor));
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(1, 640, 160, 80, 430, 12, 34), metadata.Layout);
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(1, 640, 160, 80, 430, 12, 34, 640, 160), metadata.Layout);
|
||||
},
|
||||
voice =>
|
||||
{
|
||||
@@ -109,7 +109,27 @@ public class AdvTextHistoryTests
|
||||
var record = Assert.Single(history.Records);
|
||||
Assert.Equal("new", record.Text);
|
||||
Assert.False(record.Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart));
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0), record.Layout);
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0, 320, 90), record.Layout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfiguredCursorIsDeferredUntilResetWhileBoundsApplyToTheLayout()
|
||||
{
|
||||
var history = new AdvTextHistory();
|
||||
history.DefineLayout(1, 800, 160, 0, 430);
|
||||
history.ResetLayout(1);
|
||||
history.SetCursor(1, 12, 34);
|
||||
|
||||
history.SetResetCursor(1, 100, 47);
|
||||
history.SetBounds(1, 720, 147);
|
||||
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(1, 800, 160, 0, 430, 12, 34, 720, 147),
|
||||
history.GetLayoutSnapshot(1));
|
||||
|
||||
history.ResetLayout(1);
|
||||
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(1, 800, 160, 0, 430, 100, 47, 720, 147),
|
||||
history.GetLayoutSnapshot(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -31,6 +31,58 @@ public class AdvTextOpsTests
|
||||
Assert.Equal((13, 1, 1, "speaker"), Assert.Single(host.SurfaceStrings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayoutResetRestoresConfiguredCursorAndPreservesConfiguredBounds()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "ADV_LAYOUT_CONFIG",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x70, new[] { new Operand(0, 1), new Operand(0, 800), new Operand(0, 160),
|
||||
new Operand(0, 0), new Operand(0, 430) }),
|
||||
(0x71, new[] { new Operand(0, 1) }),
|
||||
(0x79, new[] { new Operand(0, 1), new Operand(0, 100), new Operand(0, 47) }),
|
||||
(0x1c1, new[] { new Operand(0, 1), new Operand(0, 720), new Operand(0, 147) }),
|
||||
(0x7a, new[] { new Operand(0, 1), new Operand(0, 12), new Operand(0, 34) }),
|
||||
(0x71, new[] { new Operand(0, 1) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(new[] { (1, 0, 0), (1, 12, 34), (1, 100, 47) }, host.TextCursors);
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(1, 800, 160, 0, 430, 100, 47, 720, 147),
|
||||
vm.TextHistory.GetLayoutSnapshot(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System4BootstrapReplaysAllResetCursorAndBoundsConfigurations()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var scripts = Sys4ScriptProvider.Load(table);
|
||||
var history = new AdvTextHistory();
|
||||
|
||||
Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
|
||||
scripts.RequireByName("SYSTEM4.BIN"), table, history));
|
||||
|
||||
// SYSTEM4's configuration follows its initial reset run, so the configured cursor is deferred.
|
||||
Assert.Equal((0, 0, 720, 147), CursorAndBounds(history.GetLayoutSnapshot(1)));
|
||||
history.ResetLayout(1);
|
||||
Assert.Equal((100, 47, 720, 147), CursorAndBounds(history.GetLayoutSnapshot(1)));
|
||||
history.ResetLayout(4);
|
||||
Assert.Equal((45, 42, 645, 135), CursorAndBounds(history.GetLayoutSnapshot(4)));
|
||||
history.ResetLayout(8);
|
||||
Assert.Equal((53, 10, 495, 60), CursorAndBounds(history.GetLayoutSnapshot(8)));
|
||||
history.ResetLayout(9);
|
||||
Assert.Equal((10, 10, 250, 368), CursorAndBounds(history.GetLayoutSnapshot(9)));
|
||||
|
||||
static (int X, int Y, int Right, int Bottom) CursorAndBounds(AdvTextLayoutSnapshot layout)
|
||||
=> (layout.CursorX, layout.CursorY, layout.Right, layout.Bottom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitIndicatorConfigurationReachesHost()
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@ public class HistoryPresentationOpsTests
|
||||
var render = Assert.Single(host.HistoryRenders);
|
||||
Assert.Equal((4, 0, "retained dialogue"),
|
||||
(render.LayoutSlot, render.FirstRecordIndex, render.Text));
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(4, 600, 150, 65, 150, 45, 42), render.Layout);
|
||||
Assert.Equal(new AdvTextLayoutSnapshot(4, 600, 150, 65, 150, 45, 42, 600, 150), render.Layout);
|
||||
Assert.Equal((24, 0xffffffL, 0x606060L, 8),
|
||||
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
|
||||
render.Style.LineSpacing));
|
||||
|
||||
@@ -40,7 +40,9 @@ public readonly record struct AdvTextLayoutSnapshot(
|
||||
int OriginX,
|
||||
int OriginY,
|
||||
int CursorX,
|
||||
int CursorY);
|
||||
int CursorY,
|
||||
int Right,
|
||||
int Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// One semantic counterpart of AGE's 0x48-byte retained text record. Metadata uses
|
||||
@@ -80,8 +82,12 @@ public sealed class AdvTextHistory
|
||||
public int Height;
|
||||
public int OriginX;
|
||||
public int OriginY;
|
||||
public int ResetCursorX;
|
||||
public int ResetCursorY;
|
||||
public int CursorX;
|
||||
public int CursorY;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
private readonly List<AdvTextHistoryRecord> _records = new();
|
||||
@@ -105,6 +111,8 @@ public sealed class AdvTextHistory
|
||||
layout.Height = height;
|
||||
layout.OriginX = originX;
|
||||
layout.OriginY = originY;
|
||||
layout.Right = width;
|
||||
layout.Bottom = height;
|
||||
AppendBoundary(slot);
|
||||
}
|
||||
|
||||
@@ -112,11 +120,27 @@ public sealed class AdvTextHistory
|
||||
{
|
||||
int slot = SelectLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
layout.CursorX = 0;
|
||||
layout.CursorY = 0;
|
||||
layout.CursorX = layout.ResetCursorX;
|
||||
layout.CursorY = layout.ResetCursorY;
|
||||
AppendBoundary(slot);
|
||||
}
|
||||
|
||||
public void SetResetCursor(int requestedSlot, int x, int y)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
layout.ResetCursorX = x;
|
||||
layout.ResetCursorY = y;
|
||||
}
|
||||
|
||||
public void SetBounds(int requestedSlot, int right, int bottom)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
layout.Right = right;
|
||||
layout.Bottom = bottom;
|
||||
}
|
||||
|
||||
public void SetCursor(int requestedSlot, int x, int y)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
@@ -133,6 +157,9 @@ public sealed class AdvTextHistory
|
||||
layout.OriginY = y;
|
||||
}
|
||||
|
||||
public AdvTextLayoutSnapshot GetLayoutSnapshot(int requestedSlot)
|
||||
=> SnapshotLayout(ResolveLayout(requestedSlot));
|
||||
|
||||
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style,
|
||||
AdvTextHistoryRecordFlags flags = AdvTextHistoryRecordFlags.None)
|
||||
{
|
||||
@@ -291,7 +318,8 @@ public sealed class AdvTextHistory
|
||||
{
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
return new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height,
|
||||
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY);
|
||||
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY,
|
||||
layout.Right, layout.Bottom);
|
||||
}
|
||||
|
||||
private void AppendBoundary(int slot)
|
||||
@@ -325,7 +353,8 @@ public sealed class AdvTextHistory
|
||||
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
var snapshot = new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height,
|
||||
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY);
|
||||
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY,
|
||||
layout.Right, layout.Bottom);
|
||||
_records.Add(new AdvTextHistoryRecord(kind, flags, snapshot, style, text, value, auxValue, sourceOffset));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ public static class AdvTextLayoutBootstrap
|
||||
{
|
||||
int? defineOpcode = table.ByLabel("define-adv-text-layout");
|
||||
int? resetOpcode = table.ByLabel("reset-adv-text-layout");
|
||||
int? resetCursorOpcode = table.ByLabel("set-adv-text-reset-cursor");
|
||||
int? boundsOpcode = table.ByLabel("set-adv-text-bounds");
|
||||
int? addOpcode = table.ByLabel("add");
|
||||
if (defineOpcode == null || resetOpcode == null) return 0;
|
||||
|
||||
bool foundDefinition = false;
|
||||
@@ -46,6 +49,81 @@ public static class AdvTextLayoutBootstrap
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// SYSTEM4 configures the cursor restored by future resets and the right/bottom overflow bounds
|
||||
// after the initial define/reset run. Most operands are immediate; slot 1 computes its bounds with
|
||||
// two constant add instructions. Interpret only that small, data-only expression vocabulary rather
|
||||
// than entering SYSTEM4's later menu/session flow.
|
||||
if (resetCursorOpcode != null && boundsOpcode != null)
|
||||
{
|
||||
var localInts = new Dictionary<int, long>();
|
||||
bool inLayoutBlock = false;
|
||||
int cursorConfigurations = 0;
|
||||
int boundConfigurations = 0;
|
||||
foreach (var instruction in systemScript.Instructions)
|
||||
{
|
||||
if (!inLayoutBlock)
|
||||
{
|
||||
if (instruction.Opcode != defineOpcode.Value) continue;
|
||||
inLayoutBlock = true;
|
||||
}
|
||||
|
||||
if (addOpcode != null && instruction.Opcode == addOpcode.Value)
|
||||
{
|
||||
if (instruction.Args.Count == 3 && instruction.Args[0].Type == 9
|
||||
&& TryResolveConstant(instruction.Args[1], localInts, out long left)
|
||||
&& TryResolveConstant(instruction.Args[2], localInts, out long right))
|
||||
localInts[checked((int)instruction.Args[0].Value)] = left + right;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction.Opcode == resetCursorOpcode.Value)
|
||||
{
|
||||
var values = ResolveConfiguration(instruction, localInts, systemScript.Name);
|
||||
history.SetResetCursor(values[0], values[1], values[2]);
|
||||
cursorConfigurations++;
|
||||
}
|
||||
else if (instruction.Opcode == boundsOpcode.Value)
|
||||
{
|
||||
var values = ResolveConfiguration(instruction, localInts, systemScript.Name);
|
||||
history.SetBounds(values[0], values[1], values[2]);
|
||||
boundConfigurations++;
|
||||
}
|
||||
|
||||
if (cursorConfigurations >= definitions && boundConfigurations >= definitions) break;
|
||||
}
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static int[] ResolveConfiguration(
|
||||
Instruction instruction, IReadOnlyDictionary<int, long> localInts, string scriptName)
|
||||
{
|
||||
if (instruction.Args.Count != 3)
|
||||
throw new InvalidDataException(
|
||||
$"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration must use three operands");
|
||||
var values = new int[3];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (!TryResolveConstant(instruction.Args[i], localInts, out long value))
|
||||
throw new InvalidDataException(
|
||||
$"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration operand {i + 1} " +
|
||||
"is not an immediate or constant local integer");
|
||||
values[i] = checked((int)value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static bool TryResolveConstant(
|
||||
Operand operand, IReadOnlyDictionary<int, long> localInts, out long value)
|
||||
{
|
||||
if (operand.Type == 0)
|
||||
{
|
||||
value = operand.Value;
|
||||
return true;
|
||||
}
|
||||
if (operand.Type == 9 && localInts.TryGetValue(checked((int)operand.Value), out value)) return true;
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,12 +827,23 @@ public sealed class VirtualMachine
|
||||
(int)Read(a[3]), (int)Read(a[4]));
|
||||
return pc + 1;
|
||||
case "reset-adv-text-layout": // 0x71: reset layout and begin the next logical retained group
|
||||
TextHistory.ResetLayout((int)Read(a[0]));
|
||||
_host.ClearRenderedAdvTextLayout((int)Read(a[0]));
|
||||
{
|
||||
int requestedSlot = (int)Read(a[0]);
|
||||
TextHistory.ResetLayout(requestedSlot);
|
||||
var layout = TextHistory.GetLayoutSnapshot(requestedSlot);
|
||||
_host.SetAdvTextCursor(layout.Slot, layout.CursorX, layout.CursorY);
|
||||
_host.ClearRenderedAdvTextLayout(layout.Slot);
|
||||
return pc + 1;
|
||||
}
|
||||
case "set-adv-text-reset-cursor": // 0x79: configure cursor restored by a later 0x71
|
||||
TextHistory.SetResetCursor((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
return pc + 1;
|
||||
case "set-adv-text-cursor": // 0x7a (layout slot, x, y); slot 0 means current natively
|
||||
TextHistory.SetCursor((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
_host.SetAdvTextCursor((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||
case "set-adv-text-bounds": // 0x1c1: layout-local right/bottom overflow boundaries
|
||||
TextHistory.SetBounds((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
|
||||
return pc + 1;
|
||||
case "configure-adv-wait-indicator": // 0x73: per-layout animated input-wait marker
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
|
||||
Reference in New Issue
Block a user