Extract VM ADV text opcode handler

This commit is contained in:
gamer147
2026-08-03 00:04:52 -04:00
parent 07010298bd
commit 4712343c79
4 changed files with 214 additions and 143 deletions

View File

@@ -187,6 +187,9 @@ ADV-binding, and presentation labels remain in their respective dispatcher group
transforms, per-object animation control, frame-time sampling, and the shared animation-clock opcode handler.
`engine/Age.Engine/Vm/VirtualMachine.Presentation.cs` owns queued surface-alpha transitions, frame and object-range
publication, skip-aware blocking fades/crossfades, foreground-transition waits, and graphics command-queue clear.
`engine/Age.Engine/Vm/VirtualMachine.AdvText.cs` owns live ADV text emission, layout/cursor/wait-indicator state,
text style and glyph-delay control, direct surface-string and retained numeric-glyph rendering, and retained
text/wait-object bindings; text history and input/skip/auto services remain separate.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports

View File

@@ -661,6 +661,13 @@ do not mix mechanical moves with semantic changes.
labels and aliases at their existing positions and routes them through guarded `StepPresentation`; movie-mask
transition dispatch remains with the movie handler. Runtime validation remains green.
The seventh bounded `VirtualMachine.Step` extraction moved live ADV text emission, layout/cursor/wait-indicator
state, text style and glyph-delay control, direct surface-string and retained numeric-glyph rendering, and
retained text/wait-object bindings into `engine/Age.Engine/Vm/VirtualMachine.AdvText.cs`. The top-level
dispatcher retains all labels at their existing positions and routes the separated groups through guarded
`StepAdvText`; the complete instruction remains available for emission and layout-reset bytecode offsets.
Text-history and input/skip/auto services remain outside the handler. Runtime validation remains green.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move.
@@ -1034,7 +1041,7 @@ Continue step 2 of the **codebase consolidation** maintenance slice: behavior-ne
by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState`
domains isolated and the audio, movie, surface/texture, retained-object, and animation `VirtualMachine.Step`
families routed through domain handlers, with retained presentation/transition dispatch now isolated as well,
extract the ADV text-layout/rendering opcode family next without replacing the proven dispatcher or changing
public types, commands, and generated output.
and ADV text-layout/rendering dispatch isolated, extract the ADV text-history opcode family next without
replacing the proven dispatcher or changing public types, commands, and generated output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,187 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepAdvText(string label, Instruction ins, int pc)
{
var a = ins.Args;
switch (label)
{
case "show-text":
RefreshAdvReadSkipState();
foreach (var o in a)
{
if (!IsStr(o)) continue;
int off = o.Type == T_STR ? (int)o.Value : ins.Offset;
_cur.EmitSeen.TryGetValue(off, out var c); c++; _cur.EmitSeen[off] = c;
if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; }
string text = ReadStr(o);
Emitted.Add((off, text, _cur.Script.Name));
int layoutSlot = a.Count > 0 ? (int)Read(a[0]) : 0;
string[] scriptStack;
lock (_debugControlLock) scriptStack = _activeFrameNames.ToArray();
var liveRun = new AdvLiveTextRun(
off, TextHistory.GetLayoutSnapshot(layoutSlot), _advTextStyle, text, scriptStack);
TextHistory.AppendText(layoutSlot, off, text, _advTextStyle);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(liveRun.Layout.Slot);
AdvRetainedTextRunResult? retained = _host.ShowText(
Gfx, binding, liveRun, _messageGlyphDelayMilliseconds);
if (retained is { } result)
TextHistory.SetCursor(
result.LayoutSlot, result.CursorX, result.CursorY);
}
return pc + 1;
case "define-adv-text-layout": // 0x70: configure layout and begin a logical retained group
TextHistory.DefineLayout((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]),
(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
{
int requestedSlot = (int)Read(a[0]);
TextHistory.ResetLayout(requestedSlot);
var layout = TextHistory.GetLayoutSnapshot(requestedSlot);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(layout.Slot);
if (binding.FirstObjectHandle >= 0 && binding.ObjectCapacity > 0)
Gfx.EraseRange(binding.FirstObjectHandle, binding.ObjectCapacity);
_host.SetAdvTextCursor(layout.Slot, layout.CursorX, layout.CursorY);
_host.ResetRenderedAdvTextLayout(Gfx, binding);
_cur.ReadMessageOffset = ins.Offset;
_sharedProfile.ReadText.CommitPending();
RefreshAdvReadSkipState();
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]),
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]),
(int)Read(a[8]), Read(a[9])));
return pc + 1;
case "u0041B9F0":
case "set-adv-wait-indicator-enabled": // 0x1ce: explicit marker service start/stop
_host.SetAdvWaitIndicatorEnabled(Read(a[0]) != 0);
return pc + 1;
case "u00420CE0":
case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active
{
int requestedSlot = (int)Read(a[0]);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(requestedSlot);
_host.PublishAdvTextLayout(Gfx, binding);
return pc + 1;
}
case "draw-string": // 0x204 (surface slot, x, y, string)
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
_advTextStyle);
return pc + 1;
case "u00420A60": // pre-reference compatibility
case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags)
{
int x = (int)Read(a[1]);
string text = FormatIntegerForSurface(
ref x, unchecked((int)Read(a[3])), (int)Read(a[4]), (int)Read(a[5]));
_host.DrawStringToSurface((int)Read(a[0]), x, (int)Read(a[2]), text, _advTextStyle);
return pc + 1;
}
case "set-font-size":
_advTextStyle = _advTextStyle with { PrimaryFontSize = (int)Read(a[0]) }; return pc + 1;
case "set-ruby-font-size":
_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":
_advTextStyle = _advTextStyle with { Bold = Read(a[0]) != 0 }; return pc + 1;
case "set-text-color":
_advTextStyle = _advTextStyle with { TextColor = Read(a[0]) }; return pc + 1;
case "set-text-effect-color":
_advTextStyle = _advTextStyle with { EffectColor = Read(a[0]) }; return pc + 1;
case "set-text-render-mode":
_advTextStyle = _advTextStyle with { RenderMode = (int)Read(a[0]) }; return pc + 1;
case "set-text-effect-offset":
_advTextStyle = _advTextStyle with
{
EffectOffsetX = (int)Read(a[0]),
EffectOffsetY = (int)Read(a[1])
};
return pc + 1;
case "set-adv-text-layout-origin": // 0x198; slot 0 selects the current layout
case "u0041B540":
TextHistory.SetLayoutOrigin((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
return pc + 1;
case "get-message-glyph-delay": // 0x7f
case "u00414C60":
Write(a[0], _messageGlyphDelayMilliseconds); return pc + 1;
case "set-message-glyph-delay": // 0x1b5
case "u0041B5F0":
_messageGlyphDelayMilliseconds = System.Math.Max(0, (int)Read(a[0]));
_host.SetMessageGlyphDelayMilliseconds(_messageGlyphDelayMilliseconds);
return pc + 1;
case "end-text-line":
TextHistory.EndLine((int)Read(a[0]), _advTextStyle); return pc + 1;
case "set-font":
_advTextStyle = _advTextStyle with { FontFace = ReadStr(a[0]) }; return pc + 1;
case "comment": case "display-furigana": case "dev_ukn":
return pc + 1;
case "u0041F3A0":
case "register-numeric-glyph-style": // 0x13a: (style)(surface)(atlas x/y)(digit w/h)
{
int styleIndex = unchecked((int)Read(a[0]));
if (!Gfx.RegisterNumericGlyphStyle(styleIndex, unchecked((int)Read(a[1])),
unchecked((int)Read(a[2])), unchecked((int)Read(a[3])),
unchecked((int)Read(a[4])), unchecked((int)Read(a[5]))))
{
HaltReason ??= $"numeric-glyph-style-index-out-of-range:{styleIndex}";
return HALT;
}
return pc + 1;
}
case "u00422460":
case "draw-decimal-glyphs": // 0x23b: retained decimal glyph draw
{
int styleIndex = unchecked((int)Read(a[1]));
if (!Gfx.DrawDecimalGlyphs(Read(a[0]), styleIndex, unchecked((int)Read(a[2])),
unchecked((int)Read(a[3])), unchecked((int)Read(a[4])),
unchecked((int)Read(a[5])), unchecked((int)Read(a[6]))))
{
HaltReason ??= (uint)styleIndex >= 11
? $"numeric-glyph-style-index-out-of-range:{styleIndex}"
: $"numeric-glyph-style-unregistered:{styleIndex}";
return HALT;
}
return pc + 1;
}
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
{
int requestedSlot = (int)Read(a[0]);
TextHistory.SetWaitIndicatorObjectHandle(requestedSlot, Read(a[1]));
AdvTextLayoutSnapshot layout =
TextHistory.GetLayoutSnapshot(requestedSlot);
_host.BindAdvWaitIndicator(
TextHistory.GetPresentationBinding(layout.Slot),
layout);
return pc + 1;
}
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
{
TextHistory.SetTextObjectRange((int)Read(a[0]), Read(a[1]), Read(a[2]));
return pc + 1;
}
default:
throw new InvalidOperationException($"Non-ADV-text opcode routed to ADV-text handler: {label}");
}
}
}

View File

@@ -1963,91 +1963,20 @@ public sealed partial class VirtualMachine
return pc + 1;
}
case "show-text":
RefreshAdvReadSkipState();
foreach (var o in a)
{
if (!IsStr(o)) continue;
int off = o.Type == T_STR ? (int)o.Value : ins.Offset;
_cur.EmitSeen.TryGetValue(off, out var c); c++; _cur.EmitSeen[off] = c;
if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; }
string text = ReadStr(o);
Emitted.Add((off, text, _cur.Script.Name));
int layoutSlot = a.Count > 0 ? (int)Read(a[0]) : 0;
string[] scriptStack;
lock (_debugControlLock) scriptStack = _activeFrameNames.ToArray();
var liveRun = new AdvLiveTextRun(
off, TextHistory.GetLayoutSnapshot(layoutSlot), _advTextStyle, text, scriptStack);
TextHistory.AppendText(layoutSlot, off, text, _advTextStyle);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(liveRun.Layout.Slot);
AdvRetainedTextRunResult? retained = _host.ShowText(
Gfx, binding, liveRun, _messageGlyphDelayMilliseconds);
if (retained is { } result)
TextHistory.SetCursor(
result.LayoutSlot, result.CursorX, result.CursorY);
}
return pc + 1;
case "define-adv-text-layout": // 0x70: configure layout and begin a logical retained group
TextHistory.DefineLayout((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]),
(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
{
int requestedSlot = (int)Read(a[0]);
TextHistory.ResetLayout(requestedSlot);
var layout = TextHistory.GetLayoutSnapshot(requestedSlot);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(layout.Slot);
if (binding.FirstObjectHandle >= 0 && binding.ObjectCapacity > 0)
Gfx.EraseRange(binding.FirstObjectHandle, binding.ObjectCapacity);
_host.SetAdvTextCursor(layout.Slot, layout.CursorX, layout.CursorY);
_host.ResetRenderedAdvTextLayout(Gfx, binding);
_cur.ReadMessageOffset = ins.Offset;
_sharedProfile.ReadText.CommitPending();
RefreshAdvReadSkipState();
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]),
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]),
(int)Read(a[8]), Read(a[9])));
return pc + 1;
case "define-adv-text-layout":
case "reset-adv-text-layout":
case "set-adv-text-reset-cursor":
case "set-adv-text-cursor":
case "set-adv-text-bounds":
case "configure-adv-wait-indicator":
case "u0041B9F0":
case "set-adv-wait-indicator-enabled": // 0x1ce: explicit marker service start/stop
_host.SetAdvWaitIndicatorEnabled(Read(a[0]) != 0);
return pc + 1;
case "u00420CE0":
case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active
{
int requestedSlot = (int)Read(a[0]);
AdvTextLayoutPresentationBinding binding =
TextHistory.GetPresentationBinding(requestedSlot);
_host.PublishAdvTextLayout(Gfx, binding);
return pc + 1;
}
case "draw-string": // 0x204 (surface slot, x, y, string)
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
_advTextStyle);
return pc + 1;
case "draw-string":
case "u00420A60": // pre-reference compatibility
case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags)
{
int x = (int)Read(a[1]);
string text = FormatIntegerForSurface(
ref x, unchecked((int)Read(a[3])), (int)Read(a[4]), (int)Read(a[5]));
_host.DrawStringToSurface((int)Read(a[0]), x, (int)Read(a[2]), text, _advTextStyle);
return pc + 1;
}
return StepAdvText(label, ins, pc);
case "wait-for-input":
RefreshAdvReadSkipState();
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
@@ -2387,31 +2316,17 @@ public sealed partial class VirtualMachine
case "clear-text-history": // 0x85: bound the backlog to the current ordinary ADV block
TextHistory.Clear(); return pc + 1;
case "set-font-size":
_advTextStyle = _advTextStyle with { PrimaryFontSize = (int)Read(a[0]) }; return pc + 1;
case "set-ruby-font-size":
_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":
_advTextStyle = _advTextStyle with { Bold = Read(a[0]) != 0 }; return pc + 1;
case "set-text-color":
_advTextStyle = _advTextStyle with { TextColor = Read(a[0]) }; return pc + 1;
case "set-text-effect-color":
_advTextStyle = _advTextStyle with { EffectColor = Read(a[0]) }; return pc + 1;
case "set-text-render-mode":
_advTextStyle = _advTextStyle with { RenderMode = (int)Read(a[0]) }; return pc + 1;
case "set-text-effect-offset":
_advTextStyle = _advTextStyle with
{
EffectOffsetX = (int)Read(a[0]),
EffectOffsetY = (int)Read(a[1])
};
return pc + 1;
case "set-adv-text-layout-origin": // 0x198; slot 0 selects the current layout
case "u0041B540":
TextHistory.SetLayoutOrigin((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]));
return pc + 1;
return StepAdvText(label, ins, pc);
case "get-message-window-alpha": // 0x131: process-owned message:MesWinAlpha setting
case "u00415F70":
Write(a[0], _messageWindowAlphaSetting); return pc + 1;
@@ -2434,21 +2349,16 @@ public sealed partial class VirtualMachine
return pc + 1;
case "get-message-glyph-delay": // 0x7f
case "u00414C60":
Write(a[0], _messageGlyphDelayMilliseconds); return pc + 1;
case "set-message-glyph-delay": // 0x1b5
case "u0041B5F0":
_messageGlyphDelayMilliseconds = System.Math.Max(0, (int)Read(a[0]));
_host.SetMessageGlyphDelayMilliseconds(_messageGlyphDelayMilliseconds);
return pc + 1;
return StepAdvText(label, ins, pc);
case "u00415BF0":
case "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state
return pc + 1;
case "end-text-line":
TextHistory.EndLine((int)Read(a[0]), _advTextStyle); return pc + 1;
case "set-font":
_advTextStyle = _advTextStyle with { FontFace = ReadStr(a[0]) }; return pc + 1;
case "comment": case "display-furigana": case "dev_ukn":
return pc + 1;
return StepAdvText(label, ins, pc);
case "create-texture":
case "set-texture":
case "u00422E80": // pre-reference compatibility
@@ -2459,32 +2369,9 @@ public sealed partial class VirtualMachine
return StepSurface(label, a, pc);
case "u0041F3A0":
case "register-numeric-glyph-style": // 0x13a: (style)(surface)(atlas x/y)(digit w/h)
{
int styleIndex = unchecked((int)Read(a[0]));
if (!Gfx.RegisterNumericGlyphStyle(styleIndex, unchecked((int)Read(a[1])),
unchecked((int)Read(a[2])), unchecked((int)Read(a[3])),
unchecked((int)Read(a[4])), unchecked((int)Read(a[5]))))
{
HaltReason ??= $"numeric-glyph-style-index-out-of-range:{styleIndex}";
return HALT;
}
return pc + 1;
}
case "u00422460":
case "draw-decimal-glyphs": // 0x23b: retained decimal glyph draw
{
int styleIndex = unchecked((int)Read(a[1]));
if (!Gfx.DrawDecimalGlyphs(Read(a[0]), styleIndex, unchecked((int)Read(a[2])),
unchecked((int)Read(a[3])), unchecked((int)Read(a[4])),
unchecked((int)Read(a[5])), unchecked((int)Read(a[6]))))
{
HaltReason ??= (uint)styleIndex >= 11
? $"numeric-glyph-style-index-out-of-range:{styleIndex}"
: $"numeric-glyph-style-unregistered:{styleIndex}";
return HALT;
}
return pc + 1;
}
return StepAdvText(label, ins, pc);
case "get-texture-size":
case "fill-surface-rect":
case "u00420D50":
@@ -2569,22 +2456,9 @@ public sealed partial class VirtualMachine
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
case "set-current-rotation-axis-angle":
return StepRetainedObject(label, a, pc);
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
{
int requestedSlot = (int)Read(a[0]);
TextHistory.SetWaitIndicatorObjectHandle(requestedSlot, Read(a[1]));
AdvTextLayoutSnapshot layout =
TextHistory.GetLayoutSnapshot(requestedSlot);
_host.BindAdvWaitIndicator(
TextHistory.GetPresentationBinding(layout.Slot),
layout);
return pc + 1;
}
case "set-adv-text-object-range": // 0x213 (layout)(first handle)(count)
{
TextHistory.SetTextObjectRange((int)Read(a[0]), Read(a[1]), Read(a[2]));
return pc + 1;
}
case "set-adv-wait-indicator-handle":
case "set-adv-text-object-range":
return StepAdvText(label, ins, pc);
case "gfx-elem-erase":
return StepRetainedObject(label, a, pc);
case "gfx-elem-release": // 0x1fa (surface slot)