diff --git a/docs/engine-re.md b/docs/engine-re.md index 052c108..e96f711 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1393,6 +1393,12 @@ Label and independently animated wait marker. Native rendering gets this layerin 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 target-layout render batches are likewise presentation bindings, not part of the retained backlog. +`HISTORY.BIN@0x1100` re-enables recording with op `0x1bb(1)` after erasing its object range and releasing +surfaces `0xc0/0xc1`; that existing exit boundary now tells the host to discard all bound History batches. +The semantic text/index records remain untouched, so reopening History rebuilds fresh rows while exiting +cannot leave the old labels above the resumed ADV page. + 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 b0dcba7..4310d32 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1801,6 +1801,12 @@ 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. + +A follow-up exit check found that the host retained the last History render batches after the script had +erased its objects and re-enabled recording. Op `0x1bb(1)` at `HISTORY.BIN@0x1100` is now the presentation +end boundary: it clears only the host's transient layout bindings, leaving the engine-owned backlog intact +for the next opening. The real interaction regression requires zero active History batches after return. + 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 diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index c8b6478..79e36c0 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -167,6 +167,8 @@ public class HistoryInteractionOpsTests Assert.True(host.SawRenderedText); Assert.True(host.HistoryReturned); Assert.Equal(1, host.Waits); // the enclosing ADV page was never released or re-entered + Assert.Equal(1, host.HistoryPresentationEnds); + Assert.Empty(host.ActiveHistoryRenders); var historyButtons = host.FirstHistoryFrame .Where(render => render.Handle >= 0xd2fa && render.Handle <= 0xd300) diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index cd50526..c7e02e9 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -18,6 +18,8 @@ internal class RecordingHost : IHost public readonly List<(int Slot, int X, int Y)> TextCursors = new(); public readonly List<(int Surface, int X, int Y, string Text)> SurfaceStrings = new(); public readonly List HistoryRenders = new(); + public readonly Dictionary ActiveHistoryRenders = new(); + public int HistoryPresentationEnds; public readonly List ClearedTextLayouts = new(); public readonly List SurfaceFills = new(); public readonly List<(long First, long Count)> PresentedRanges = new(); @@ -38,8 +40,21 @@ internal class RecordingHost : IHost => SurfaceStrings.Add((surfaceSlot, x, y, text)); public void DrawStringToSurface(int surfaceSlot, int x, int y, string text, AdvTextStyle style) => SurfaceStrings.Add((surfaceSlot, x, y, text)); - public void ClearRenderedAdvTextLayout(int layoutSlot) => ClearedTextLayouts.Add(layoutSlot); - public void RenderTextHistory(AdvTextHistoryRenderBatch batch) => HistoryRenders.Add(batch); + public void ClearRenderedAdvTextLayout(int layoutSlot) + { + ClearedTextLayouts.Add(layoutSlot); + ActiveHistoryRenders.Remove(layoutSlot); + } + public void RenderTextHistory(AdvTextHistoryRenderBatch batch) + { + HistoryRenders.Add(batch); + ActiveHistoryRenders[batch.LayoutSlot] = batch; + } + public void EndTextHistoryPresentation() + { + HistoryPresentationEnds++; + ActiveHistoryRenders.Clear(); + } public int MessageWindowAlphaSetting { get; set; } public void FillSurfaceRect(SurfaceRectFill fill) => SurfaceFills.Add(fill); public void PresentObjectRange(GfxState gfx, long firstHandle, long count) diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index 3d07bc5..7ca6699 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -24,6 +24,9 @@ public interface IHost => DrawStringToSurface(surfaceSlot, x, y, text); void ClearRenderedAdvTextLayout(int layoutSlot) { } void RenderTextHistory(AdvTextHistoryRenderBatch batch) { } + // History render batches are transient bindings, unlike the retained backlog itself. HISTORY.BIN's + // recording re-enable at exit ends that presentation and drops every bound target layout. + void EndTextHistoryPresentation() { } int MessageWindowAlphaSetting => 0; void FillSurfaceRect(SurfaceRectFill fill) { } void PresentObjectRange(GfxState gfx, long firstHandle, long count) { } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index f5b60f8..f6d38b9 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -772,7 +772,12 @@ public sealed class VirtualMachine case "reset-message-voice-state": // 0x1bc resets native per-message voice/queued-voice state _autoVoicePending = false; return pc + 1; case "set-text-history-recording": // 0x1bb: HISTORY.BIN suppresses recording its own UI - if (Read(a[0]) is 0 or 1) TextHistory.SetRecordingEnabled(Read(a[0]) == 1); + if (Read(a[0]) is 0 or 1) + { + bool enabled = Read(a[0]) == 1; + TextHistory.SetRecordingEnabled(enabled); + if (enabled) _host.EndTextHistoryPresentation(); + } return pc + 1; case "append-text-history-metadata": // 0x1d2: typed value attached to the current group TextHistory.AppendMetadata(Read(a[0]), Read(a[1]), _advTextStyle); return pc + 1; diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index f003574..68b291f 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -149,6 +149,12 @@ public sealed class GodotAdvHost : IHost }); } + public void EndTextHistoryPresentation() + { + lock (_textLock) _historyText.Clear(); + _timeline?.Event("history-presentation-end"); + } + public IReadOnlyList SnapshotRenderedTextHistory() { lock (_textLock) return _historyText.Values.OrderBy(batch => batch.LayoutSlot).ToArray();