Implement retained ADV History writers

This commit is contained in:
gamer147
2026-07-18 23:28:26 -04:00
parent bd6c92519c
commit dae651e0dd
6 changed files with 415 additions and 9 deletions

View File

@@ -1316,10 +1316,17 @@ rectangle fill (`0x20b`), message-window alpha (`0x131`), and retained-object pr
`0xd3/0xd4/0xd5` callback-sequence family drives the smooth scrollbar interpolation and is the main `0xd3/0xd4/0xd5` callback-sequence family drives the smooth scrollbar interpolation and is the main
remaining scheduler detail; omitting it would affect motion fidelity, not the backlog data model. remaining scheduler detail; omitting it would affect motion fidelity, not the backlog data model.
The port currently retains only the current `_advText` plus a diagnostic `(offset,text)` capture list. The first port slice now implements an engine/session-owned `AdvTextHistory` service. `GameSession` carries
History should therefore begin with an engine/session-owned `AdvTextHistory` service—records, logical index, one instance across VM scene runs, while a standalone VM owns its instance for the full top-level and nested
suppression, and clear lifetime—fed by the native write opcodes. The host should render a requested group; script lifetime. The model retains the logical index, typed text/metadata/voice records, group-start flags,
it should not own the canonical backlog. No persistence format is required for that first slice. layout/cursor snapshots, and font/color/effect state. Ops `0x70`, `0x71`, `0x6e`, `0xc4`, `0x1d2`, `0x1bb`,
and `0x85` feed it directly; Godot remains only a presentation host and does not own the canonical backlog.
The model intentionally has no JSON or disk serialization. `GameSession.ToJson()` continues to snapshot
only the pre-existing global banks, so landing the live backlog does not silently choose a save/profile
backend. Native numbered-save restoration remains the explicit future integration seam described above.
The next History slice can now implement generic inline arrays/value dispatch and ops `0x1d0..0x1d4` over
real retained data before adding host rendering.
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is

View File

@@ -1680,3 +1680,27 @@ backend decision in this slice. Full layout, lifecycle, and opcode evidence live
path first, with focused synthetic tests for grouping, metadata, voice pairs, suppression, and clear. Then path first, with focused synthetic tests for grouping, metadata, voice pairs, suppression, and clear. Then
add generic literal-array/switch support and the HISTORY read/render path. This orders the work around the add generic literal-array/switch support and the HISTORY read/render path. This orders the work around the
data that must exist before clicking History can display anything. data that must exist before clicking History can display anything.
### ADV History retained writer model implemented (2026-07-18)
The recommended first slice is complete. `AdvTextHistory` is an engine model rather than a Godot widget:
standalone VMs own it for their complete nested-script lifetime, and `GameSession` supplies one shared
instance to successive scene VMs. It stores logical `{layout_slot,first_record_index}` entries and semantic
text, typed-metadata, and voice-pair records with native flags, layout/cursor snapshots, and the active
font/color/effect style.
The ordinary native write path is live. Ops `0x70/0x71` configure/reset layouts and establish group
boundaries; `0x6e`, `0xc4`, and `0x1d2` append text, voice, and metadata records; `0x1bb` suppresses all of
those writers while HISTORY renders itself; and `0x85` clears the retained vectors at ordinary ADV block
boundaries. The already-reversed font, color, mode, effect, bold, and cursor setters update the snapshots
future history rendering will consume.
Five focused regressions cover grouping and first-record flags, metadata and voice fields, style/layout
snapshots, suppression/re-enable, clear lifetime, session sharing, and the real SC0000 first page reaching
its wait with string `0x14963` retained. Validation: engine 182/182, zero-warning Godot build, threaded
`SELFTEST OK`. Static HISTORY coverage is now 56/78 distinct opcodes and 798/854 instructions handled or
safe-noop.
**Next:** implement generic inline integer arrays (`0x64`) and value-switch dispatch (`0xa1/0xa2/0xa3`),
then wire History navigation/query ops `0x1d0`, `0x1d3`, and `0x1d4` against this model. Rendering op `0x1d1`
and its presentation operations can follow once those data-side script paths execute correctly.

View File

@@ -0,0 +1,155 @@
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class AdvTextHistoryTests
{
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand I(long value) => new(0, value);
private static Operand S(int index) => new(2, index);
[Fact]
public void OrdinaryAdvOpsBuildGroupedStyledMetadataVoiceAndTextRecords()
{
var script = ScriptAssembler.Assemble(Table, "HISTORY_WRITE",
new List<(int, Operand[])>
{
(0x70, new[] { I(1), I(640), I(160), I(80), I(430) }),
(0x75, new[] { I(24) }),
(0x76, new[] { I(0xf0e0d0) }),
(0x7a, new[] { I(0), I(12), I(34) }),
(0x1d2, new[] { I(123), I(1) }),
(0xc4, new[] { I(77) }),
(0x6e, new[] { I(0), S(0) }),
(0x71, new[] { I(1) }),
(0x6e, new[] { I(0), S(1) }),
(0x2, Array.Empty<Operand>()),
}, new[] { "first", "second" });
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal("exit", vm.HaltReason);
Assert.Equal(new[]
{
new AdvTextHistoryEntry(1, 0),
new AdvTextHistoryEntry(1, 3),
}, vm.TextHistory.Entries);
Assert.Collection(vm.TextHistory.Records,
metadata =>
{
Assert.Equal(AdvTextHistoryRecordKind.Metadata, metadata.Kind);
Assert.Equal(AdvTextHistoryRecordFlags.TypedMetadata | AdvTextHistoryRecordFlags.GroupStart,
metadata.Flags);
Assert.Equal(123, metadata.Value);
Assert.Equal(1, metadata.AuxValue);
Assert.Equal((24, 0xf0e0d0L), (metadata.Style.PrimaryFontSize, metadata.Style.TextColor));
Assert.Equal(new AdvTextLayoutSnapshot(1, 640, 160, 80, 430, 12, 34), metadata.Layout);
},
voice =>
{
Assert.Equal(AdvTextHistoryRecordKind.Voice, voice.Kind);
Assert.Equal(AdvTextHistoryRecordFlags.VoicePair, voice.Flags);
Assert.Equal((77L, 0L), (voice.Value, voice.AuxValue));
},
text =>
{
Assert.Equal(AdvTextHistoryRecordKind.Text, text.Kind);
Assert.Equal("first", text.Text);
Assert.Equal(AdvTextHistoryRecordFlags.None, text.Flags);
},
text =>
{
Assert.Equal(AdvTextHistoryRecordKind.Text, text.Kind);
Assert.Equal("second", text.Text);
Assert.Equal(AdvTextHistoryRecordFlags.GroupStart, text.Flags);
Assert.Equal((0, 0), (text.Layout.CursorX, text.Layout.CursorY));
});
}
[Fact]
public void RecordingSuppressionBlocksEveryWriterUntilReenabled()
{
var script = ScriptAssembler.Assemble(Table, "HISTORY_SUPPRESS",
new List<(int, Operand[])>
{
(0x1bb, new[] { I(0) }),
(0x70, new[] { I(2), I(500), I(100), I(10), I(20) }),
(0x1d2, new[] { I(9), I(2) }),
(0xc4, new[] { I(88) }),
(0x6e, new[] { I(0), S(0) }),
(0x1bb, new[] { I(1) }),
(0x71, new[] { I(2) }),
(0x6e, new[] { I(0), S(1) }),
(0x2, Array.Empty<Operand>()),
}, new[] { "hidden", "retained" });
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.False(vm.TextHistory.RecordingSuppressed);
Assert.Equal(new AdvTextHistoryEntry(2, 0), Assert.Single(vm.TextHistory.Entries));
var record = Assert.Single(vm.TextHistory.Records);
Assert.Equal("retained", record.Text);
Assert.True(record.Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart));
}
[Fact]
public void ClearDropsRecordsIndexAndPendingGroupStartButRetainsLayoutDefinition()
{
var history = new AdvTextHistory();
history.DefineLayout(3, 320, 90, 20, 400);
history.AppendText(0, 10, "old", AdvTextStyle.Default);
history.Clear();
history.AppendText(0, 11, "new", AdvTextStyle.Default);
Assert.Empty(history.Entries);
var record = Assert.Single(history.Records);
Assert.Equal("new", record.Text);
Assert.False(record.Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart));
Assert.Equal(new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0), record.Layout);
}
[Fact]
public void GameSessionSharesOneLiveHistoryAcrossSceneVms()
{
Script Scene(string name, string text) => ScriptAssembler.Assemble(Table, name,
new List<(int, Operand[])>
{
(0x71, new[] { I(1) }),
(0x6e, new[] { I(0), S(0) }),
(0x2, Array.Empty<Operand>()),
}, new[] { text });
var session = new GameSession();
session.RunScene(Scene("FIRST", "one"), Table, new RecordingHost());
session.RunScene(Scene("SECOND", "two"), Table, new RecordingHost());
Assert.Equal(new[] { "one", "two" }, session.TextHistory.Records.Select(r => r.Text));
Assert.Equal(new[]
{
new AdvTextHistoryEntry(1, 0),
new AdvTextHistoryEntry(1, 1),
}, session.TextHistory.Entries);
}
[Fact]
public void RealSc0000FirstPagePopulatesTheRetainedBacklogBeforeItsWait()
{
var scripts = Sys4ScriptProvider.Load(Table);
var scene = scripts.RequireByName("SC0000.BIN");
var vm = new VirtualMachine(scene, Table, new RecordingHost(),
new VmOptions(MaxSteps: 1_000_000, HaltAtWaitForInput: true), scripts);
vm.Run();
Assert.Equal("wait-for-input", vm.HaltReason);
Assert.NotEmpty(vm.TextHistory.Entries);
var text = Assert.Single(vm.TextHistory.Records.Where(r => r.Kind == AdvTextHistoryRecordKind.Text));
Assert.Equal(0x14963, text.SourceOffset);
Assert.NotEmpty(text.Text);
Assert.True(text.Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart));
}
}

View File

@@ -0,0 +1,177 @@
namespace Age.Engine.Model;
[Flags]
public enum AdvTextHistoryRecordFlags : uint
{
None = 0,
GroupStart = 0x00000001,
NavigationFiltered = 0x00000002,
TypedMetadata = 0x20000000,
VoicePair = 0x40000000,
}
public enum AdvTextHistoryRecordKind
{
Text,
Metadata,
Voice,
}
/// <summary>The text raster state copied into each native retained text record.</summary>
public readonly record struct AdvTextStyle(
int PrimaryFontSize,
int RubyFontSize,
bool Bold,
long TextColor,
long EffectColor,
int RenderMode,
int EffectOffsetX,
int EffectOffsetY)
{
public static AdvTextStyle Default => new(0, 0, false, 0, 0, 0, 0, 0);
}
/// <summary>A stable snapshot of the layout state associated with a retained record.</summary>
public readonly record struct AdvTextLayoutSnapshot(
int Slot,
int Width,
int Height,
int OriginX,
int OriginY,
int CursorX,
int CursorY);
/// <summary>
/// One semantic counterpart of AGE's 0x48-byte retained text record. Metadata uses
/// <see cref="Value"/> plus <see cref="AuxValue"/> as value/type; voice uses them as id/argument.
/// </summary>
public sealed record AdvTextHistoryRecord(
AdvTextHistoryRecordKind Kind,
AdvTextHistoryRecordFlags Flags,
AdvTextLayoutSnapshot Layout,
AdvTextStyle Style,
string Text,
long Value,
long AuxValue,
int SourceOffset);
/// <summary>AGE's logical 8-byte history index entry.</summary>
public readonly record struct AdvTextHistoryEntry(int LayoutSlot, int FirstRecordIndex);
/// <summary>
/// Engine-owned retained ADV backlog. It deliberately has no persistence behavior: native numbered-save
/// restoration belongs to the future unified save architecture, while live HISTORY.BIN reads this model.
/// </summary>
public sealed class AdvTextHistory
{
private sealed class LayoutState
{
public int Width;
public int Height;
public int OriginX;
public int OriginY;
public int CursorX;
public int CursorY;
}
private readonly List<AdvTextHistoryRecord> _records = new();
private readonly List<AdvTextHistoryEntry> _entries = new();
private readonly Dictionary<int, LayoutState> _layouts = new();
private readonly HashSet<int> _pendingGroupStarts = new();
public IReadOnlyList<AdvTextHistoryRecord> Records => _records;
public IReadOnlyList<AdvTextHistoryEntry> Entries => _entries;
public bool RecordingSuppressed { get; private set; }
public int CurrentLayoutSlot { get; private set; }
public void SetRecordingEnabled(bool enabled) => RecordingSuppressed = !enabled;
public void DefineLayout(int requestedSlot, int width, int height, int originX, int originY)
{
int slot = SelectLayout(requestedSlot);
var layout = GetOrCreateLayout(slot);
layout.Width = width;
layout.Height = height;
layout.OriginX = originX;
layout.OriginY = originY;
AppendBoundary(slot);
}
public void ResetLayout(int requestedSlot)
{
int slot = SelectLayout(requestedSlot);
var layout = GetOrCreateLayout(slot);
layout.CursorX = 0;
layout.CursorY = 0;
AppendBoundary(slot);
}
public void SetCursor(int requestedSlot, int x, int y)
{
int slot = ResolveLayout(requestedSlot);
var layout = GetOrCreateLayout(slot);
layout.CursorX = x;
layout.CursorY = y;
}
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style)
{
int slot = ResolveLayout(requestedSlot);
AppendRecord(slot, AdvTextHistoryRecordKind.Text, AdvTextHistoryRecordFlags.None,
style, text, 0, 0, sourceOffset);
}
public void AppendMetadata(long value, long metadataType, AdvTextStyle style)
=> AppendRecord(CurrentLayoutSlot, AdvTextHistoryRecordKind.Metadata,
AdvTextHistoryRecordFlags.TypedMetadata, style, "", value, metadataType, -1);
public void AppendVoice(long voiceId, long voiceArgument, AdvTextStyle style)
=> AppendRecord(CurrentLayoutSlot, AdvTextHistoryRecordKind.Voice,
AdvTextHistoryRecordFlags.VoicePair, style, "", voiceId, voiceArgument, -1);
/// <summary>Clear the retained records and logical index while keeping reusable layout definitions.</summary>
public void Clear()
{
_records.Clear();
_entries.Clear();
_pendingGroupStarts.Clear();
}
private int SelectLayout(int requestedSlot)
{
int slot = requestedSlot == 0 ? CurrentLayoutSlot : requestedSlot;
CurrentLayoutSlot = slot;
return slot;
}
private int ResolveLayout(int requestedSlot) => requestedSlot == 0 ? CurrentLayoutSlot : requestedSlot;
private LayoutState GetOrCreateLayout(int slot)
{
if (!_layouts.TryGetValue(slot, out var layout))
{
layout = new LayoutState();
_layouts.Add(slot, layout);
}
return layout;
}
private void AppendBoundary(int slot)
{
if (RecordingSuppressed) return;
_entries.Add(new AdvTextHistoryEntry(slot, _records.Count));
_pendingGroupStarts.Add(slot);
}
private void AppendRecord(int slot, AdvTextHistoryRecordKind kind, AdvTextHistoryRecordFlags flags,
AdvTextStyle style, string text, long value, long auxValue, int sourceOffset)
{
if (RecordingSuppressed) return;
if (_pendingGroupStarts.Remove(slot)) flags |= AdvTextHistoryRecordFlags.GroupStart;
var layout = GetOrCreateLayout(slot);
var snapshot = new AdvTextLayoutSnapshot(slot, layout.Width, layout.Height,
layout.OriginX, layout.OriginY, layout.CursorX, layout.CursorY);
_records.Add(new AdvTextHistoryRecord(kind, flags, snapshot, style, text, value, auxValue, sourceOffset));
}
}

View File

@@ -20,6 +20,8 @@ public sealed class GameSession
{ {
public Dictionary<int, long> Globals { get; } = new(); public Dictionary<int, long> Globals { get; } = new();
public Dictionary<int, string> GlobalStrings { get; } = new(); public Dictionary<int, string> GlobalStrings { get; } = new();
/// <summary>The live retained ADV backlog shared by every VM run in this session.</summary>
public AdvTextHistory TextHistory { get; } = new();
public void Seed(int addr, long value) => Globals[addr] = value; public void Seed(int addr, long value) => Globals[addr] = value;
public void SeedString(int addr, string value) => GlobalStrings[addr] = value; public void SeedString(int addr, string value) => GlobalStrings[addr] = value;
@@ -29,7 +31,7 @@ public sealed class GameSession
VmOptions? options = null, IScriptProvider? provider = null, VmOptions? options = null, IScriptProvider? provider = null,
ITraceSink? sink = null) ITraceSink? sink = null)
{ {
var vm = new VirtualMachine(script, table, host, options, provider, sink); var vm = new VirtualMachine(script, table, host, options, provider, sink, TextHistory);
foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value; foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value;
foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value; foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
@@ -42,8 +44,9 @@ public sealed class GameSession
return new SceneResult(vm.Emitted.ToList(), vm.HaltReason, vm.Steps); return new SceneResult(vm.Emitted.ToList(), vm.HaltReason, vm.Steps);
} }
/// <summary>Serialize the persistent state to JSON (globals + string globals, keyed by decimal address). /// <summary>Serialize the persistent global banks to JSON, keyed by decimal address.
/// Lets an expensive booted state be snapshotted and reused; foundation for save-file work.</summary> /// Lets an expensive booted state be snapshotted and reused. Live <see cref="TextHistory"/> is
/// intentionally excluded until the unified save/profile architecture defines its lifecycle.</summary>
public string ToJson() public string ToJson()
{ {
var snap = new StateSnapshot( var snap = new StateSnapshot(

View File

@@ -34,6 +34,7 @@ public sealed class VirtualMachine
private long _autoMessageTime1Ms = 2000; private long _autoMessageTime1Ms = 2000;
private bool _autoVoicePending; private bool _autoVoicePending;
private volatile bool _messageSkipEnabled; private volatile bool _messageSkipEnabled;
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
public long CallScriptDispatches { get; private set; } public long CallScriptDispatches { get; private set; }
public Dictionary<int, long> Globals { get; } = new(); public Dictionary<int, long> Globals { get; } = new();
@@ -46,11 +47,13 @@ public sealed class VirtualMachine
public long Steps { get; private set; } public long Steps { get; private set; }
public bool AutoMessageEnabled => _autoMessageEnabled; public bool AutoMessageEnabled => _autoMessageEnabled;
public bool MessageSkipEnabled => _messageSkipEnabled; public bool MessageSkipEnabled => _messageSkipEnabled;
public AdvTextHistory TextHistory { get; }
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null, public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null,
IScriptProvider? provider = null, ITraceSink? sink = null) IScriptProvider? provider = null, ITraceSink? sink = null,
AdvTextHistory? textHistory = null)
{ _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider; { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider;
_sink = sink ?? NullTraceSink.Instance; } _sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory(); }
/// <summary>Update the native 800x600 cursor coordinate without advancing the current ADV page.</summary> /// <summary>Update the native 800x600 cursor coordinate without advancing the current ADV page.</summary>
public void UpdatePointer(int x, int y) public void UpdatePointer(int x, int y)
@@ -436,10 +439,20 @@ public sealed class VirtualMachine
if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; } if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; }
string text = _cur.Script.GetString(off); string text = _cur.Script.GetString(off);
Emitted.Add((off, text, _cur.Script.Name)); Emitted.Add((off, text, _cur.Script.Name));
int layoutSlot = a.Count > 0 ? (int)Read(a[0]) : 0;
TextHistory.AppendText(layoutSlot, off, text, _advTextStyle);
_host.ShowText(off, text); _host.ShowText(off, text);
} }
return pc + 1; 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
TextHistory.ResetLayout((int)Read(a[0]));
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]));
_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 "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(
@@ -601,6 +614,32 @@ public sealed class VirtualMachine
case "block-mark": case "block-mark":
case "reset-message-voice-state": // 0x1bc resets native per-message voice/queued-voice state case "reset-message-voice-state": // 0x1bc resets native per-message voice/queued-voice state
_autoVoicePending = false; return pc + 1; _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) TextHistory.SetRecordingEnabled(Read(a[0]) == 1);
return pc + 1;
case "append-text-history-metadata": // 0x1d2: typed value attached to the current group
TextHistory.AppendMetadata(Read(a[0]), Read(a[1]), _advTextStyle); return pc + 1;
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 "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 "u00415BF0": case "u00415BF0":
case "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state case "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state
return pc + 1; return pc + 1;
@@ -632,6 +671,7 @@ public sealed class VirtualMachine
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1; case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice": case "play-voice":
_autoVoicePending = true; _autoVoicePending = true;
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
_host.PlayVoice(Read(a[0])); return pc + 1; _host.PlayVoice(Read(a[0])); return pc + 1;
case "play-sound-effect": // 0xb4 / semantics: sfx-load case "play-sound-effect": // 0xb4 / semantics: sfx-load
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1; _host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;