Implement ADV layout cursor and bounds

This commit is contained in:
gamer147
2026-07-21 09:48:25 -04:00
parent e07458c2fa
commit 6243ceff64
12 changed files with 301 additions and 46 deletions

View File

@@ -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 19 are defined and initially reset. Layout 1 configures cursor `(100,47)` and
bounds `(720,147)`; layouts 26 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 26; 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) ## Native walls backlog (targets for this loop)
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index. - ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index.

View File

@@ -8,11 +8,13 @@
### 0x70 `define-adv-text-layout` (define-adv-text-layout, argc 5) ### 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. - **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 - **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. - **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) ### 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. - **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 - **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. - **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) ### 0x72 `wait-for-input` (wait-for-input, argc 1)
@@ -45,6 +47,12 @@
- **grounding:** source=investigation, confidence=high - **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. - **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) ### 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. - **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 - **grounding:** source=investigation, confidence=high
@@ -80,6 +88,12 @@
- **grounding:** source=investigation, confidence=high - **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. - **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) ### 0x1ce `set-adv-wait-indicator-enabled` (u0041B9F0, argc 1)
- **summary:** (enabled) - explicitly start or stop the animated ADV input-wait indicator service. - **summary:** (enabled) - explicitly start or stop the animated ADV input-wait indicator service.
- **grounding:** source=investigation, confidence=high - **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:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
### 0x79 `u0041AD30` (u0041AD30, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x7f `u00414C60` (u00414C60, argc 1) ### 0x7f `u00414C60` (u00414C60, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **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:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0x1c1 `u0041B820` (u0041B820, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1c8 `toString` (toString, argc 2) ### 0x1c8 `toString` (toString, argc 2)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med

View File

@@ -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 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 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. 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 SYSTEM4's paired `0x79`/`0x1c1` setup is now natively resolved and implemented: `0x79` configures the
currently releases a parked ADV wait before process teardown, so the page map may contain one trailing 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. shutdown-only page; the final timeline `input-wait` is the authoritative stop.
## Stage B2 — Faithful full boot ## Stage B2 — Faithful full boot

View File

@@ -45,7 +45,7 @@ public class AdvTextHistoryTests
Assert.Equal(123, metadata.Value); Assert.Equal(123, metadata.Value);
Assert.Equal(1, metadata.AuxValue); Assert.Equal(1, metadata.AuxValue);
Assert.Equal((24, 0xf0e0d0L), (metadata.Style.PrimaryFontSize, metadata.Style.TextColor)); 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 => voice =>
{ {
@@ -109,7 +109,27 @@ public class AdvTextHistoryTests
var record = Assert.Single(history.Records); var record = Assert.Single(history.Records);
Assert.Equal("new", record.Text); Assert.Equal("new", record.Text);
Assert.False(record.Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart)); 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] [Fact]

View File

@@ -31,6 +31,58 @@ public class AdvTextOpsTests
Assert.Equal((13, 1, 1, "speaker"), Assert.Single(host.SurfaceStrings)); 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] [Fact]
public void WaitIndicatorConfigurationReachesHost() public void WaitIndicatorConfigurationReachesHost()
{ {

View File

@@ -41,7 +41,7 @@ public class HistoryPresentationOpsTests
var render = Assert.Single(host.HistoryRenders); var render = Assert.Single(host.HistoryRenders);
Assert.Equal((4, 0, "retained dialogue"), Assert.Equal((4, 0, "retained dialogue"),
(render.LayoutSlot, render.FirstRecordIndex, render.Text)); (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), Assert.Equal((24, 0xffffffL, 0x606060L, 8),
(render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor, (render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor,
render.Style.LineSpacing)); render.Style.LineSpacing));

View File

@@ -40,7 +40,9 @@ public readonly record struct AdvTextLayoutSnapshot(
int OriginX, int OriginX,
int OriginY, int OriginY,
int CursorX, int CursorX,
int CursorY); int CursorY,
int Right,
int Bottom);
/// <summary> /// <summary>
/// One semantic counterpart of AGE's 0x48-byte retained text record. Metadata uses /// 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 Height;
public int OriginX; public int OriginX;
public int OriginY; public int OriginY;
public int ResetCursorX;
public int ResetCursorY;
public int CursorX; public int CursorX;
public int CursorY; public int CursorY;
public int Right;
public int Bottom;
} }
private readonly List<AdvTextHistoryRecord> _records = new(); private readonly List<AdvTextHistoryRecord> _records = new();
@@ -105,6 +111,8 @@ public sealed class AdvTextHistory
layout.Height = height; layout.Height = height;
layout.OriginX = originX; layout.OriginX = originX;
layout.OriginY = originY; layout.OriginY = originY;
layout.Right = width;
layout.Bottom = height;
AppendBoundary(slot); AppendBoundary(slot);
} }
@@ -112,11 +120,27 @@ public sealed class AdvTextHistory
{ {
int slot = SelectLayout(requestedSlot); int slot = SelectLayout(requestedSlot);
var layout = GetOrCreateLayout(slot); var layout = GetOrCreateLayout(slot);
layout.CursorX = 0; layout.CursorX = layout.ResetCursorX;
layout.CursorY = 0; layout.CursorY = layout.ResetCursorY;
AppendBoundary(slot); 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) public void SetCursor(int requestedSlot, int x, int y)
{ {
int slot = ResolveLayout(requestedSlot); int slot = ResolveLayout(requestedSlot);
@@ -133,6 +157,9 @@ public sealed class AdvTextHistory
layout.OriginY = y; layout.OriginY = y;
} }
public AdvTextLayoutSnapshot GetLayoutSnapshot(int requestedSlot)
=> SnapshotLayout(ResolveLayout(requestedSlot));
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style, public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style,
AdvTextHistoryRecordFlags flags = AdvTextHistoryRecordFlags.None) AdvTextHistoryRecordFlags flags = AdvTextHistoryRecordFlags.None)
{ {
@@ -291,7 +318,8 @@ public sealed class AdvTextHistory
{ {
var layout = GetOrCreateLayout(slot); var layout = GetOrCreateLayout(slot);
return new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height, 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) private void AppendBoundary(int slot)
@@ -325,7 +353,8 @@ public sealed class AdvTextHistory
var layout = GetOrCreateLayout(slot); var layout = GetOrCreateLayout(slot);
var snapshot = new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height, 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)); _records.Add(new AdvTextHistoryRecord(kind, flags, snapshot, style, text, value, auxValue, sourceOffset));
} }
} }

View File

@@ -14,6 +14,9 @@ public static class AdvTextLayoutBootstrap
{ {
int? defineOpcode = table.ByLabel("define-adv-text-layout"); int? defineOpcode = table.ByLabel("define-adv-text-layout");
int? resetOpcode = table.ByLabel("reset-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; if (defineOpcode == null || resetOpcode == null) return 0;
bool foundDefinition = false; bool foundDefinition = false;
@@ -46,6 +49,81 @@ public static class AdvTextLayoutBootstrap
} }
break; 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; 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;
}
} }

View File

@@ -827,12 +827,23 @@ public sealed class VirtualMachine
(int)Read(a[3]), (int)Read(a[4])); (int)Read(a[3]), (int)Read(a[4]));
return pc + 1; return pc + 1;
case "reset-adv-text-layout": // 0x71: reset layout and begin the next logical retained group 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; return pc + 1;
case "set-adv-text-cursor": // 0x7a (layout slot, x, y); slot 0 means current natively 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])); 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; _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 case "configure-adv-wait-indicator": // 0x73: per-layout animated input-wait marker
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig( _host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
(int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),

View File

@@ -159,6 +159,20 @@ public sealed class GodotAdvHost : IHost
} }
} }
/// <summary>
/// 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.
/// </summary>
public int AdvPageLayoutSlot
{
get
{
lock (_textLock)
return IsWaiting && _activeWaitLayout != 0 ? _activeWaitLayout : _currentAdvLayout;
}
}
public IReadOnlyList<SurfaceTextDraw> SnapshotSurfaceText(int surfaceSlot) public IReadOnlyList<SurfaceTextDraw> SnapshotSurfaceText(int surfaceSlot)
{ {
lock (_textLock) lock (_textLock)

View File

@@ -714,8 +714,10 @@ public partial class Main : Godot.Control
_text.Visible = !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive; _text.Visible = !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive;
if (!_text.Visible) return; if (!_text.Visible) return;
var t = _host.SnapshotAdvText(); var t = _host.SnapshotAdvText();
_text.Position = new Vector2(t.X, 430 + t.Y); var layout = _vm.TextHistory.GetLayoutSnapshot(_host.AdvPageLayoutSlot);
_text.Size = new Vector2(System.Math.Max(1, 720 - t.X), System.Math.Max(1, 147 - t.Y)); _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); int count = System.Math.Clamp(t.VisibleGlyphs, 0, t.Text.Length);
_text.Text = count == 0 ? "" : t.Text[..count]; _text.Text = count == 0 ? "" : t.Text[..count];
} }
@@ -733,8 +735,8 @@ public partial class Main : Godot.Control
} }
var layout = batch.Layout; var layout = batch.Layout;
label.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY); label.Position = new Vector2(layout.OriginX + layout.CursorX, layout.OriginY + layout.CursorY);
label.Size = new Vector2(System.Math.Max(1, layout.Width - layout.CursorX), label.Size = new Vector2(System.Math.Max(1, layout.Right - layout.CursorX),
System.Math.Max(1, layout.Height - layout.CursorY)); System.Math.Max(1, layout.Bottom - layout.CursorY));
label.Text = batch.Text; label.Text = batch.Text;
ApplyAdvTextStyle(label, batch.Style); ApplyAdvTextStyle(label, batch.Style);
label.Visible = true; label.Visible = true;

View File

@@ -1182,33 +1182,33 @@ observed_types = ["imm"]
[[opcode]] [[opcode]]
op = 0x79 op = 0x79
label = "u0041AD30" label = "set-adv-text-reset-cursor"
argc = 3 argc = 3
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041AD30" name = "set-adv-text-reset-cursor"
category = "unknown" category = "adv"
summary = "" 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 noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = [0x70, 0x71]
evidence = "" 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]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "layout slot (0 = current)"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "reset cursor x"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "reset cursor y"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]
@@ -4092,33 +4092,33 @@ evidence = ""
[[opcode]] [[opcode]]
op = 0x1c1 op = 0x1c1
label = "u0041B820" label = "set-adv-text-bounds"
argc = 3 argc = 3
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B820" name = "set-adv-text-bounds"
category = "unknown" category = "adv"
summary = "" 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 noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = [0x70, 0x71]
evidence = "" 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]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "layout slot (0 = current)"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "right overflow boundary"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "bottom overflow boundary"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode]] [[opcode]]