Restore multi-row ADV History display

This commit is contained in:
gamer147
2026-07-19 10:57:58 -04:00
parent 280705d69c
commit c30cf6cb08
5 changed files with 142 additions and 11 deletions

View File

@@ -1374,11 +1374,24 @@ local pointer reach the correct bank. This restores the six controls at x=768/y=
at x=768/y=549, and the hovered-row highlight while preserving RECOVER's global-array pointer behavior.
The Python A0 oracle uses the same typed-address model.
The History text batches already carry the native x origin 65 and cursor x 45. Their Godot labels were
created with a full-rect anchor preset before being parented, which discarded the intended absolute
placement in the live UI and caused a Godot parent/layout diagnostic. Dynamically composited ADV labels now
use the default top-left anchors and their explicit position/size, yielding the native text x=110 and
avoiding that diagnostic.
The History text batches already carry the native x origin 65 and cursor x 45. Their Godot labels were first
created with a full-rect anchor preset before being parented, which discarded the intended absolute placement
and caused a Godot parent/layout diagnostic. Dynamically composited ADV labels now use top-left anchors, and
all root Controls apply their presets only after parenting.
A second manual check separated the apparent bottom row from the real History batches: it was the ordinary
ADV dialogue Label leaking above HISTORY's full-screen retained surface. The real batches contained several
non-empty retained groups, but their target layouts had width/height zero and Godot clipped each to one pixel.
`HISTORY.BIN` deliberately only resets and repositions layouts 2..6; `SYSTEM4.BIN@0x7..0x82` defines and
resets all nine shared layouts before scene dispatch. In particular, slots 2..6 are 650x150 at x=65 and
y=0/150/300/450/600. The Phase-A single-scene bootstrap now carries forward that exact nine-layout prefix,
in the same category as its inherited SO000/SO001 state, until full SYSTEM4 replay replaces the bridge.
The multi-message regression proves that at least two non-empty rows retain 650x150 geometry.
While a nested timed raw-input frame owns the screen, Godot now hides the enclosing page's ordinary dialogue
Label and independently animated wait marker. Native rendering gets this layering naturally because both are
part of retained composition; the port must state it explicitly because those two elements are separate
Godot overlays. They reappear with the same parked page after the modal frame returns.
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.

View File

@@ -1790,5 +1790,18 @@ Godot's dynamically created History/surface labels also no longer request full-r
a parent. They use top-left absolute placement, matching the batch's origin+cursor coordinates and removing
the associated parent/layout diagnostic.
**Next:** manually recheck History layout and hover against the original screenshot. If it matches, proceed
The next manual run showed that the sole bottom line was actually the enclosing ADV dialogue overlay, while
the genuine retained batches were clipped to one pixel. The backlog itself is healthy: a new real SC0000
six-message regression produces multiple non-empty History rows. The missing state was the exact
`SYSTEM4.BIN` prefix that defines nine shared text layouts before dispatching any scene; HISTORY only resets
and repositions slots 2..6. The single-scene Godot bootstrap now carries forward all nine definitions, with
History rows sized 650x150 at x=65 and y increments of 150, until full SYSTEM4 replay replaces this existing
class of inherited-state bridges.
Godot also suppresses the parked page's ordinary dialogue Label and wait indicator while any nested timed
raw-input frame owns the modal screen, then restores them on return. Root Control anchor presets are now
applied after parenting, removing the remaining occurrence of the associated runtime layout diagnostic.
Validation: engine 198/198, zero-warning Godot build, threaded `SELFTEST OK`, and vm0 RECOVER clean.
**Next:** manually recheck the five History rows and overlay cleanup against the original. If they match, proceed
with stored History voice replay (`0x1bd`), followed by `0xd3/0xd4/0xd5` smooth-scroll fidelity.

View File

@@ -14,9 +14,14 @@ public class HistoryInteractionOpsTests
private sealed class StopAfterHistoryReturnsException : Exception { }
private static void SeedSystem4AdvLayouts(Sys4ScriptProvider scripts, AdvTextHistory history)
=> Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
scripts.RequireByName("SYSTEM4.BIN"), Table, history));
private sealed class Sc0000HistoryCloseHost : RecordingHost
{
public VirtualMachine Vm = null!;
private readonly int _openAtWait;
private long _now;
private int _modalSleeps;
public bool HistoryReturned;
@@ -24,6 +29,8 @@ public class HistoryInteractionOpsTests
public IReadOnlyList<RenderObject> FirstHistoryFrame = Array.Empty<RenderObject>();
public override long InputClockMilliseconds => _now;
public Sc0000HistoryCloseHost(int openAtWait = 1) => _openAtWait = openAtWait;
public override void Sleep(long duration)
{
base.Sleep(duration);
@@ -47,6 +54,7 @@ public class HistoryInteractionOpsTests
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
Waits++;
if (Waits < _openAtWait) return;
Vm.UpdatePointer(684, 572);
while (serviceInputCallback()) { }
Assert.True(Vm.TryActivatePointer(684, 572));
@@ -57,6 +65,34 @@ public class HistoryInteractionOpsTests
}
}
[Fact]
public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages()
{
var scripts = Sys4ScriptProvider.Load(Table);
var host = new Sc0000HistoryCloseHost(openAtWait: 6);
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryReturnsException>(() => vm.Run());
var visibleRows = host.HistoryRenders
.Where(render => render.Text.Length > 0)
.GroupBy(render => render.LayoutSlot)
.Select(group => group.Last())
.ToArray();
Assert.True(visibleRows.Length >= 2,
$"Expected multiple retained History rows after six messages, got {visibleRows.Length}: "
+ string.Join(" | ", visibleRows.Select(render => render.Text)));
Assert.All(visibleRows, render =>
{
Assert.Equal(650, render.Layout.Width);
Assert.Equal(150, render.Layout.Height);
});
}
[Fact]
public void LookupArrayPreservesLocalStorageForLocalBases()
{
@@ -123,6 +159,7 @@ public class HistoryInteractionOpsTests
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryReturnsException>(() => vm.Run());

View File

@@ -0,0 +1,51 @@
using Age.Engine.Model;
namespace Age.Engine.Vm;
/// <summary>
/// Replays the data-only leading ADV layout block from an engine system script. This lets a bounded
/// single-scene runner inherit script-owned layout state without hard-coding one game's coordinates or
/// entering the system script's later menu/session control flow.
/// </summary>
public static class AdvTextLayoutBootstrap
{
public static int ApplyLeadingDefinitionsAndResets(
Script systemScript, OpcodeTable table, AdvTextHistory history)
{
int? defineOpcode = table.ByLabel("define-adv-text-layout");
int? resetOpcode = table.ByLabel("reset-adv-text-layout");
if (defineOpcode == null || resetOpcode == null) return 0;
bool foundDefinition = false;
bool resetPhase = false;
int definitions = 0;
foreach (var instruction in systemScript.Instructions)
{
if (!foundDefinition && instruction.Opcode != defineOpcode.Value) continue;
if (instruction.Opcode == defineOpcode.Value && !resetPhase)
{
if (instruction.Args.Count != 5 || instruction.Args.Any(arg => arg.Type != 0))
throw new InvalidDataException(
$"{systemScript.Name}@0x{instruction.Offset:x}: leading ADV layout must use five immediates");
history.DefineLayout(
checked((int)instruction.Args[0].Value), checked((int)instruction.Args[1].Value),
checked((int)instruction.Args[2].Value), checked((int)instruction.Args[3].Value),
checked((int)instruction.Args[4].Value));
foundDefinition = true;
definitions++;
continue;
}
if (foundDefinition && instruction.Opcode == resetOpcode.Value)
{
if (instruction.Args.Count != 1 || instruction.Args[0].Type != 0)
throw new InvalidDataException(
$"{systemScript.Name}@0x{instruction.Offset:x}: leading ADV reset must use one immediate");
resetPhase = true;
history.ResetLayout(checked((int)instruction.Args[0].Value));
continue;
}
break;
}
return definitions;
}
}

View File

@@ -84,8 +84,8 @@ public partial class Main : Godot.Control
StretchMode = TextureRect.StretchModeEnum.Scale,
MouseFilter = MouseFilterEnum.Ignore,
};
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
AddChild(_screenView); // added first -> draws behind the text/status labels
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
// Native ADV wait marker: a tiny independently animated atlas region. Keeping it separate from the
// 800x600 software backbuffer avoids recompositing the entire retained scene throughout static waits.
@@ -99,16 +99,16 @@ public partial class Main : Godot.Control
AddChild(_waitIndicator);
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = MouseFilterEnum.Ignore };
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
AddChild(_text);
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_speaker = new Label { MouseFilter = MouseFilterEnum.Ignore, Visible = false };
_speaker.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
AddChild(_speaker);
_speaker.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_surfaceTextLabels.Add(_speaker);
_status = new Label();
AddChild(_status);
_status.SetAnchorsAndOffsetsPreset(LayoutPreset.BottomWide);
_status.OffsetLeft = 40; _status.OffsetTop = -60;
AddChild(_status);
_locatorHud = new Label { Visible = false, MouseFilter = MouseFilterEnum.Ignore };
_locatorHud.Position = new Vector2(8, 8);
AddChild(_locatorHud);
@@ -212,6 +212,13 @@ public partial class Main : Godot.Control
if (histFile != null) { _hist = new Age.Engine.Diagnostics.HistogramTraceSink();
sink = new Age.Engine.Diagnostics.CompositeTraceSink(_trace, _hist); }
_vm = new VirtualMachine(script, table, _host, new VmOptions(MaxSteps: 20_000_000), provider, sink);
// SYSTEM4.BIN defines these nine shared ADV text layouts before dispatching any scene. The
// single-scene harness starts after that prefix, so carry forward its exact script-owned state
// alongside the inherited SO000/SO001 state below. Full Phase-B SYSTEM4 replay will replace this
// bootstrap as one unit; HISTORY depends on the 650x150 dimensions of layouts 2..6 for clipping.
if (!_selftest)
AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
scripts!.RequireByName("SYSTEM4.BIN"), table, _vm.TextHistory);
// SYSTEM4 loads the shared SO001 chrome sheet into surface slot 17 before any scene runs.
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
if (!_selftest && resources.ResolveName("SO001.AGF") is { } systemChrome)
@@ -541,7 +548,10 @@ public partial class Main : Godot.Control
private void UpdateAdvTextPresentation()
{
_text.Visible = !_host.IsAdvPagePresentationSuspended;
// Modal callback scripts composite their own full-screen UI while the enclosing ADV wait remains
// parked. The ordinary dialogue Label is a Godot overlay rather than part of the retained surface,
// so hide it while that nested input owner is active or it leaks above HISTORY's background.
_text.Visible = !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive;
if (!_text.Visible) return;
var t = _host.SnapshotAdvText();
_text.Position = new Vector2(t.X, 430 + t.Y);
@@ -621,6 +631,13 @@ public partial class Main : Godot.Control
private void UpdateAdvWaitIndicatorPresentation()
{
// As with the ordinary dialogue Label, the enclosing page's separately animated marker must sit
// out while HISTORY/HIDEWIN owns raw input; native retained composition naturally covers it.
if (_vm.IsRawInputCallbackActive)
{
_waitIndicator.Visible = false;
return;
}
var snapshot = _host.SnapshotAdvWaitIndicator();
if (snapshot == null)
{