Implement retained ADV History writers
This commit is contained in:
@@ -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
|
||||
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.
|
||||
History should therefore begin with an engine/session-owned `AdvTextHistory` service—records, logical index,
|
||||
suppression, and clear lifetime—fed by the native write opcodes. The host should render a requested group;
|
||||
it should not own the canonical backlog. No persistence format is required for that first slice.
|
||||
The first port slice now implements an engine/session-owned `AdvTextHistory` service. `GameSession` carries
|
||||
one instance across VM scene runs, while a standalone VM owns its instance for the full top-level and nested
|
||||
script lifetime. The model retains the logical index, typed text/metadata/voice records, group-start flags,
|
||||
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
|
||||
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
155
engine/Age.Engine.Tests/AdvTextHistoryTests.cs
Normal file
155
engine/Age.Engine.Tests/AdvTextHistoryTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
177
engine/Age.Engine/Model/AdvTextHistory.cs
Normal file
177
engine/Age.Engine/Model/AdvTextHistory.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ public sealed class GameSession
|
||||
{
|
||||
public Dictionary<int, long> Globals { 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 SeedString(int addr, string value) => GlobalStrings[addr] = value;
|
||||
@@ -29,7 +31,7 @@ public sealed class GameSession
|
||||
VmOptions? options = null, IScriptProvider? provider = 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 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);
|
||||
}
|
||||
|
||||
/// <summary>Serialize the persistent state to JSON (globals + string globals, keyed by decimal address).
|
||||
/// Lets an expensive booted state be snapshotted and reused; foundation for save-file work.</summary>
|
||||
/// <summary>Serialize the persistent global banks to JSON, keyed by decimal address.
|
||||
/// 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()
|
||||
{
|
||||
var snap = new StateSnapshot(
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class VirtualMachine
|
||||
private long _autoMessageTime1Ms = 2000;
|
||||
private bool _autoVoicePending;
|
||||
private volatile bool _messageSkipEnabled;
|
||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||
public long CallScriptDispatches { get; private set; }
|
||||
|
||||
public Dictionary<int, long> Globals { get; } = new();
|
||||
@@ -46,11 +47,13 @@ public sealed class VirtualMachine
|
||||
public long Steps { get; private set; }
|
||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||
public bool MessageSkipEnabled => _messageSkipEnabled;
|
||||
public AdvTextHistory TextHistory { get; }
|
||||
|
||||
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;
|
||||
_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>
|
||||
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; }
|
||||
string text = _cur.Script.GetString(off);
|
||||
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);
|
||||
}
|
||||
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
|
||||
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 "configure-adv-wait-indicator": // 0x73: per-layout animated input-wait marker
|
||||
_host.ConfigureAdvWaitIndicator(new AdvWaitIndicatorConfig(
|
||||
@@ -601,6 +614,32 @@ public sealed 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) 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 "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state
|
||||
return pc + 1;
|
||||
@@ -632,6 +671,7 @@ public sealed class VirtualMachine
|
||||
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
|
||||
case "play-voice":
|
||||
_autoVoicePending = true;
|
||||
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
|
||||
_host.PlayVoice(Read(a[0])); return pc + 1;
|
||||
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user