Implement diagnostic output opcodes
This commit is contained in:
111
engine/Age.Engine.Tests/DiagnosticOutputOpcodeTests.cs
Normal file
111
engine/Age.Engine.Tests/DiagnosticOutputOpcodeTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
|
||||
public class DiagnosticOutputOpcodeTests
|
||||
{
|
||||
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);
|
||||
private static Operand G(int address) => new(3, address);
|
||||
private static (int, Operand[]) Exit() => (0x2, []);
|
||||
|
||||
[Fact]
|
||||
public void TrioPreservesNativeAppendShowClearAndPostClearOrdering()
|
||||
{
|
||||
Script scene = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
|
||||
[
|
||||
(0x1b2, [S(0)]),
|
||||
(0x1b2, [G(0x100)]),
|
||||
(0x1b3, []),
|
||||
(0x1b4, []),
|
||||
(0x1b3, []),
|
||||
(0x1b2, [S(1)]),
|
||||
(0x1b4, []),
|
||||
Exit(),
|
||||
], ["invalid mode=", "tail"]);
|
||||
var host = new RecordingHost();
|
||||
VirtualMachine? vm = null;
|
||||
var pendingAtPresentation = new List<string>();
|
||||
host.OnDiagnosticMessage = _ => pendingAtPresentation.Add(vm!.PendingDiagnosticText);
|
||||
vm = new VirtualMachine(scene, Table, host);
|
||||
vm.Globals[0x100] = -3;
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"invalid mode=-3\r\n",
|
||||
"\r\ntail",
|
||||
], pendingAtPresentation);
|
||||
Assert.Equal(2, host.Diagnostics.Count);
|
||||
Assert.All(host.Diagnostics, message => Assert.Equal("エラーが発生しました", message.Caption));
|
||||
Assert.Equal(
|
||||
"invalid mode=-3\r\n" +
|
||||
"\n\nデバック情報:\n" +
|
||||
"FILE=SYSTEM4.BIN ADDRESS=7 LINE=-1 COMMAND=-(436) DEPTH=0\n",
|
||||
host.Diagnostics[0].Text);
|
||||
Assert.Equal(
|
||||
"\r\ntail" +
|
||||
"\n\nデバック情報:\n" +
|
||||
"FILE=SYSTEM4.BIN ADDRESS=C LINE=-1 COMMAND=-(436) DEPTH=0\n",
|
||||
host.Diagnostics[1].Text);
|
||||
Assert.Equal("", vm.PendingDiagnosticText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenericFormatterCoversIntegerFloatStringAndPointerOperandFamilies()
|
||||
{
|
||||
Script scene = ScriptAssembler.Assemble(Table, "FORMAT_DIAGNOSTIC.BIN",
|
||||
[
|
||||
(0x1b2, [I(unchecked((long)(uint)int.MinValue))]),
|
||||
(0x1b2, [S(0)]),
|
||||
(0x1b2, [new Operand(4, 0x10)]),
|
||||
(0x1b2, [S(1)]),
|
||||
(0x1b2, [new Operand(6, 0x20)]),
|
||||
(0x1b2, [S(2)]),
|
||||
(0x1b2, [new Operand(8, 0x21)]),
|
||||
(0x1b4, []),
|
||||
Exit(),
|
||||
], [":", "/", "/"]);
|
||||
var host = new CaptureHost();
|
||||
var vm = new VirtualMachine(scene, Table, host);
|
||||
vm.GlobalFloats[0x10] = BitConverter.SingleToInt32Bits(1.25f);
|
||||
vm.Globals[0x20] = 0x30;
|
||||
vm.Globals[0x30] = -9;
|
||||
vm.Globals[0x21] = 0x31;
|
||||
vm.GlobalStrings[0x31] = "pointer text";
|
||||
|
||||
vm.Run();
|
||||
|
||||
DiagnosticMessage diagnostic = Assert.Single(host.Diagnostics);
|
||||
Assert.StartsWith("-2147483648:1.250000/-9/pointer text", diagnostic.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameSessionCarriesEngineCtxAccumulatorAcrossFreshSceneVms()
|
||||
{
|
||||
var session = new GameSession();
|
||||
Script field = ScriptAssembler.Assemble(Table, "FIELD.BIN",
|
||||
[
|
||||
(0x1b2, [S(0)]),
|
||||
(0x1b3, []),
|
||||
Exit(),
|
||||
], ["field diagnostic"]);
|
||||
Script system = ScriptAssembler.Assemble(Table, "SYSTEM4.BIN",
|
||||
[
|
||||
(0x1b2, [S(0)]),
|
||||
(0x1b4, []),
|
||||
Exit(),
|
||||
], ["system diagnostic"]);
|
||||
var host = new CaptureHost();
|
||||
|
||||
session.RunScene(field, Table, new CaptureHost());
|
||||
Assert.Equal("field diagnostic\r\n", session.DiagnosticOutput.PendingText);
|
||||
session.RunScene(system, Table, host);
|
||||
|
||||
Assert.StartsWith("field diagnostic\r\nsystem diagnostic", Assert.Single(host.Diagnostics).Text);
|
||||
Assert.Equal("", session.DiagnosticOutput.PendingText);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ internal class RecordingHost : IHost
|
||||
public readonly List<bool> MessageSkipChanges = new();
|
||||
public readonly List<bool> PhysicalMessageSkipChanges = new();
|
||||
public readonly List<string> Warnings = new();
|
||||
public readonly List<DiagnosticMessage> Diagnostics = new();
|
||||
public System.Action<DiagnosticMessage>? OnDiagnosticMessage;
|
||||
public readonly List<long> CursorResources = new();
|
||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||
public readonly List<(AdvLiveTextRun Run, int GlyphDelayMilliseconds)> LiveTextRuns = new();
|
||||
@@ -67,6 +69,11 @@ internal class RecordingHost : IHost
|
||||
public int CursorClearCount;
|
||||
public int SceneContextResets;
|
||||
public void ReportWarning(string message) => Warnings.Add(message);
|
||||
public void ShowDiagnosticMessage(DiagnosticMessage message)
|
||||
{
|
||||
Diagnostics.Add(message);
|
||||
OnDiagnosticMessage?.Invoke(message);
|
||||
}
|
||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ namespace Age.Engine.Hosting;
|
||||
public sealed class CaptureHost : IHost
|
||||
{
|
||||
public List<(int Offset, string Text)> Emitted { get; } = new();
|
||||
public List<DiagnosticMessage> Diagnostics { get; } = new();
|
||||
public void ShowDiagnosticMessage(DiagnosticMessage message) => Diagnostics.Add(message);
|
||||
public void ShowText(int offset, string text) => Emitted.Add((offset, text));
|
||||
public void WaitForInput() { }
|
||||
public void Sleep(long duration) { }
|
||||
|
||||
@@ -28,6 +28,9 @@ public readonly record struct SurfaceRectCopy(
|
||||
int SourceSurface, int DestinationSurface, int SourceX, int SourceY,
|
||||
int Width, int Height, int DestinationX, int DestinationY);
|
||||
|
||||
/// <summary>A synchronous AGE-owned diagnostic prompt after native body/context formatting.</summary>
|
||||
public readonly record struct DiagnosticMessage(string Caption, string Text);
|
||||
|
||||
public enum SurfaceBlackFadeDirection
|
||||
{
|
||||
FromBlack,
|
||||
@@ -38,6 +41,9 @@ public interface IHost
|
||||
{
|
||||
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
|
||||
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
||||
/// <summary>Present a modal diagnostic and return only after the user dismisses it.</summary>
|
||||
void ShowDiagnosticMessage(DiagnosticMessage message)
|
||||
=> System.Console.Error.WriteLine($"{message.Caption}: {message.Text}");
|
||||
// Script context is retained for diagnostics/page location; resource operands are universal packed ids.
|
||||
void EnterScriptContext(string scriptName) { }
|
||||
void ExitScriptContext() { }
|
||||
|
||||
17
engine/Age.Engine/Vm/DiagnosticOutputState.cs
Normal file
17
engine/Age.Engine/Vm/DiagnosticOutputState.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Age.Engine.Vm;
|
||||
|
||||
/// <summary>
|
||||
/// EngineCtx-lifetime accumulator used by opcodes 0x1b2 through 0x1b4. GameSession shares this
|
||||
/// state across fresh scene VMs just as native AGE retains the embedded string across script frames.
|
||||
/// </summary>
|
||||
public sealed class DiagnosticOutputState
|
||||
{
|
||||
private readonly StringBuilder _text = new();
|
||||
|
||||
public string PendingText => _text.ToString();
|
||||
|
||||
internal void Append(string value) => _text.Append(value);
|
||||
internal void Clear() => _text.Clear();
|
||||
}
|
||||
@@ -32,6 +32,8 @@ public sealed class GameSession
|
||||
public AudioMixerSettings AudioMixerSettings { get; }
|
||||
/// <summary>The live retained ADV backlog shared by every VM run in this session.</summary>
|
||||
public AdvTextHistory TextHistory { get; } = new();
|
||||
/// <summary>The EngineCtx-lifetime diagnostic accumulator shared across script/scene VMs.</summary>
|
||||
public DiagnosticOutputState DiagnosticOutput { get; } = new();
|
||||
|
||||
public GameSession(SharedProfile? sharedProfile = null, INativeDatStore? nativeDatStore = null,
|
||||
AudioMixerSettings? audioMixerSettings = null)
|
||||
@@ -51,7 +53,7 @@ public sealed class GameSession
|
||||
{
|
||||
var vm = new VirtualMachine(
|
||||
script, table, host, options, provider, sink, TextHistory, SharedProfile, NativeDatStore,
|
||||
AudioMixerSettings);
|
||||
AudioMixerSettings, DiagnosticOutput);
|
||||
foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value;
|
||||
foreach (var kv in GlobalFloats) vm.GlobalFloats[kv.Key] = kv.Value;
|
||||
foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
|
||||
|
||||
@@ -29,9 +29,10 @@ public sealed class VirtualMachine
|
||||
private const int HOTSPOT_RETURN = int.MinValue + 2;
|
||||
private const int ROOT_RELOAD = int.MinValue + 3;
|
||||
private const int SceneEntryCoroutineGate = 0xaba5c;
|
||||
private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6,
|
||||
private const int T_IMM = 0, T_FLOAT = 1, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6,
|
||||
T_GSTRPTR = 8, T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12,
|
||||
T_LSTRPTR = 14;
|
||||
private const string DiagnosticCaption = "エラーが発生しました";
|
||||
|
||||
private readonly Script _s;
|
||||
private readonly OpcodeTable _t;
|
||||
@@ -41,6 +42,7 @@ public sealed class VirtualMachine
|
||||
private readonly IScriptProvider? _provider;
|
||||
private readonly SharedProfile _sharedProfile;
|
||||
private readonly AudioMixerSettings _audioMixerSettings;
|
||||
private readonly DiagnosticOutputState _diagnosticOutput;
|
||||
private readonly INativeDatStore? _nativeDatStore;
|
||||
private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1";
|
||||
private ExecFrame _cur = null!;
|
||||
@@ -108,6 +110,7 @@ public sealed class VirtualMachine
|
||||
public long Steps { get; private set; }
|
||||
public bool AutoMessageEnabled => _autoMessageEnabled;
|
||||
public bool MessageSkipEnabled => _messageSkipEnabled;
|
||||
public string PendingDiagnosticText => _diagnosticOutput.PendingText;
|
||||
/// <summary>
|
||||
/// Zero-based active-frame cutoff selected by opcode 0x1ad, or null when no surviving marker
|
||||
/// exists. A numbered-save serializer consumes this boundary in the full payload slice.
|
||||
@@ -151,7 +154,8 @@ public sealed class VirtualMachine
|
||||
IScriptProvider? provider = null, ITraceSink? sink = null,
|
||||
AdvTextHistory? textHistory = null, SharedProfile? sharedProfile = null,
|
||||
INativeDatStore? nativeDatStore = null,
|
||||
AudioMixerSettings? audioMixerSettings = null)
|
||||
AudioMixerSettings? audioMixerSettings = null,
|
||||
DiagnosticOutputState? diagnosticOutput = null)
|
||||
{
|
||||
_s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider;
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
@@ -159,6 +163,7 @@ public sealed class VirtualMachine
|
||||
_sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory();
|
||||
_sharedProfile = sharedProfile ?? new SharedProfile();
|
||||
_audioMixerSettings = audioMixerSettings ?? new AudioMixerSettings();
|
||||
_diagnosticOutput = diagnosticOutput ?? new DiagnosticOutputState();
|
||||
_nativeDatStore = nativeDatStore;
|
||||
_messageWindowAlphaSetting = host.MessageWindowAlphaSetting;
|
||||
_messageGlyphDelayMilliseconds = System.Math.Max(0, host.MessageGlyphDelayMilliseconds);
|
||||
@@ -644,6 +649,33 @@ public sealed class VirtualMachine
|
||||
? ReadStr(operand)
|
||||
: unchecked((int)Read(operand)).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatDiagnosticOperand(Operand operand)
|
||||
{
|
||||
if (IsStr(operand)) return ReadStr(operand);
|
||||
if (operand.Type is T_FLOAT or T_GFLOAT or T_LFLOAT)
|
||||
{
|
||||
float value = BitConverter.Int32BitsToSingle(unchecked((int)Read(operand)));
|
||||
return value.ToString("F6", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
return unchecked((int)Read(operand))
|
||||
.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private DiagnosticMessage BuildDiagnosticMessage(Instruction instruction)
|
||||
{
|
||||
// Himegari's release AGE initializes both optional debug metadata tables to null and has no
|
||||
// writer for either one. The native formatter consequently emits -1 and "-" here.
|
||||
const int sourceLine = -1;
|
||||
const string commandName = "-";
|
||||
int nativeDepth = Math.Max(0, _depth - 1);
|
||||
string context = string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
"\n\nデバック情報:\nFILE={0} ADDRESS={1:X} LINE={2} COMMAND={3}({4}) DEPTH={5}\n",
|
||||
_cur.Script.Name, instruction.Offset, sourceLine, commandName,
|
||||
instruction.Opcode, nativeDepth);
|
||||
return new DiagnosticMessage(DiagnosticCaption, _diagnosticOutput.PendingText + context);
|
||||
}
|
||||
|
||||
private sealed class RootReloadRequestedException : Exception { }
|
||||
private sealed class NumberedRestoreRequestedException : Exception { }
|
||||
private sealed class ProcessExitRequestedException : Exception { }
|
||||
@@ -1194,6 +1226,19 @@ public sealed class VirtualMachine
|
||||
case "halve-strlen": // 0x1a6: strlen(native encoded bytes) >> 1
|
||||
Write(a[0], NativeStringByteLength(ReadStr(a[1])) >> 1);
|
||||
return pc + 1;
|
||||
case "u00425790": // upstream ABI label
|
||||
case "append-diagnostic-value": // 0x1b2: generic operand text -> EngineCtx accumulator
|
||||
_diagnosticOutput.Append(FormatDiagnosticOperand(a[0]));
|
||||
return pc + 1;
|
||||
case "u004257D0": // upstream ABI label
|
||||
case "append-diagnostic-newline": // 0x1b3: exact native CRLF bytes
|
||||
_diagnosticOutput.Append("\r\n");
|
||||
return pc + 1;
|
||||
case "u004237C0": // upstream ABI label
|
||||
case "show-and-clear-diagnostic": // 0x1b4: synchronous host prompt, then erase
|
||||
_host.ShowDiagnosticMessage(BuildDiagnosticMessage(ins));
|
||||
_diagnosticOutput.Clear();
|
||||
return pc + 1;
|
||||
case "is-catalog-resource-unlocked": // 0x19d
|
||||
Write(a[0], _sharedProfile.IsCatalogResourceUnlocked(Read(a[1])) ? 1 : 0);
|
||||
return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user