Implement AGE text line spacing

This commit is contained in:
gamer147
2026-07-19 23:32:38 -04:00
parent b8928ec7b8
commit e0aae05f32
11 changed files with 90 additions and 17 deletions

View File

@@ -16,6 +16,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0xb558` | `gfx_dirty_a` | `int` | gfx dirty flag (anim set raises) | | `0xb558` | `gfx_dirty_a` | `int` | gfx dirty flag (anim set raises) |
| `0xb560` | `gfx_dirty_b` | `int` | gfx dirty flag | | `0xb560` | `gfx_dirty_b` | `int` | gfx dirty flag |
| `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) | | `0x14d54` | `gfx_obj_ptr_table` | `void*` | per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c) |
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
| `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) | | `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) |
| `0x46d14` | `query_table_46d14` | `void*` | stride-0x14 table read by op 0x216 | | `0x46d14` | `query_table_46d14` | `void*` | stride-0x14 table read by op 0x216 |
| `0x51b64` | `frame_timer` | `int` | frame timer (present updates 0x51b64/0x51b68) | | `0x51b64` | `frame_timer` | `int` | frame timer (present updates 0x51b64/0x51b68) |

View File

@@ -1700,6 +1700,31 @@ inventing unsigned behavior. `/v2` names/comments this mount/load/open chain and
--- ---
### ADV text line spacing -- opcode `0x8b` (2026-07-19)
`op_0x8b_set_text_line_spacing@0x41f270` stores its single operand at ADV text-manager offset
`+0x560`, or `EngineCtx.text_line_spacing` (`ctx+0x14ea0`). This is pixel leading, not a font face,
weight, or effect selector. `adv_text_manager_initialize@0x456800` gives it a native default of 6.
The horizontal line-break path (`adv_text_append_line_break_horizontal@0x456fd0`) and retained History
renderer (`text_history_render_records@0x452970`) pass `manager+0x560 - manager+0x4cc` as the line
advance. Offset `+0x4cc` is the primary `LOGFONT.lfHeight`, which AGE stores as a negative pixel height,
so the effective pitch is `font pixel height + text_line_spacing`. The corresponding vertical-writing
path uses the same pitch to move to the next column. Corpus values support that interpretation: scripts
pair 8 pixels with 22/24-pixel Mincho text and 9 pixels with 16-pixel Gothic text.
History records do not snapshot the `+0x560` field. `text_history_append_text_record@0x456000` retains
font, color, effect, geometry, flags, and string state, while `text_history_render_records` reads the
manager's current line spacing when it encounters a retained line-break marker. The port therefore keeps
line spacing in the current ADV style manager and overrides a History render batch with that current value;
the individual record continues to supply its retained font/color/effect fields.
The `/v2` image names/comments the handler, manager initializer, horizontal/vertical line-break paths, and
their low-level cursor-advance helpers. The regenerated 58-field `EngineCtx` is applied and the program is
saved.
---
## 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

@@ -55,6 +55,11 @@
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x85_handler@0x4163d0 calls text_history_clear@0x455d70 on the text manager. The worker destroys the 0x48-byte record vector and clears the 8-byte logical index vector. Corpus: two sites in each of 143 normal ADV scripts bracket the retained backlog lifetime. - **evidence:** Ghidra /v2: op_0x85_handler@0x4163d0 calls text_history_clear@0x455d70 on the text manager. The worker destroys the 0x48-byte record vector and clears the 8-byte logical index vector. Corpus: two sites in each of 143 normal ADV scripts bracket the retained backlog lifetime.
### 0x8b `set-text-line-spacing` (u0041B3D0, argc 1)
- **summary:** (pixels) - set the extra pixel spacing added between text lines.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x8b_set_text_line_spacing@0x41f270 stores operand 1 at text manager +0x560 (ctx+0x14ea0). adv_text_manager_initialize@0x456800 defaults it to 6. Horizontal newline/history paths at 0x456fd0/0x452970 advance y by (+0x560 - primary LOGFONT.lfHeight); lfHeight is negative, so this is font pixel height plus configured leading. Corpus arguments are 8 for 22/24px Mincho blocks and 9 for 16px Gothic blocks.
### 0x197 `set-ruby-font-size` (set-ruby-font-size, argc 1) ### 0x197 `set-ruby-font-size` (set-ruby-font-size, argc 1)
- **summary:** (pixels) - set the secondary/ruby text font height and rebuild its native rasterization state. - **summary:** (pixels) - set the secondary/ruby text font height and rebuild its native rasterization state.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
@@ -815,10 +820,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
### 0x8b `u0041B3D0` (u0041B3D0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x8c `jmp` (jmp, argc 1) ### 0x8c `jmp` (jmp, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med

View File

@@ -1899,3 +1899,30 @@ gap. Keep `0x1cb` deferred, and treat `0x1ce`/`0x20a` together as their own spri
Validation: all 205 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the Validation: all 205 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
Godot build has zero warnings, and threaded `SELFTEST OK`. Godot build has zero warnings, and threaded `SELFTEST OK`.
### ADV History text line spacing implemented (2026-07-19)
Native RE resolves the two remaining `0x8b` calls as text-manager line leading. The handler writes its
single pixel count to manager offset `+0x560` (`EngineCtx+0x14ea0`), whose initializer default is 6.
Horizontal newline and History rendering add that value to the primary font's positive pixel height; the
script's concrete style blocks use 8 pixels with their 22/24-pixel Mincho fonts and 9 with a 16-pixel
Gothic font.
The VM now retains that current style property, Godot applies it as the Label `line_spacing` theme
constant, and History rendering matches native ownership: line spacing comes from the text manager's
current state rather than from the retained record. A focused regression distinguishes a record retained
with spacing 9 from the current History spacing 8, and the real HISTORY route requires all non-empty rows
to render with its setup value of 8.
The canonical opcode and EngineCtx sources are regenerated. The `/v2` handler and all supporting
initializer/line-advance functions are named and commented, the expanded 58-field `EngineCtx` is reapplied,
and the program is saved. HISTORY is now 75/78 distinct opcodes and 851/854 instructions handled or
safe-noop. The remaining instructions are paired sprite-animation service ops `0x1ce`/`0x20a` and the
deferred Read-message Skip setting getter `0x1cb`.
**Next:** investigate and implement `0x1ce`/`0x20a` together as the final non-persistence History support
slice. Keep `0x1cb` deferred until the global save/profile storage boundary is chosen.
Validation: all 205 engine tests pass, including four focused History presentation cases; opcode and
EngineCtx tests/lints are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded
`SELFTEST OK`.

View File

@@ -17,7 +17,7 @@ public class HistoryPresentationOpsTests
history.DefineLayout(1, 640, 160, 80, 430); history.DefineLayout(1, 640, 160, 80, 430);
history.AppendMetadata(9, 2, AdvTextStyle.Default); history.AppendMetadata(9, 2, AdvTextStyle.Default);
history.AppendText(0, 10, "retained dialogue", new AdvTextStyle(24, 8, false, history.AppendText(0, 10, "retained dialogue", new AdvTextStyle(24, 8, false,
0xffffff, 0x606060, 3, 1, 1)); 0xffffff, 0x606060, 3, 1, 1, 9));
history.ResetLayout(1); history.ResetLayout(1);
history.AppendText(0, 11, "next group", AdvTextStyle.Default); history.AppendText(0, 11, "next group", AdvTextStyle.Default);
history.SetRecordingEnabled(false); history.SetRecordingEnabled(false);
@@ -29,6 +29,7 @@ public class HistoryPresentationOpsTests
{ {
(0x198, new[] { I(4), I(65), I(150) }), (0x198, new[] { I(4), I(65), I(150) }),
(0x7a, new[] { I(4), I(45), I(42) }), (0x7a, new[] { I(4), I(45), I(42) }),
(0x8b, new[] { I(8) }),
(0x1d1, new[] { I(4), I(0), I(0), I(0), I(0) }), (0x1d1, new[] { I(4), I(0), I(0), I(0), I(0) }),
(0x2, Array.Empty<Operand>()), (0x2, Array.Empty<Operand>()),
}, Array.Empty<string>()); }, Array.Empty<string>());
@@ -41,8 +42,9 @@ public class HistoryPresentationOpsTests
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), render.Layout);
Assert.Equal((24, 0xffffffL, 0x606060L), 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));
} }
[Fact] [Fact]
@@ -97,6 +99,8 @@ public class HistoryPresentationOpsTests
Assert.NotEqual("step-cap", result.Halt); Assert.NotEqual("step-cap", result.Halt);
Assert.Contains(host.HistoryRenders, render => render.Text.Length > 0); Assert.Contains(host.HistoryRenders, render => render.Text.Length > 0);
Assert.All(host.HistoryRenders.Where(render => render.Text.Length > 0),
render => Assert.Equal(8, render.Style.LineSpacing));
Assert.Contains(host.PresentedRanges, range => range == (0L, 60000L)); Assert.Contains(host.PresentedRanges, range => range == (0L, 60000L));
} }
} }

View File

@@ -26,9 +26,10 @@ public readonly record struct AdvTextStyle(
long EffectColor, long EffectColor,
int RenderMode, int RenderMode,
int EffectOffsetX, int EffectOffsetX,
int EffectOffsetY) int EffectOffsetY,
int LineSpacing)
{ {
public static AdvTextStyle Default => new(0, 0, false, 0, 0, 0, 0, 0); public static AdvTextStyle Default => new(0, 0, false, 0, 0, 0, 0, 0, 6);
} }
/// <summary>A stable snapshot of the layout state associated with a retained record.</summary> /// <summary>A stable snapshot of the layout state associated with a retained record.</summary>

View File

@@ -868,7 +868,11 @@ public sealed class VirtualMachine
int flags = (int)Read(a[2]); int flags = (int)Read(a[2]);
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch( if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch)) (int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
_host.RenderTextHistory(batch); _host.RenderTextHistory(batch with
{
// Native History uses the text manager's current leading, not a retained-record field.
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
});
return pc + 1; return pc + 1;
} }
case "u0041BB90": case "u0041BB90":
@@ -891,6 +895,9 @@ public sealed class VirtualMachine
_advTextStyle = _advTextStyle with { PrimaryFontSize = (int)Read(a[0]) }; return pc + 1; _advTextStyle = _advTextStyle with { PrimaryFontSize = (int)Read(a[0]) }; return pc + 1;
case "set-ruby-font-size": case "set-ruby-font-size":
_advTextStyle = _advTextStyle with { RubyFontSize = (int)Read(a[0]) }; return pc + 1; _advTextStyle = _advTextStyle with { RubyFontSize = (int)Read(a[0]) }; return pc + 1;
case "u0041B3D0":
case "set-text-line-spacing":
_advTextStyle = _advTextStyle with { LineSpacing = (int)Read(a[0]) }; return pc + 1;
case "set-font-bold": case "set-font-bold":
_advTextStyle = _advTextStyle with { Bold = Read(a[0]) != 0 }; return pc + 1; _advTextStyle = _advTextStyle with { Bold = Read(a[0]) != 0 }; return pc + 1;
case "set-text-color": case "set-text-color":

View File

@@ -630,6 +630,7 @@ public partial class Main : Godot.Control
label.AddThemeFontSizeOverride("font_size", fontSize); label.AddThemeFontSizeOverride("font_size", fontSize);
label.AddThemeColorOverride("font_color", RgbColor(style.TextColor, Colors.White)); label.AddThemeColorOverride("font_color", RgbColor(style.TextColor, Colors.White));
label.AddThemeColorOverride("font_outline_color", RgbColor(style.EffectColor, new Color(0.38f, 0.38f, 0.38f))); label.AddThemeColorOverride("font_outline_color", RgbColor(style.EffectColor, new Color(0.38f, 0.38f, 0.38f)));
label.AddThemeConstantOverride("line_spacing", style.LineSpacing);
int outline = style.RenderMode == 0 ? 0 : System.Math.Max(1, int outline = style.RenderMode == 0 ? 0 : System.Math.Max(1,
System.Math.Max(System.Math.Abs(style.EffectOffsetX), System.Math.Abs(style.EffectOffsetY))); System.Math.Max(System.Math.Abs(style.EffectOffsetX), System.Math.Abs(style.EffectOffsetY)));
label.AddThemeConstantOverride("outline_size", outline); label.AddThemeConstantOverride("outline_size", outline);

View File

@@ -9,6 +9,7 @@ INFERRED: dict[int, dict] = {
0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'), 0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'),
0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'), 0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'),
0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'), 0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'),
0x8b: dict(name='set-text-line-spacing', category='adv', noop=False, confidence='high', source='investigation', summary='(pixels) - set the extra pixel spacing added between text lines.'),
0x90: dict(name='register-hotspot-callbacks', category='input', noop=True, confidence='high', source='investigation', summary="(x)(y)(w)(h)(on_enter_pc)(on_leave_pc)(on_activate_pc) - append an inclusive cursor rectangle and three callback PCs to the current script frame's native input registry. This opcode only registers; the ADV input service dispatches callbacks later."), 0x90: dict(name='register-hotspot-callbacks', category='input', noop=True, confidence='high', source='investigation', summary="(x)(y)(w)(h)(on_enter_pc)(on_leave_pc)(on_activate_pc) - append an inclusive cursor rectangle and three callback PCs to the current script frame's native input registry. This opcode only registers; the ADV input service dispatches callbacks later."),
0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."), 0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."),
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'), 0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),

View File

@@ -58,6 +58,11 @@ name = "gfx_obj_ptr_table"
type = "void*" type = "void*"
note = "per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c)" note = "per-object pointer table (ops 0x212/0x213 write obj+0x64/0x68/0x6c)"
[[field]] [[field]]
offset = 0x14ea0
name = "text_line_spacing"
type = "int"
note = "extra pixel leading between text lines; defaults to 6, op 0x8b writes it"
[[field]]
offset = 0x14f45 offset = 0x14f45
name = "script_frame_index" name = "script_frame_index"
type = "int" type = "int"

View File

@@ -1405,18 +1405,18 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041B3D0" name = "set-text-line-spacing"
category = "unknown" category = "adv"
summary = "" summary = "(pixels) - set the extra pixel spacing added between text lines."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0x8b_set_text_line_spacing@0x41f270 stores operand 1 at text manager +0x560 (ctx+0x14ea0). adv_text_manager_initialize@0x456800 defaults it to 6. Horizontal newline/history paths at 0x456fd0/0x452970 advance y by (+0x560 - primary LOGFONT.lfHeight); lfHeight is negative, so this is font pixel height plus configured leading. Corpus arguments are 8 for 22/24px Mincho blocks and 9 for 16px Gothic blocks."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "extra line spacing in pixels"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]