Implement AGE text line spacing
This commit is contained in:
@@ -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) |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.'),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
Reference in New Issue
Block a user