From c30cf6cb083053742d89a3c95d602eb5d01d1d30 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 19 Jul 2026 10:57:58 -0400 Subject: [PATCH] Restore multi-row ADV History display --- docs/engine-re.md | 23 +++++++-- docs/phase-a-slice-plan.md | 15 +++++- .../HistoryInteractionOpsTests.cs | 37 ++++++++++++++ .../Age.Engine/Vm/AdvTextLayoutBootstrap.cs | 51 +++++++++++++++++++ godot/Main.cs | 27 ++++++++-- 5 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs diff --git a/docs/engine-re.md b/docs/engine-re.md index e149218..052c108 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 5230de2..b0dcba7 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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. diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index 76a3db0..c8b6478 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -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 FirstHistoryFrame = Array.Empty(); 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 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(() => 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(() => vm.Run()); diff --git a/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs b/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs new file mode 100644 index 0000000..1628533 --- /dev/null +++ b/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs @@ -0,0 +1,51 @@ +using Age.Engine.Model; + +namespace Age.Engine.Vm; + +/// +/// 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. +/// +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; + } +} diff --git a/godot/Main.cs b/godot/Main.cs index 66515e9..7564559 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -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) {