From d0b2d31d2ee5ef4e1b0c99a93a0a82bab17a44f2 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 21 Jul 2026 09:48:25 -0400 Subject: [PATCH] Implement ADV layout cursor and bounds --- docs/engine-re.md | 39 ++++++++++ docs/opcode-reference.md | 22 ++++-- docs/phase-b-framework.md | 8 +- .../Age.Engine.Tests/AdvTextHistoryTests.cs | 24 +++++- engine/Age.Engine.Tests/AdvTextOpsTests.cs | 52 +++++++++++++ .../HistoryPresentationOpsTests.cs | 2 +- engine/Age.Engine/Model/AdvTextHistory.cs | 39 ++++++++-- .../Age.Engine/Vm/AdvTextLayoutBootstrap.cs | 78 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 15 +++- godot/GodotAdvHost.cs | 14 ++++ godot/Main.cs | 10 ++- vm-map/opcodes.toml | 44 +++++------ 12 files changed, 301 insertions(+), 46 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index de0ecca..0942df7 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -2227,6 +2227,45 @@ the deliberately deferred shared-profile write `0x1a2`. --- +### ADV layout reset cursor and overflow bounds — `0x79` / `0x1c1` (2026-07-21) + +SYSTEM4 uses these two three-operand operations as a pair while constructing its nine ADV layouts. They +configure persistent layout properties used by later resets; neither operation draws text or moves the +current cursor immediately. + +`op_0x79_set_adv_text_reset_cursor@0x41eb50` reads `(layout_slot, x, y)` and calls +`adv_text_layout_set_reset_cursor@0x44fed0`. Slot zero resolves through the current-layout selector at +manager `+0x4c8`; otherwise the helper selects `manager+0x414[slot]`. It writes `x/y` to layout +`+0x1c/+0x20`. This is distinct from `0x7a`, whose `adv_text_set_cursor@0x4530f0` path modifies the live +cursor record immediately. + +`op_0x1c1_set_adv_text_bounds@0x41f6c0` reads `(layout_slot, right, bottom)` and calls +`adv_text_layout_set_bounds@0x44ff00`, which writes the pair to layout `+0x24/+0x28`. They are absolute +layout-local overflow coordinates. `adv_text_layout_check_overflow@0x45efc0` compares glyph end x/y +against those fields and returns horizontal/vertical overflow bits. When a layout resets, +`adv_text_layout_reset_cursor_and_bounds_record@0x455070` replaces its retained state with a five-dword +record `{0, reset_x, reset_y, right, bottom}` copied from all four configured fields. + +The corpus supplies a complete consistency check: all nine sites for each opcode are in SYSTEM4 and +alternate after layouts 1–9 are defined and initially reset. Layout 1 configures cursor `(100,47)` and +bounds `(720,147)`; layouts 2–6 use `(45,42)` and `(645,135)`; layout 7 uses `(53,10)` and `(495,180)`; +layout 8 uses `(53,10)` and `(495,60)`; layout 9 uses `(10,10)` and `(250,368)`. Layout 1 computes its +bounds as `100+620` and `47+100`, further excluding width/height-delta semantics. + +The port now keeps configured reset cursor and right/bottom bounds in its engine-owned layout state. +`0x70` initializes bounds from width/height, `0x79` changes only the deferred reset cursor, `0x1c1` +changes the bounds, and `0x71` restores the configured cursor while notifying the host of the concrete +selected slot. Retained snapshots carry the boundaries, and ordinary/history Godot labels are positioned +from the layout origin and sized from `right-cursor_x` / `bottom-cursor_y`; the former hardcoded slot-1 +rectangle is gone. The ordinary overlay remains bound to the parent input wait's captured layout while a +nested callback such as HISTORY selects and renders into layouts 2–6; closing the callback therefore +reveals the retained parent text at its original geometry rather than consulting the callback's last +current-layout selection. The direct-scene SYSTEM4 bootstrap also recognizes all nine configuration pairs and +evaluates the two constant `add` expressions used for slot 1 without entering SYSTEM4's control flow. This +is script-owned state, with no boot seed or game-specific coordinates in the runtime. + +--- + ## Native walls backlog (targets for this loop) - ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script ` = raw SYS4INI file index. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 6ccabda..db52f75 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -8,11 +8,13 @@ ### 0x70 `define-adv-text-layout` (define-adv-text-layout, argc 5) - **summary:** (layout_slot)(width)(height)(x)(y) - configure an ADV text layout/surface and, when history recording is enabled, append its logical history index and arm the next retained record as a group start. - **grounding:** source=investigation, confidence=high +- **depended on by:** 0x79, 0x1c1 - **evidence:** Ghidra /v2: op_0x70_handler@0x41e4e0 forwards the five operands and ctx+0x55110 to adv_text_layout_define@0x4550e0. The worker stores width/height/origin, creates or resizes surface slot layout+0x14, appends {layout_slot,current_record_count} to the 8-byte history index unless suppression bit31 is set, and arms manager+0xd40[slot] so the next 0x48-byte record receives group-start flag bit0. ### 0x71 `reset-adv-text-layout` (reset-adv-text-layout, argc 1) - **summary:** (layout_slot) - clear/reset an ADV text layout, append a retained-history boundary when recording is enabled, snapshot the current code/text position, and commit pending ReadTextDB records. T1 entries target these structural reset sites. - **grounding:** source=investigation, confidence=high +- **depended on by:** 0x79, 0x1c1 - **evidence:** Corpus: T1 targets op-0x71 records, but the operand is a layout slot (SC0000 uses 1; HISTORY computes 2..6), not an anchor id. Ghidra /v2: op_0x71_handler@0x41e540 calls adv_text_layout_reset@0x455210 with ctx+0x55110; the worker clears the selected layout and appends {slot,current_record_count}/arms group-start unless suppressed. The handler also snapshots (frame_pc-frame_codebase)/4 and text state and calls read_text_db_commit_pending@0x46ae20. ### 0x72 `wait-for-input` (wait-for-input, argc 1) @@ -45,6 +47,12 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x78_handler@0x41eb10 writes operand 1 to text-manager+0x558 and rebuilds font state. Direct draw and text_history_render_records branch on this field (including a distinct mode-3 offset path). HISTORY.BIN selects mode 3 and restores mode 3 with the normal ADV font preset. +### 0x79 `set-adv-text-reset-cursor` (set-adv-text-reset-cursor, argc 3) +- **summary:** (layout_slot)(x)(y) - configure the cursor coordinates restored by a later reset of the selected ADV text layout. Slot 0 selects the current layout. This does not move the live cursor immediately; opcode 0x7a does that. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x70, 0x71 +- **evidence:** Ghidra /v2: op_0x79_set_adv_text_reset_cursor@0x41eb50 reads three operands and calls adv_text_layout_set_reset_cursor@0x44fed0. Slot 0 resolves through the manager current-layout field at +0x4c8; the selected layout pointer is manager+0x414[slot], and x/y are stored at layout +0x1c/+0x20. adv_text_layout_reset_cursor_and_bounds_record@0x455070 later copies those configured values into the initial retained record on layout reset. SYSTEM4 has all nine corpus sites, paired with 0x1c1 after defining/resetting layouts 1..9; slot 1 configures (100,47), layouts 2..6 configure (45,42), layouts 7/8 (53,10), and layout 9 (10,10). Port implementation stores the configured cursor separately from the live cursor, restores it on 0x71, and replays the SYSTEM4 configuration in the direct-scene bootstrap. + ### 0x7a `set-adv-text-cursor` (set-adv-text-cursor, argc 3) - **summary:** (layout_slot)(x)(y) - set the cursor in the selected ADV text layout's last 20-byte record. Slot 0 selects the current layout. - **grounding:** source=investigation, confidence=high @@ -80,6 +88,12 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x1bb_handler@0x41f650 writes 0x80000000 to ctx+0x55110 for operand 0 and zero for operand 1, rejecting other values. Text/layout/metadata/voice paths test or propagate that high bit before appending retained history. HISTORY.BIN disables recording at entry and reenables it at exit so the backlog UI does not record itself. +### 0x1c1 `set-adv-text-bounds` (set-adv-text-bounds, argc 3) +- **summary:** (layout_slot)(right)(bottom) - configure the selected ADV text layout's right and bottom overflow boundaries in layout-local coordinates. Slot 0 selects the current layout. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x70, 0x71 +- **evidence:** Ghidra /v2: op_0x1c1_set_adv_text_bounds@0x41f6c0 reads three operands and calls adv_text_layout_set_bounds@0x44ff00. That helper stores right/bottom at layout +0x24/+0x28; adv_text_layout_reset_cursor_and_bounds_record@0x455070 copies them into the initial retained record, and adv_text_layout_check_overflow@0x45efc0 compares glyph end coordinates against them. SYSTEM4 has all nine sites, paired with 0x79. Slot 1 computes (720,147), layouts 2..6 use (645,135), layout 7 (495,180), layout 8 (495,60), and layout 9 (250,368), proving these are layout-local boundary coordinates rather than width/height deltas. Port implementation retains the configured bounds in engine-owned layout snapshots and sizes ordinary/history Godot labels from right-cursor_x and bottom-cursor_y. + ### 0x1ce `set-adv-wait-indicator-enabled` (u0041B9F0, argc 1) - **summary:** (enabled) - explicitly start or stop the animated ADV input-wait indicator service. - **grounding:** source=investigation, confidence=high @@ -931,10 +945,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x79 `u0041AD30` (u0041AD30, argc 3) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x7f `u00414C60` (u00414C60, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -1151,10 +1161,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x1c1 `u0041B820` (u0041B820, argc 3) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1c8 `toString` (toString, argc 2) - **summary:** — - **grounding:** source=kelebek, confidence=med diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index ec5307f..c3c4a2f 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -280,8 +280,12 @@ GAMESTART profile operations still deliberately deferred. The reached `0x63`/`0x also implemented: native `0x63` aliases a typed backing-cell address into a pointer, while `0x1b0` copies a counted dword span through direct or pointer endpoints. The traced natural boot reaches both without fallback; UNITECH is now 31/31 handled and CALCCC 14/15, with only deferred profile op `0x1a2` remaining. -The next reached effectful cluster to investigate is SYSTEM4's paired `0x79`/`0x1c1` setup. Closing Godot -currently releases a parked ADV wait before process teardown, so the page map may contain one trailing +SYSTEM4's paired `0x79`/`0x1c1` setup is now natively resolved and implemented: `0x79` configures the +cursor restored by later layout resets, while `0x1c1` configures layout-local right/bottom overflow +boundaries. `AdvTextHistory`, VM dispatch, the direct-scene SYSTEM4 bootstrap, and ordinary/history Godot +label geometry now share that script-owned state; the former slot-1 hardcoding is removed. The next +natural Game Start diagnostic should identify the next reached effectful fallback. Closing Godot currently +releases a parked ADV wait before process teardown, so the page map may contain one trailing shutdown-only page; the final timeline `input-wait` is the authoritative stop. ## Stage B2 — Faithful full boot diff --git a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs index aa1208b..dddefd8 100644 --- a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs +++ b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs @@ -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] diff --git a/engine/Age.Engine.Tests/AdvTextOpsTests.cs b/engine/Age.Engine.Tests/AdvTextOpsTests.cs index 36ac045..62e74f9 100644 --- a/engine/Age.Engine.Tests/AdvTextOpsTests.cs +++ b/engine/Age.Engine.Tests/AdvTextOpsTests.cs @@ -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()), + }, Array.Empty()); + 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() { diff --git a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs index 6e67e44..75f8807 100644 --- a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs @@ -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)); diff --git a/engine/Age.Engine/Model/AdvTextHistory.cs b/engine/Age.Engine/Model/AdvTextHistory.cs index 201437f..4d7430f 100644 --- a/engine/Age.Engine/Model/AdvTextHistory.cs +++ b/engine/Age.Engine/Model/AdvTextHistory.cs @@ -40,7 +40,9 @@ public readonly record struct AdvTextLayoutSnapshot( int OriginX, int OriginY, int CursorX, - int CursorY); + int CursorY, + int Right, + int Bottom); /// /// 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 _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)); } } diff --git a/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs b/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs index 1628533..7d14cb4 100644 --- a/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs +++ b/engine/Age.Engine/Vm/AdvTextLayoutBootstrap.cs @@ -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(); + 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 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 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; + } } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 91791f7..1be13bd 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -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]), diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index b7b95a3..81604a1 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -159,6 +159,20 @@ public sealed class GodotAdvHost : IHost } } + /// + /// Layout that owns the ordinary ADV overlay. Nested callback scripts such as HISTORY can select and + /// mutate other layouts while the parent wait remains parked; those transient selections must not move + /// the parent page when its overlay becomes visible again. + /// + public int AdvPageLayoutSlot + { + get + { + lock (_textLock) + return IsWaiting && _activeWaitLayout != 0 ? _activeWaitLayout : _currentAdvLayout; + } + } + public IReadOnlyList SnapshotSurfaceText(int surfaceSlot) { lock (_textLock) diff --git a/godot/Main.cs b/godot/Main.cs index 20faf7f..3f3943f 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -714,8 +714,10 @@ public partial class Main : Godot.Control _text.Visible = !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive; if (!_text.Visible) return; var t = _host.SnapshotAdvText(); - _text.Position = new Vector2(t.X, 430 + t.Y); - _text.Size = new Vector2(System.Math.Max(1, 720 - t.X), System.Math.Max(1, 147 - t.Y)); + var layout = _vm.TextHistory.GetLayoutSnapshot(_host.AdvPageLayoutSlot); + _text.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY); + _text.Size = new Vector2(System.Math.Max(1, layout.Right - layout.CursorX), + System.Math.Max(1, layout.Bottom - layout.CursorY)); int count = System.Math.Clamp(t.VisibleGlyphs, 0, t.Text.Length); _text.Text = count == 0 ? "" : t.Text[..count]; } @@ -733,8 +735,8 @@ public partial class Main : Godot.Control } var layout = batch.Layout; label.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY); - label.Size = new Vector2(System.Math.Max(1, layout.Width - layout.CursorX), - System.Math.Max(1, layout.Height - layout.CursorY)); + label.Size = new Vector2(System.Math.Max(1, layout.Right - layout.CursorX), + System.Math.Max(1, layout.Bottom - layout.CursorY)); label.Text = batch.Text; ApplyAdvTextStyle(label, batch.Style); label.Visible = true; diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 6dc1463..a52df84 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1182,33 +1182,33 @@ observed_types = ["imm"] [[opcode]] op = 0x79 -label = "u0041AD30" +label = "set-adv-text-reset-cursor" argc = 3 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041AD30" -category = "unknown" -summary = "" +name = "set-adv-text-reset-cursor" +category = "adv" +summary = "(layout_slot)(x)(y) - configure the cursor coordinates restored by a later reset of the selected ADV text layout. Slot 0 selects the current layout. This does not move the live cursor immediately; opcode 0x7a does that." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "" +source = "investigation" +confidence = "high" +depends_on = [0x70, 0x71] +evidence = "Ghidra /v2: op_0x79_set_adv_text_reset_cursor@0x41eb50 reads three operands and calls adv_text_layout_set_reset_cursor@0x44fed0. Slot 0 resolves through the manager current-layout field at +0x4c8; the selected layout pointer is manager+0x414[slot], and x/y are stored at layout +0x1c/+0x20. adv_text_layout_reset_cursor_and_bounds_record@0x455070 later copies those configured values into the initial retained record on layout reset. SYSTEM4 has all nine corpus sites, paired with 0x1c1 after defining/resetting layouts 1..9; slot 1 configures (100,47), layouts 2..6 configure (45,42), layouts 7/8 (53,10), and layout 9 (10,10). Port implementation stores the configured cursor separately from the live cursor, restores it on 0x71, and replays the SYSTEM4 configuration in the direct-scene bootstrap." [[opcode.semantics.args]] i = 1 -role = "" +role = "layout slot (0 = current)" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "" +role = "reset cursor x" observed_types = ["imm"] [[opcode.semantics.args]] i = 3 -role = "" +role = "reset cursor y" observed_types = ["imm"] [[opcode]] @@ -4092,33 +4092,33 @@ evidence = "" [[opcode]] op = 0x1c1 -label = "u0041B820" +label = "set-adv-text-bounds" argc = 3 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041B820" -category = "unknown" -summary = "" +name = "set-adv-text-bounds" +category = "adv" +summary = "(layout_slot)(right)(bottom) - configure the selected ADV text layout's right and bottom overflow boundaries in layout-local coordinates. Slot 0 selects the current layout." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "" +source = "investigation" +confidence = "high" +depends_on = [0x70, 0x71] +evidence = "Ghidra /v2: op_0x1c1_set_adv_text_bounds@0x41f6c0 reads three operands and calls adv_text_layout_set_bounds@0x44ff00. That helper stores right/bottom at layout +0x24/+0x28; adv_text_layout_reset_cursor_and_bounds_record@0x455070 copies them into the initial retained record, and adv_text_layout_check_overflow@0x45efc0 compares glyph end coordinates against them. SYSTEM4 has all nine sites, paired with 0x79. Slot 1 computes (720,147), layouts 2..6 use (645,135), layout 7 (495,180), layout 8 (495,60), and layout 9 (250,368), proving these are layout-local boundary coordinates rather than width/height deltas. Port implementation retains the configured bounds in engine-owned layout snapshots and sizes ordinary/history Godot labels from right-cursor_x and bottom-cursor_y." [[opcode.semantics.args]] i = 1 -role = "" +role = "layout slot (0 = current)" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "" +role = "right overflow boundary" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 3 -role = "" +role = "bottom overflow boundary" observed_types = ["imm", "l-int"] [[opcode]]