Extract VM text history opcode handler

This commit is contained in:
gamer147
2026-08-03 00:08:24 -04:00
parent f85e8643a0
commit 0df1b1856f
4 changed files with 89 additions and 55 deletions

View File

@@ -190,6 +190,8 @@ publication, skip-aware blocking fades/crossfades, foreground-transition waits,
`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.
`engine/Age.Engine/Vm/VirtualMachine.TextHistory.cs` owns history recording control, metadata append/navigation,
retained history rendering, metadata/voice lookup, and history backlog clearing.
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

@@ -668,6 +668,12 @@ do not mix mechanical moves with semantic changes.
`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.
The eighth bounded `VirtualMachine.Step` extraction moved text-history recording control, metadata append and
navigation, retained history rendering, metadata/voice lookup, and backlog clearing into
`engine/Age.Engine/Vm/VirtualMachine.TextHistory.cs`. The top-level dispatcher retains all labels and aliases
at their existing positions and routes them through guarded `StepTextHistory`; live ADV text remains with
`StepAdvText`, while skip/auto-message services remain outside both handlers. 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.
@@ -1039,9 +1045,9 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
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,
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.
domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, and
text-history `VirtualMachine.Step` families routed through domain handlers, extract the ADV skip/auto-message
service 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,72 @@
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepTextHistory(string label, IReadOnlyList<Operand> a, int pc)
{
switch (label)
{
case "set-text-history-recording": // 0x1bb: HISTORY.BIN suppresses recording its own UI
if (Read(a[0]) is 0 or 1)
{
bool enabled = Read(a[0]) == 1;
TextHistory.SetRecordingEnabled(enabled);
if (enabled) _host.EndTextHistoryPresentation(Gfx);
}
return pc + 1;
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
TextHistory.AppendMetadata(Read(a[1]), Read(a[0]), _advTextStyle); return pc + 1;
case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary
if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry))
{
Write(a[0], historyEntry.LayoutSlot);
Write(a[1], historyEntry.FirstRecordIndex);
}
else
{
Write(a[0], -1);
Write(a[1], -1);
}
return pc + 1;
case "render-text-history": // 0x1d1: rasterize/bind one retained group to a target layout
case "u0041BAE0":
{
int flags = (int)Read(a[2]);
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
{
batch = batch with
{
// Native History uses the text manager's current leading, not a retained-record field.
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
};
_host.RenderTextHistory(
Gfx,
TextHistory.GetPresentationBinding(batch.LayoutSlot),
batch);
}
return pc + 1;
}
case "u0041BB90":
case "find-text-history-value": // 0x1d3: operand 3 is accepted but ignored natively
{
bool found = TextHistory.TryFindMetadata((int)Read(a[3]), Read(a[4]), out long value);
Write(a[0], found ? 1 : 0);
Write(a[1], value);
return pc + 1;
}
case "u0041BC00":
case "find-text-history-pair": // 0x1d4: operand 3 is accepted but ignored natively
TextHistory.TryFindVoicePair((int)Read(a[3]), out long voiceId, out long voiceArgument);
Write(a[0], voiceId);
Write(a[1], voiceArgument);
return pc + 1;
case "clear-text-history": // 0x85: bound the backlog to the current ordinary ADV block
TextHistory.Clear(); return pc + 1;
default:
throw new InvalidOperationException($"Non-text-history opcode routed to text-history handler: {label}");
}
}
}

View File

@@ -2258,63 +2258,17 @@ public sealed partial class VirtualMachine
case "block-mark":
case "reset-message-voice-state": // 0x1bc resets native per-message voice/queued-voice state
_autoVoicePending = false; return pc + 1;
case "set-text-history-recording": // 0x1bb: HISTORY.BIN suppresses recording its own UI
if (Read(a[0]) is 0 or 1)
{
bool enabled = Read(a[0]) == 1;
TextHistory.SetRecordingEnabled(enabled);
if (enabled) _host.EndTextHistoryPresentation(Gfx);
}
return pc + 1;
case "append-text-history-metadata": // 0x1d2: (metadata type, value)
TextHistory.AppendMetadata(Read(a[1]), Read(a[0]), _advTextStyle); return pc + 1;
case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary
if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry))
{
Write(a[0], historyEntry.LayoutSlot);
Write(a[1], historyEntry.FirstRecordIndex);
}
else
{
Write(a[0], -1);
Write(a[1], -1);
}
return pc + 1;
case "set-text-history-recording":
case "append-text-history-metadata":
case "step-text-history":
case "render-text-history": // 0x1d1: rasterize/bind one retained group to a target layout
case "u0041BAE0":
{
int flags = (int)Read(a[2]);
if ((flags & 4) == 0 && TextHistory.TryBuildRenderBatch(
(int)Read(a[0]), (int)Read(a[1]), flags, Read(a[3]), Read(a[4]), out var batch))
{
batch = batch with
{
// Native History uses the text manager's current leading, not a retained-record field.
Style = batch.Style with { LineSpacing = _advTextStyle.LineSpacing }
};
_host.RenderTextHistory(
Gfx,
TextHistory.GetPresentationBinding(batch.LayoutSlot),
batch);
}
return pc + 1;
}
case "u0041BB90":
case "find-text-history-value": // 0x1d3: operand 3 is accepted but ignored natively
{
bool found = TextHistory.TryFindMetadata((int)Read(a[3]), Read(a[4]), out long value);
Write(a[0], found ? 1 : 0);
Write(a[1], value);
return pc + 1;
}
case "u0041BC00":
case "find-text-history-pair": // 0x1d4: operand 3 is accepted but ignored natively
TextHistory.TryFindVoicePair((int)Read(a[3]), out long voiceId, out long voiceArgument);
Write(a[0], voiceId);
Write(a[1], voiceArgument);
return pc + 1;
case "clear-text-history": // 0x85: bound the backlog to the current ordinary ADV block
TextHistory.Clear(); return pc + 1;
case "clear-text-history":
return StepTextHistory(label, a, pc);
case "set-font-size":
case "set-ruby-font-size":
case "u0041B3D0":