Implement retained ADV History writers
This commit is contained in:
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