using Age.Engine.Diagnostics; using Age.Engine.Hosting; using Age.Engine.Model; using Age.Engine.Persistence; using Age.Engine.Sys4; using System.Text; namespace Age.Engine.Vm; /// A stable identity/snapshot of the exact script frame currently executing. public sealed record DebugFrameSnapshot(long FrameId, string CurrentScript, IReadOnlyList CallStack); public enum SharedProfileShutdownFlushOutcome { Saved, AlreadyHandled, Suppressed, StoreUnavailable, Failed, } public readonly record struct SharedProfileShutdownFlushResult( SharedProfileShutdownFlushOutcome Outcome, string? Error = null); public sealed partial class VirtualMachine { private const long NoJump = 0xFFFFFFFF; private const int HALT = int.MinValue; private const int FRAME_RETURN = int.MinValue + 1; 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_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; private readonly IHost _host; private readonly VmOptions _o; private readonly Encoding _nativeStringEncoding; private readonly IScriptProvider? _provider; private readonly SharedProfile _sharedProfile; private readonly DiagnosticOutputState _diagnosticOutput; private readonly INativeDatStore? _nativeDatStore; private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1"; private ExecFrame _cur = null!; private int _depth; private readonly ITraceSink _sink; private readonly object _interactiveLock = new(); private readonly object _debugControlLock = new(); private readonly object _sharedProfileShutdownLock = new(); private readonly List _activeFrameNames = new(); private readonly List _activeExecutionFrames = new(); private ExecFrame? _saveResumeFrame; private NativeNumberedSaveState? _loadedNumberedState; private NativeNumberedSaveState? _retainedNativeNumberedState; private int _restoreFrameIndex = -1; private uint _accumulatedPlaySeconds; private readonly long _sessionStartTimestamp; private ExecFrame? _debugActiveFrame; private long _debugActiveFrameId; private long _debugNextFrameId; private DebugFrameReturnRequest? _debugFrameReturnRequest; private ExecFrame? _interactiveFrame; private ExecFrame? _rawInputFrame; private int _pointerX = int.MinValue, _pointerY = int.MinValue; private int _mouseButtonState; private int _mouseWheelDelta; private int _heldInputCallbackMask; private int _queuedInputCallbackMask; private bool _autoMessageEnabled; private long _autoMessageTime0Ms = 500; private long _autoMessageTime1Ms = 2000; private bool _autoVoicePending; private bool _initialRootRun = true; private volatile bool _stopRequested; private bool _sharedProfileShutdownHandled; private volatile bool _messageSkipEnabled; private volatile bool _messageSkipServiceActive; private bool _advReadSkipState; private AdvTextStyle _advTextStyle = AdvTextStyle.Default; private int _messageWindowAlphaSetting; private int _messageGlyphDelayMilliseconds; // EngineCtx +0xa0d10: AGERC queries this through IAGEService to gray its native // settings/save menu actions while CONFIG owns the scripted settings screen. private int _systemMenuActionsEnabled = 1; // EngineCtx +0x5511c: unsigned TIMER_SHOWMENU dwell threshold. Native op 0x148 // returns the same dword through the VM's signed integer-cell representation. private uint _systemMenuShowDelayMilliseconds; private readonly Dictionary _valueSwitchTargets = new(StringComparer.Ordinal); // Eleven safely isolated handler-addressable FIFO slots. Native physically owns ten at // +0x55130; its admitted id 10 aliases stack slot zero. ATSEEK/MVSEEK use FIFO slot zero. private readonly Queue?[] _intQueues = new Queue?[11]; // Eleven safely isolated handler-addressable LIFO slots. Native constructs ten at +0x55158 // on every scene reset; its admitted id 10 aliases numeric-glyph-style storage. private readonly Stack[] _intStacks = CreateIntegerStacks(); // Opcodes 0x06/0x08 load scripts into numbered EngineCtx frame slots and invoke them later. // Unlike ordinary call-script frames, native non-adjacent slots survive return with locals intact. private readonly Dictionary _preloadedScriptSlots = new(); public long CallScriptDispatches { get; private set; } public Dictionary Globals { get; } = new(); public Dictionary GlobalFloats { get; } = new(); /// Native/profile-owned values read by scripts but maintained outside script-visible writes. public Dictionary ExternalGlobals { get; } = new(); public Dictionary GlobalStrings { get; } = new(); public Dictionary GlobalPointers { get; } = new(); public Dictionary GlobalStringPointers { get; } = new(); public GfxState Gfx { get; } = new(); public InputBindings InputBindings { get; } = new(); public List<(int Offset, string Text, string Script)> Emitted { get; } = new(); public string? HaltReason { get; private set; } public long Steps { get; private set; } public bool AutoMessageEnabled => _autoMessageEnabled; public bool MessageSkipEnabled => _messageSkipEnabled; public int SystemMenuActionsEnabled => _systemMenuActionsEnabled; public uint SystemMenuShowDelayMilliseconds => _systemMenuShowDelayMilliseconds; public string PendingDiagnosticText => _diagnosticOutput.PendingText; /// /// 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. /// public int? SaveResumeFrameDepth { get { lock (_debugControlLock) { int index = _saveResumeFrame == null ? -1 : _activeExecutionFrames.IndexOf(_saveResumeFrame); return index >= 0 ? index : null; } } } /// True while the currently executing script frame owns a timed raw mouse/input callback /// loop (HISTORY/HIDEWIN/FIELD family). A registered callback in a suspended parent frame is dormant /// while a nested ADV scene owns the VM and must not suppress that child's ordinary page input. public bool IsRawInputCallbackActive { get { ExecFrame? rawInputFrame; lock (_interactiveLock) rawInputFrame = _rawInputFrame; if (rawInputFrame == null) return false; lock (_debugControlLock) return ReferenceEquals(rawInputFrame, _debugActiveFrame); } } public string? RawInputCallbackScriptName { get { lock (_interactiveLock) return _rawInputFrame?.Script.Name; } } public AdvTextHistory TextHistory { get; } /// The currently executing recursive script frame and stack, or null outside VM execution. public DebugFrameSnapshot? DebugFrame { get { lock (_debugControlLock) return _debugActiveFrame == null ? null : new DebugFrameSnapshot(_debugActiveFrameId, _debugActiveFrame.Script.Name, _activeFrameNames.ToArray()); } } public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null, IScriptProvider? provider = null, ITraceSink? sink = null, AdvTextHistory? textHistory = null, SharedProfile? sharedProfile = null, INativeDatStore? nativeDatStore = null, AudioMixerSettings? audioMixerSettings = null, DiagnosticOutputState? diagnosticOutput = null) { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider; Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); _nativeStringEncoding = Encoding.GetEncoding(_o.NativeStringCodePage); _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); _accumulatedPlaySeconds = _sharedProfile.AccumulatedPlaySeconds; _sessionStartTimestamp = System.Diagnostics.Stopwatch.GetTimestamp(); } /// Request a clean stop at the next opcode boundary. public void RequestStop() => _stopRequested = true; /// /// Match AGE's accepted-WM_CLOSE shared-profile lifecycle. The native NoSaveDat setting gates only /// this shutdown write; numbered-save opcode 0x19e continues to flush shared state independently. /// Repeated frontend teardown notifications are handled without rotating backups more than once. /// public SharedProfileShutdownFlushResult FlushSharedProfileOnShutdown() { lock (_sharedProfileShutdownLock) { if (_sharedProfileShutdownHandled) return new(SharedProfileShutdownFlushOutcome.AlreadyHandled); _sharedProfileShutdownHandled = true; if (_o.NoSaveDat) return new(SharedProfileShutdownFlushOutcome.Suppressed); if (_nativeDatStore == null) return new(SharedProfileShutdownFlushOutcome.StoreUnavailable); try { _sharedProfile.Save( _nativeDatStore, NativeSystemTime.FromLocalDateTime(DateTime.Now), AccumulatedPlaySeconds()); return new(SharedProfileShutdownFlushOutcome.Saved); } catch (Exception error) when ( error is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentOutOfRangeException or OverflowException) { return new(SharedProfileShutdownFlushOutcome.Failed, error.Message); } } } /// Queue global writes and return only the identified active frame at its next opcode boundary. /// Writes are copied here and applied by the VM thread before another opcode executes. public bool TryRequestDebugFrameReturn(long frameId, IReadOnlyDictionary globalWrites) { ArgumentNullException.ThrowIfNull(globalWrites); lock (_debugControlLock) { if (_debugActiveFrame == null || _debugActiveFrameId != frameId || _debugFrameReturnRequest != null) return false; _debugFrameReturnRequest = new DebugFrameReturnRequest( _debugActiveFrame, new Dictionary(globalWrites)); return true; } } /// Update the native 800x600 cursor coordinate without advancing the current ADV page. public void UpdatePointer(int x, int y) { bool wake = false; lock (_interactiveLock) { _pointerX = x; _pointerY = y; if (_interactiveFrame != null) wake = _interactiveFrame.Hotspots.UpdatePointer(x, y); } if (wake) _host.WakeInputCallbackService(); } /// Queue an armed hotspot's activation callback. True means the click was consumed. public bool TryActivatePointer(int x, int y) { bool consumed = false; lock (_interactiveLock) { _pointerX = x; _pointerY = y; if (_interactiveFrame != null) { _interactiveFrame.Hotspots.UpdatePointer(x, y); consumed = _interactiveFrame.Hotspots.Activate(x, y); } } if (consumed) _host.WakeInputCallbackService(); return consumed; } /// Queue the first armed hotspot callback bound by op 0x97 to any action in /// . True means the logical input was consumed. public bool TryActivateInputActions(int actionMask) { bool consumed; lock (_interactiveLock) consumed = _interactiveFrame?.Hotspots.ActivateBoundActions(actionMask) == true; if (consumed) _host.WakeInputCallbackService(); return consumed; } /// Update one native mouse-button bit (left=0x1, right=0x2 in Himegari). public void UpdateMouseButtonState(int bit, bool pressed) => UpdateMaskBit(ref _mouseButtonState, bit, pressed); /// Accumulate a signed native mouse-wheel delta until op 0x10d consumes it. public void QueueMouseWheelDelta(int delta) { if (delta == 0) return; Interlocked.Add(ref _mouseWheelDelta, delta); _host.WakeInputCallbackService(); } /// Update one held logical AGE action directly. Physical frontends should use the /// keyboard/mouse/joystick methods below so script-configured bindings remain authoritative. public void UpdateInputCallbackState(int index, bool pressed) { if ((uint)index >= 32) return; UpdateMaskBit(ref _heldInputCallbackMask, 1 << index, pressed); _host.WakeInputCallbackService(); } public int UpdateKeyboardVirtualKeyState(int virtualKey, bool pressed) { InputBindings.UpdateKeyboardVirtualKey(virtualKey, pressed); RefreshPhysicalMessageSkipState(); _host.WakeInputCallbackService(); return InputBindings.KeyboardAction(virtualKey); } public int UpdatePhysicalMouseButtonState(int physicalButton, bool pressed) { InputBindings.UpdateMouseButton(physicalButton, pressed); RefreshPhysicalMessageSkipState(); _host.WakeInputCallbackService(); return InputBindings.MouseAction(physicalButton); } public int UpdateJoystickButtonState(int physicalButton, bool pressed) { InputBindings.UpdateJoystickButton(physicalButton, pressed); RefreshPhysicalMessageSkipState(); _host.WakeInputCallbackService(); return InputBindings.JoystickButtonActionMask(physicalButton); } public void UpdateJoystickAxisState(int axis, double value) { InputBindings.UpdateJoystickAxis(axis, value); _host.WakeInputCallbackService(); } /// Queue a one-shot logical action for diagnostics/tests. The native no-input callback is /// table index ActionCount and is selected automatically when polling returns an empty mask. public void QueueInputCallback(int index) { if ((uint)index >= 32) return; if (index == InputBindings.ActionCount) { _host.WakeInputCallbackService(); return; } int bit = 1 << index; int before, after; do { before = Volatile.Read(ref _queuedInputCallbackMask); after = before | bit; } while (Interlocked.CompareExchange(ref _queuedInputCallbackMask, after, before) != before); _host.WakeInputCallbackService(); } private static void UpdateMaskBit(ref int field, int bit, bool set) { int before, after; do { before = Volatile.Read(ref field); after = set ? before | bit : before & ~bit; } while (Interlocked.CompareExchange(ref field, after, before) != before); } /// Mirror adv_interpreter_tick's live bit-0x40 poll. The bit is logical action 6 from the /// process-owned binding map, not a hardcoded Ctrl test; Himegari also binds C and retains the /// engine's default Backspace binding. Native polls this physical channel independently of the /// op-0x19b/0x19c lifecycle used by persistent/read-message Skip. private void RefreshPhysicalMessageSkipState() { bool active = (InputBindings.PollActionMask() & 0x40) != 0; _host.SetPhysicalMessageSkipActive(active); } private int CurrentReadMessageIndex() { if (_cur.ReadMessageOffset < 0) return -1; for (int i = 0; i < _cur.Script.ReadMessageOffsets.Count; i++) if (_cur.Script.ReadMessageOffsets[i] == _cur.ReadMessageOffset) return i; return -1; } private void RefreshAdvReadSkipState() { int messageIndex = CurrentReadMessageIndex(); _advReadSkipState = _sharedProfile.ReadMessageSkipEnabled && _sharedProfile.ReadText.IsMessageRead( _cur.Script.PackedId, messageIndex); _messageSkipServiceActive = _messageSkipEnabled || _advReadSkipState; _host.SetMessageSkipActive(_messageSkipServiceActive); } private static long Gi(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : 0; private int GlobalPointer(int index) => GlobalPointers.TryGetValue(index, out int value) ? value : unchecked((int)Gi(Globals, index)); private int GlobalStringPointer(int index) => GlobalStringPointers.TryGetValue(index, out int value) ? value : unchecked((int)Gi(Globals, index)); private long ReadGlobal(int k) => ExternalGlobals.TryGetValue(k, out var v) ? v : Gi(Globals, k); private static string Gs(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : ""; private static long PyDiv(long a, long b) { if (b == 0) return 0; long q = a / b, r = a % b; if (r != 0 && (r < 0) != (b < 0)) q--; return q; } private static long PyMod(long a, long b) { if (b == 0) return 0; long r = a % b; if (r != 0 && (r < 0) != (b < 0)) r += b; return r; } private static bool IsStr(Operand o) => o.Type is T_STR or T_GSTR or T_GSTRPTR or T_LSTR or T_LSTRPTR; private static bool SameOperand(Operand a, Operand b) => a.Type == b.Type && a.Value == b.Value; private static bool IsAdvLabeledYield(Script script, Instruction ins) => ins.Opcode == 0x140 && ins.Args.Count >= 4 && ins.Args[1].Type == T_STR && ins.Args[2].Type == T_STR && script.GetString((int)ins.Args[1].Value) == "LABEL" && script.GetString((int)ins.Args[2].Value) == "J"; private bool TryGetAdvYieldTerminal(int pc, Operand output, out long terminal) { terminal = 0; if (pc + 2 >= _cur.Script.Instructions.Count) return false; var setTerminal = _cur.Script.Instructions[pc + 1]; var compare = _cur.Script.Instructions[pc + 2]; if (_t.Label(setTerminal.Opcode) != "mov" || setTerminal.Args.Count < 2 || setTerminal.Args[1].Type != T_IMM || _t.Label(compare.Opcode) != "eq" || compare.Args.Count < 3) return false; var terminalOperand = setTerminal.Args[0]; bool comparesTerminalToOutput = (SameOperand(compare.Args[1], terminalOperand) && SameOperand(compare.Args[2], output)) || (SameOperand(compare.Args[2], terminalOperand) && SameOperand(compare.Args[1], output)); if (!comparesTerminalToOutput) return false; terminal = Read(setTerminal.Args[1]); return true; } private long Read(Operand op) => op.Type switch { T_IMM => op.Value, T_GINT => ReadGlobal((int)op.Value), T_GFLOAT => Gi(GlobalFloats, (int)op.Value), T_GPTR => Gi(Globals, GlobalPointer((int)op.Value)), T_LINT => Gi(_cur.Locals.I, (int)op.Value), T_LFLOAT => Gi(_cur.Locals.F, (int)op.Value), T_LPTR => ReadIntCell(Ga(_cur.Locals.P, (int)op.Value)), _ => op.Value, }; private void Write(Operand op, long val) { switch (op.Type) { case T_GINT: Globals[(int)op.Value] = val; break; case T_GFLOAT: GlobalFloats[(int)op.Value] = val; break; case T_GPTR: Globals[GlobalPointer((int)op.Value)] = val; break; case T_LINT: _cur.Locals.I[(int)op.Value] = val; break; case T_LFLOAT: _cur.Locals.F[(int)op.Value] = val; break; case T_LPTR: WriteIntCell(Ga(_cur.Locals.P, (int)op.Value), val); break; } } private string ReadStr(Operand op) => op.Type switch { T_STR => _cur.Script.GetString((int)op.Value), T_GSTR => Gs(GlobalStrings, (int)op.Value), T_GSTRPTR => Gs(GlobalStrings, GlobalStringPointer((int)op.Value)), T_LSTR => Gs(_cur.Locals.S, (int)op.Value), T_LSTRPTR => ReadStringCell(Ga(_cur.Locals.SP, (int)op.Value)), _ => "", }; private void WriteStr(Operand op, string val) { switch (op.Type) { case T_GSTR: GlobalStrings[(int)op.Value] = val; break; case T_GSTRPTR: GlobalStrings[GlobalStringPointer((int)op.Value)] = val; break; case T_LSTR: _cur.Locals.S[(int)op.Value] = val; break; case T_LSTRPTR: WriteStringCell(Ga(_cur.Locals.SP, (int)op.Value), val); break; } } private int NativeStringByteLength(string value) { int nul = value.IndexOf('\0'); return _nativeStringEncoding.GetByteCount(nul < 0 ? value : value[..nul]); } private string FormatIntegerForSurface(ref int x, int value, int fieldWidth, int flags) { int width = Math.Max(0, fieldWidth); var field = new char[Math.Max(1, width)]; bool signed = value < 0 || (value > 0 && (flags & 0x08) != 0) || (value == 0 && (flags & 0x30) != 0); char sign = value < 0 || (value == 0 && (flags & 0x10) == 0 && (flags & 0x20) != 0) ? '-' : '+'; int digitSlots = width - (signed ? 1 : 0); int first = 0; long magnitude = value < 0 ? -(long)value : value; for (int pos = digitSlots - 1; pos >= 0; pos--) { if (pos == digitSlots - 1 || magnitude != 0 || (flags & 0x01) != 0) { field[pos + (signed ? 1 : 0)] = (char)('0' + magnitude % 10); first = pos; } magnitude /= 10; } if (signed) field[first] = digitSlots < 0 ? '#' : sign; int length = first; while (length < field.Length && field[length] != '\0') length++; string text = new(field, first, length - first); // Native uses the primary LOGFONT cell height as a fixed-width advance, halved for ASCII. // Odd heights (and the engine's 32/33-pixel special cases) are rounded down on font rebuild. int fontSize = _advTextStyle.PrimaryFontSize > 0 ? _advTextStyle.PrimaryFontSize : 24; int cellAdvance = fontSize == 33 ? 31 : fontSize == 32 || (fontSize & 1) != 0 ? fontSize - 1 : fontSize; if ((flags & 0x04) == 0) { int divisor = (flags & 0x10000) != 0 ? 2 : 1; if ((flags & 0x02) != 0) divisor *= 2; x += (cellAdvance * first) / divisor; } if ((flags & 0x10000) == 0) text = string.Concat(text.Select(c => c switch { >= '0' and <= '9' => (char)('0' + c - '0'), '-' => '-', >= 'A' and <= 'Z' => (char)('A' + c - 'A'), >= 'a' and <= 'z' => (char)('a' + c - 'a'), _ => '?', })); return text; } private static VmAddress Ga(Dictionary d, int k) => d.TryGetValue(k, out var value) ? value : VmAddress.Global(0); private long ReadIntCell(VmAddress address) => address.Space switch { VmAddressSpace.LocalInteger => Gi(_cur.Locals.I, address.Address), VmAddressSpace.LocalFloat => Gi(_cur.Locals.F, address.Address), _ => Gi(Globals, address.Address), }; private void WriteIntCell(VmAddress address, long value) { switch (address.Space) { case VmAddressSpace.LocalInteger: _cur.Locals.I[address.Address] = value; break; case VmAddressSpace.LocalFloat: _cur.Locals.F[address.Address] = value; break; default: Globals[address.Address] = value; break; } } private string ReadStringCell(VmAddress address) => address.Space == VmAddressSpace.LocalString ? Gs(_cur.Locals.S, address.Address) : Gs(GlobalStrings, address.Address); private void WriteStringCell(VmAddress address, string value) { if (address.Space == VmAddressSpace.LocalString) _cur.Locals.S[address.Address] = value; else GlobalStrings[address.Address] = value; } private VmAddress BaseAddr(Operand op) => op.Type switch { T_LINT => VmAddress.LocalInteger((int)op.Value), T_LFLOAT => VmAddress.LocalFloat((int)op.Value), T_LSTR => VmAddress.LocalString((int)op.Value), T_GPTR => VmAddress.Global(GlobalPointer((int)op.Value)), T_GSTRPTR => VmAddress.Global(GlobalStringPointer((int)op.Value)), T_LPTR => Ga(_cur.Locals.P, (int)op.Value), T_LSTRPTR => Ga(_cur.Locals.SP, (int)op.Value), _ => VmAddress.Global((int)op.Value), }; private bool TryResolveSharedProfileCell(Operand operand, bool isString, out int address) { bool acceptedType = isString ? operand.Type is T_GSTR or T_GSTRPTR or T_LSTRPTR : operand.Type is T_GINT or T_GPTR or T_LPTR; VmAddress resolved = acceptedType ? BaseAddr(operand) : default; if (!acceptedType || resolved.Space != VmAddressSpace.Global || resolved.Address < 0) { address = 0; return false; } address = resolved.Address; return true; } private bool TryStoreAddress(Operand destination, VmAddress address) { switch (destination.Type) { case T_LPTR: _cur.Locals.P[(int)destination.Value] = address; return true; case T_LSTRPTR: _cur.Locals.SP[(int)destination.Value] = address; return true; case T_GPTR: GlobalPointers[(int)destination.Value] = address.Address; return true; case T_GSTRPTR: GlobalStringPointers[(int)destination.Value] = address.Address; return true; default: return false; } } private void LookupStore(Operand dst, VmAddress addr) { if (TryStoreAddress(dst, addr)) return; switch (dst.Type) { default: if (IsStr(dst)) WriteStr(dst, ReadStringCell(addr)); else Write(dst, ReadIntCell(addr)); break; } } private void WriteConsecutive(Operand destination, int index, long value) { int address = checked((int)destination.Value + index); switch (destination.Type) { case T_GINT: Globals[address] = value; break; case T_GFLOAT: GlobalFloats[address] = value; break; case T_LINT: _cur.Locals.I[address] = value; break; case T_LFLOAT: _cur.Locals.F[address] = value; break; case T_GPTR: Globals[checked(GlobalPointer((int)destination.Value) + index)] = value; break; case T_LPTR: WriteIntCell(Ga(_cur.Locals.P, (int)destination.Value).Offset(index), value); break; } } private long ReadAddressedCell(Operand operand, int offset) { return operand.Type switch { T_LINT => Gi(_cur.Locals.I, checked((int)operand.Value + offset)), T_LFLOAT => Gi(_cur.Locals.F, checked((int)operand.Value + offset)), T_GINT => ReadGlobal(checked((int)operand.Value + offset)), T_GFLOAT => Gi(GlobalFloats, checked((int)operand.Value + offset)), T_LPTR => ReadIntCell(Ga(_cur.Locals.P, (int)operand.Value).Offset(offset)), T_GPTR => Gi(Globals, checked(GlobalPointer((int)operand.Value) + offset)), _ => Gi(Globals, checked((int)operand.Value + offset)), }; } private (VmAddressSpace Space, int Address) AddressedCellIdentity(Operand operand, int offset) => operand.Type switch { T_LINT => (VmAddressSpace.LocalInteger, checked((int)operand.Value + offset)), T_LFLOAT => (VmAddressSpace.LocalFloat, checked((int)operand.Value + offset)), T_LPTR => PointerIdentity(Ga(_cur.Locals.P, (int)operand.Value).Offset(offset)), T_GPTR => (VmAddressSpace.Global, checked(GlobalPointer((int)operand.Value) + offset)), _ => (VmAddressSpace.Global, checked((int)operand.Value + offset)), }; private static (VmAddressSpace Space, int Address) PointerIdentity(VmAddress address) => (address.Space, address.Address); private string FormatSwitchValue(Operand operand) => IsStr(operand) ? 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 { } private sealed record DebugFrameReturnRequest(ExecFrame Frame, IReadOnlyDictionary GlobalWrites); private sealed record PreloadedScriptSlot(long ScriptId, ExecFrame Frame); private enum FrameOutcome { Returned, DebugReturned, RootReload, ExitRequested, Halted, RanOff } public void Run(int entryOffset = 0) { // The native scheduler supplies this scene-entry state outside script-visible global writes. // Restrict it to the byte-identical ADV LABEL/J idiom; op 0x140 also has an unrelated TITLE use. if (entryOffset == 0 && _s.Instructions.Any(ins => IsAdvLabeledYield(_s, ins))) Globals[SceneEntryCoroutineGate] = 1; Script root = _s; int rootEntry = root.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0; FrameCause cause = FrameCause.TopScene; NativeSavedScriptFrame? restoredRootFrame = null; while (true) { FrameOutcome outcome; try { ExecFrame rootFrame = restoredRootFrame == null ? new ExecFrame(root, rootEntry) : CreateRestoredFrame(root, restoredRootFrame); restoredRootFrame = null; outcome = RunFrame(rootFrame, cause); } catch (NumberedRestoreRequestedException) { if (_loadedNumberedState == null) throw; Script? callback = _provider?.GetByName("CALLBACK_LOAD.BIN"); if (callback != null) { int callbackEntry = callback.IndexByOffset.TryGetValue(0, out int ci) ? ci : 0; FrameOutcome callbackOutcome = RunFrame( new ExecFrame(callback, callbackEntry), FrameCause.SaveRestore, callback.PackedId); if (callbackOutcome is FrameOutcome.Halted or FrameOutcome.ExitRequested) { outcome = callbackOutcome; break; } } root = ResolveSavedScript(_loadedNumberedState.Frames[0]); rootEntry = 0; _restoreFrameIndex = 0; restoredRootFrame = _loadedNumberedState.Frames[0]; cause = FrameCause.SaveRestore; continue; } if (outcome == FrameOutcome.RootReload) { // Native 0x9 performs the scene reset before attempting the resource-0 load. Keep // that ordering even when a diagnostic provider cannot resolve the root script. ResetSceneContextForRootReload(); var reloaded = _provider?.GetById(0); if (reloaded == null) { HaltReason ??= "root-reload-unresolved:0x0"; break; } root = reloaded; rootEntry = root.IndexByOffset.TryGetValue(0, out int ri) ? ri : 0; cause = FrameCause.RootReload; continue; } if (outcome == FrameOutcome.RanOff) HaltReason ??= "pc-out-of-range"; else if (outcome is FrameOutcome.Returned or FrameOutcome.DebugReturned) HaltReason ??= "exit"; else if (outcome == FrameOutcome.ExitRequested) HaltReason ??= "exit-request"; // Halted: HaltReason already set by the halting op. break; } _sink.Emit(TraceEvent.Halt(HaltReason ?? "unknown", Steps)); } private void ResetSceneContextForRootReload() { Gfx.ResetSceneContext(); _valueSwitchTargets.Clear(); _preloadedScriptSlots.Clear(); ResetIntegerStacks(); lock (_interactiveLock) { _interactiveFrame = null; _rawInputFrame = null; _mouseButtonState = 0; _mouseWheelDelta = 0; _heldInputCallbackMask = 0; _queuedInputCallbackMask = 0; } lock (_debugControlLock) { _debugActiveFrame = null; _debugActiveFrameId = 0; _debugFrameReturnRequest = null; _saveResumeFrame = null; } _autoMessageEnabled = false; _autoVoicePending = false; _messageSkipEnabled = false; _messageSkipServiceActive = false; _advReadSkipState = false; _advTextStyle = AdvTextStyle.Default; _systemMenuActionsEnabled = 1; _systemMenuShowDelayMilliseconds = 0; TextHistory.SetRecordingEnabled(true); _host.SetMessageSkipActive(false); _host.SetPhysicalMessageSkipActive(false); _host.ResetSceneContext(); RefreshPhysicalMessageSkipState(); } private static Stack[] CreateIntegerStacks() { var stacks = new Stack[11]; for (int i = 0; i < stacks.Length; i++) stacks[i] = new Stack(0x100); return stacks; } private void ResetIntegerStacks() { for (int i = 0; i < _intStacks.Length; i++) _intStacks[i] = new Stack(0x100); } private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0) { ExecFrame? previousInteractiveFrame; ExecFrame? previousRawInputFrame; lock (_interactiveLock) { previousInteractiveFrame = _interactiveFrame; previousRawInputFrame = _rawInputFrame; } var prev = _cur; _cur = frame; _depth++; ExecFrame? previousDebugActiveFrame; long previousDebugActiveFrameId; lock (_debugControlLock) { previousDebugActiveFrame = _debugActiveFrame; previousDebugActiveFrameId = _debugActiveFrameId; _debugActiveFrame = frame; _debugActiveFrameId = ++_debugNextFrameId; _activeFrameNames.Add(frame.Script.Name); _activeExecutionFrames.Add(frame); } bool hostContextEntered = false; try { _host.EnterScriptContext(frame.Script.Name); hostContextEntered = true; _sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId)); var outcome = FrameOutcome.RanOff; int pc = frame.Pc; try { while (pc >= 0 && pc < frame.Script.Instructions.Count) { if (_stopRequested) { outcome = FrameOutcome.ExitRequested; break; } if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; outcome = FrameOutcome.Halted; break; } Steps++; frame.Pc = pc; if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, frame.Script.Instructions[pc], _depth)); int next = Step(frame.Script.Instructions[pc], pc); _host.FrameYield(); if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; } if (next == ROOT_RELOAD) { outcome = FrameOutcome.RootReload; break; } if (next == HALT) { outcome = FrameOutcome.Halted; break; } if (TryConsumeDebugFrameReturn(frame)) { outcome = FrameOutcome.DebugReturned; break; } pc = next; } } catch (RootReloadRequestedException) { outcome = FrameOutcome.RootReload; } catch (ProcessExitRequestedException) { outcome = FrameOutcome.ExitRequested; } _sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString())); return outcome; } finally { lock (_debugControlLock) { if (ReferenceEquals(_debugFrameReturnRequest?.Frame, frame)) _debugFrameReturnRequest = null; if (ReferenceEquals(_saveResumeFrame, frame)) _saveResumeFrame = null; if (_activeFrameNames.Count > 0) _activeFrameNames.RemoveAt(_activeFrameNames.Count - 1); if (_activeExecutionFrames.Count > 0) _activeExecutionFrames.RemoveAt(_activeExecutionFrames.Count - 1); _debugActiveFrame = previousDebugActiveFrame; _debugActiveFrameId = previousDebugActiveFrameId; } lock (_interactiveLock) { if (cause == FrameCause.CallScript) _interactiveFrame = previousInteractiveFrame?.Hotspots.Armed == true ? previousInteractiveFrame : null; else if (ReferenceEquals(_interactiveFrame, frame)) _interactiveFrame = null; if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame; } try { if (hostContextEntered) _host.ExitScriptContext(); } finally { _cur = prev; _depth--; } } } private bool TryConsumeDebugFrameReturn(ExecFrame frame) { if (Volatile.Read(ref _debugFrameReturnRequest) is not { } pending || !ReferenceEquals(pending.Frame, frame)) return false; lock (_debugControlLock) { if (!ReferenceEquals(_debugFrameReturnRequest?.Frame, frame)) return false; foreach (var (address, value) in _debugFrameReturnRequest.GlobalWrites) Globals[address] = value; _debugFrameReturnRequest = null; return true; } } private NativeNumberedSaveState CaptureNumberedState() { const int himegariIntegerCount = 0x6241b; const int himegariFloatCount = 1; const int himegariStringCount = 0x315; const int himegariPointerCount = 1; ExecFrame[] active; int cutoff; lock (_debugControlLock) { active = _activeExecutionFrames.ToArray(); cutoff = _saveResumeFrame == null ? active.Length - 1 : Array.IndexOf(active, _saveResumeFrame); } if (cutoff < 0) throw new InvalidDataException("No active script frame is available for a numbered save."); var frames = new NativeSavedScriptFrame[cutoff + 1]; for (int i = 0; i <= cutoff; i++) { ExecFrame frame = active[i]; NativeSavedScriptFrame? restored = frame.RestoredSaveFrame; int[] returns = restored?.ReturnIndices.ToArray() ?? frame.CallStack .Where(returnPc => (uint)returnPc < (uint)frame.Script.Instructions.Count) .Select(returnPc => { int returnOffset = frame.Script.Instructions[returnPc].Offset; return FindTableIndex(frame.Script.LocalCallOffsets, returnOffset - 3); }) .Where(index => index >= 0) .ToArray(); int resumeIndex = restored?.ResumeIndex ?? CurrentReadMessageIndex(frame); int callTargetIndex = i == cutoff ? -1 : restored?.CallTargetIndex ?? ((uint)frame.Pc >= (uint)frame.Script.Instructions.Count ? -1 : FindTableIndex( frame.Script.ScriptCallOffsets, frame.Script.Instructions[frame.Pc].Offset)); frames[i] = new NativeSavedScriptFrame( i - 1, frame.Script.PackedId, returns, resumeIndex, callTargetIndex); } NativeNumberedSaveState basis = _retainedNativeNumberedState ?? NativeNumberedSaveCodec.Empty(frames); var gfx = NativeGfxPersistenceCodec.Capture(Gfx); return basis with { BgmTrackId = unchecked((int)_currentBgmTrackId), SoundEffectResourceIds = _loadedSoundEffectResourceIds .Select(id => unchecked((int)id)).ToArray(), Frames = frames, IntegerGlobals = DenseValues(Globals, himegariIntegerCount), FloatGlobals = DenseValues(GlobalFloats, himegariFloatCount), StringGlobals = DenseStrings(GlobalStrings, himegariStringCount), PointerGlobals = DensePointerValues(GlobalPointers, himegariPointerCount), PointerStrings = DensePointerValues(GlobalStringPointers, himegariPointerCount), LocalPointerScratch = new int[himegariPointerCount], SurfaceRecords = gfx.SurfaceRecords, GfxObjects = gfx.Objects, RangeTransformFirst = gfx.RangeFirst, RangeTransformCount = gfx.RangeCount, RangeTransformRecord = gfx.RangeRecord, }; } private bool TryLoadNumberedState(int slot, bool restoreHistory) { if (_nativeDatStore == null) return false; try { NativeNumberedSaveFile? file = _nativeDatStore.LoadNumberedFile(slot); if (file == null) return false; NativeNumberedSaveState state = NativeNumberedSaveCodec.Decode(file.Document.Payload); ApplyNumberedState(state); _retainedNativeNumberedState = state; _accumulatedPlaySeconds = file.Document.Metadata.AccumulatedPlaySeconds; if (restoreHistory) { if (file.HistoryTail.Length == 0) TextHistory.Clear(); else NativeTextHistoryCodec.DecodeInto(file.HistoryTail, TextHistory); _loadedNumberedState = state; } else { _loadedNumberedState = null; _restoreFrameIndex = -1; } return true; } catch (Exception error) when ( error is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentOutOfRangeException or OverflowException) { _loadedNumberedState = null; _restoreFrameIndex = -1; return false; } } private void ApplyNumberedState(NativeNumberedSaveState state) { ReplaceDensePrefix( Globals, state.IntegerGlobals.Select(value => (long)value).ToArray(), value => value != 0); ReplaceDensePrefix( GlobalFloats, state.FloatGlobals.Select(value => (long)value).ToArray(), value => value != 0); ReplaceDensePrefix(GlobalStrings, state.StringGlobals, value => value.Length != 0); ReplaceDensePrefix(GlobalPointers, state.PointerGlobals, value => value != 0); ReplaceDensePrefix(GlobalStringPointers, state.PointerStrings, value => value != 0); _currentBgmTrackId = unchecked((uint)state.BgmTrackId); if (_currentBgmTrackId == 0) _host.FadeBgm(0, 0); else _host.PlayBgm(_currentBgmTrackId); for (int channel = 0; channel < _loadedSoundEffectResourceIds.Length; channel++) { _host.ReleaseSoundEffect(channel); long resourceId = unchecked((uint)state.SoundEffectResourceIds[channel]); _loadedSoundEffectResourceIds[channel] = resourceId; if (resourceId != 0) _host.LoadSoundEffect(resourceId, channel); } GfxPersistenceSnapshot gfxSnapshot = NativeGfxPersistenceCodec.Decode(state); bool releaseAllSurfaces = _o.CreateObject && _o.AutoFreeTextures; if (releaseAllSurfaces) { Gfx.ReleaseSurfaceRange(0, 1000); _host.ReleaseSurfaceRange(0, 1000); } var restoredSurfaces = Gfx.CapturePersistenceSnapshot().Surfaces .ToDictionary(item => item.Slot); foreach (GfxSurfacePersistenceState surface in gfxSnapshot.Surfaces .Where(item => item.ReloadOnRestore && item.ResourceId >= 0)) restoredSurfaces[surface.Slot] = surface; Gfx.RestorePersistenceSnapshot(gfxSnapshot with { Surfaces = restoredSurfaces.Values.OrderBy(item => item.Slot).ToArray(), }); foreach (GfxSurfacePersistenceState surface in gfxSnapshot.Surfaces .Where(item => item.ReloadOnRestore && item.ResourceId >= 0)) _host.SetTexture(surface.ResourceId, surface.Slot, surface.ColorKey); } private static void ReplaceDensePrefix( Dictionary bank, IReadOnlyList values, Func retain) { foreach (int key in bank.Keys.Where(key => (uint)key < (uint)values.Count).ToArray()) bank.Remove(key); for (int i = 0; i < values.Count; i++) if (retain(values[i])) bank[i] = values[i]; } private Script ResolveSavedScript(NativeSavedScriptFrame frame) { if (_s.PackedId == frame.ScriptId) return _s; Script? script = _provider?.GetById(frame.ScriptId); return script ?? throw new InvalidDataException( $"Numbered save references unresolved script 0x{frame.ScriptId:x}."); } private static int FindRestoreRendezvous(Script script) { for (int i = 0; i < script.Instructions.Count; i++) if (script.Instructions[i].Opcode == 0xae) return i; throw new InvalidDataException( $"Saved script {script.Name} has no opcode 0xae restore rendezvous."); } private static ExecFrame CreateRestoredFrame(Script script, NativeSavedScriptFrame saved) { // Native creates each saved script context at its ordinary entrypoint. The script runs its // local-array/constants/resource prologue and rendezvouses at 0xae itself; jumping directly // to 0xae leaves those frame-local tables zeroed (FIELD then collapses its map zoom to 0%). _ = FindRestoreRendezvous(script); int entry = script.IndexByOffset.TryGetValue(0, out int index) ? index : 0; var frame = new ExecFrame(script, entry) { RestoredSaveFrame = saved, }; if ((uint)saved.ResumeIndex < (uint)script.ReadMessageOffsets.Count) frame.ReadMessageOffset = script.ReadMessageOffsets[saved.ResumeIndex]; foreach (int returnIndex in saved.ReturnIndices) { if ((uint)returnIndex >= (uint)script.LocalCallOffsets.Count) continue; int returnOffset = checked(script.LocalCallOffsets[returnIndex] + 3); if (script.IndexByOffset.TryGetValue(returnOffset, out int returnPc)) frame.CallStack.Add(returnPc); } return frame; } private static int ResolveTableOffset( Script script, IReadOnlyList table, int index, int fallback) { if ((uint)index >= (uint)table.Count) return fallback; return script.IndexByOffset.TryGetValue(table[index], out int pc) ? pc : fallback; } private static int CurrentReadMessageIndex(ExecFrame frame) { for (int i = 0; i < frame.Script.ReadMessageOffsets.Count; i++) if (frame.Script.ReadMessageOffsets[i] == frame.ReadMessageOffset) return i; return -1; } private static int FindTableIndex(IReadOnlyList table, int offset) { for (int i = 0; i < table.Count; i++) if (table[i] == offset) return i; return -1; } private static int[] DenseValues(IReadOnlyDictionary source, int fixedCount) { var result = new int[fixedCount]; foreach ((int index, long value) in source) if ((uint)index < (uint)result.Length) result[index] = unchecked((int)value); return result; } private static int[] DensePointerValues(IReadOnlyDictionary source, int fixedCount) { var result = new int[fixedCount]; foreach ((int index, int value) in source) if ((uint)index < (uint)result.Length) result[index] = value; return result; } private static string[] DenseStrings(IReadOnlyDictionary source, int fixedCount) { var result = Enumerable.Repeat(string.Empty, fixedCount).ToArray(); foreach ((int index, string value) in source) if ((uint)index < (uint)result.Length) result[index] = value; return result; } private uint AccumulatedPlaySeconds() { double elapsedSeconds = System.Diagnostics.Stopwatch.GetElapsedTime(_sessionStartTimestamp).TotalSeconds; return unchecked(_accumulatedPlaySeconds + (uint)Math.Min(uint.MaxValue, elapsedSeconds)); } private bool ServiceHotspotCallback() { int target; lock (_interactiveLock) { if (_interactiveFrame == null || !_interactiveFrame.Hotspots.TryDequeue(out target)) return false; } if (_cur.Script.IndexByOffset.TryGetValue(target, out int pc)) { _cur.CallStack.Add(HOTSPOT_RETURN); while (pc >= 0 && pc < _cur.Script.Instructions.Count) { if (_stopRequested) break; if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; break; } Steps++; _cur.Pc = pc; if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, _cur.Script.Instructions[pc], _depth)); int next = Step(_cur.Script.Instructions[pc], pc); _host.FrameYield(); if (next == ROOT_RELOAD) throw new RootReloadRequestedException(); if (next == HOTSPOT_RETURN || next == FRAME_RETURN) break; if (next == HALT) break; pc = next; } // A malformed callback must not leave its sentinel in the page's ordinary local-call stack. int sentinel = _cur.CallStack.LastIndexOf(HOTSPOT_RETURN); if (sentinel >= 0) _cur.CallStack.RemoveAt(sentinel); } lock (_interactiveLock) { // History/Hide callbacks cancel the active registry, run a nested script, then republish the // parent frame's definitions. Nested RunFrame deliberately clears the disarmed interactive // pointer, so restore the still-running parent before rearming its rebuilt registry. if (_interactiveFrame == null && _cur.Hotspots.HasDefinitions) _interactiveFrame = _cur; _interactiveFrame?.Hotspots.RearmAfterCallback(_pointerX, _pointerY); } _host.InputCallbackCompleted(Gfx); return true; } private int Step(Instruction ins, int pc) { int op = ins.Opcode; var a = ins.Args; string label = _t.Label(op); switch (label) { case "script-entry": Gfx.ClearSurfaceReloadPolicies(); return pc + 1; case "set-surface-persistence-flags": // 0x258 (slot)(flags): bit 0 = numbered-load reload Gfx.SetSurfaceReloadOnRestore(unchecked((int)Read(a[0])), (Read(a[1]) & 1) != 0); return pc + 1; case "add": Write(a[0], Read(a[1]) + Read(a[2])); return pc + 1; case "sub": Write(a[0], Read(a[1]) - Read(a[2])); return pc + 1; case "mul": Write(a[0], Read(a[1]) * Read(a[2])); return pc + 1; case "div": Write(a[0], PyDiv(Read(a[1]), Read(a[2]))); return pc + 1; case "mod": Write(a[0], PyMod(Read(a[1]), Read(a[2]))); return pc + 1; case "and": Write(a[0], Read(a[1]) & Read(a[2])); return pc + 1; case "or": Write(a[0], Read(a[1]) | Read(a[2])); return pc + 1; case "sar": Write(a[0], Read(a[1]) >> (int)(Read(a[2]) & 31)); return pc + 1; case "shl": Write(a[0], Read(a[1]) << (int)(Read(a[2]) & 31)); return pc + 1; case "eq": Write(a[0], Read(a[1]) == Read(a[2]) ? 1 : 0); return pc + 1; case "ne": Write(a[0], Read(a[1]) != Read(a[2]) ? 1 : 0); return pc + 1; case "string-equals": Write(a[0], string.Equals(ReadStr(a[1]), ReadStr(a[2]), StringComparison.Ordinal) ? 1 : 0); return pc + 1; case "string-not-equals": Write(a[0], string.Equals(ReadStr(a[1]), ReadStr(a[2]), StringComparison.Ordinal) ? 0 : 1); return pc + 1; case "concat": { string left = ReadStr(a[1]); string right = ReadStr(a[2]); WriteStr(a[0], left + right); return pc + 1; } case "toString": WriteStr(a[0], unchecked((int)Read(a[1])).ToString(System.Globalization.CultureInfo.InvariantCulture)); return pc + 1; case "absolute-value": { int value = unchecked((int)Read(a[1])); int sign = value >> 31; Write(a[0], unchecked((value ^ sign) - sign)); return pc + 1; } case "get-monotonic-time-ms": Write(a[0], unchecked((int)_host.InputClockMilliseconds)); return pc + 1; case "lt": Write(a[0], Read(a[1]) < Read(a[2]) ? 1 : 0); return pc + 1; case "lte": Write(a[0], Read(a[1]) <= Read(a[2]) ? 1 : 0); return pc + 1; case "gr": Write(a[0], Read(a[1]) > Read(a[2]) ? 1 : 0); return pc + 1; case "gre": Write(a[0], Read(a[1]) >= Read(a[2]) ? 1 : 0); return pc + 1; case "mov": case "set-string": if (IsStr(a[0]) || IsStr(a[1])) WriteStr(a[0], ReadStr(a[1])); else Write(a[0], Read(a[1])); return pc + 1; case "halve-strlen": // 0x1a6: strlen(native encoded bytes) >> 1 Write(a[0], NativeStringByteLength(ReadStr(a[1])) >> 1); return pc + 1; case "edit-fullwidth-string-dialog": // 0x144: blocking AGERc command-10 editor { string current = ReadStr(a[0]); string initial = ReadStr(a[1]); FullwidthTextEditResult result = _host.EditFullwidthString(new(current, initial)); if (result.Accepted) WriteStr(a[0], result.Text); return pc + 1; } case "cp932-character-length": // 0x2c6: Japanese-locale _mbstrlen Write(a[0], Cp932Text.CharacterLength(ReadStr(a[1]), _nativeStringEncoding)); return pc + 1; case "cp932-substring": // 0x2c8: multibyte-character interval [start,start+count) WriteStr(a[0], Cp932Text.Substring( ReadStr(a[1]), unchecked((int)Read(a[2])), unchecked((int)Read(a[3])), _nativeStringEncoding)); 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; case "save-numbered-slot": // 0x19e { if (_nativeDatStore == null) { Write(a[0], 1); return pc + 1; } try { int slot = unchecked((int)Read(a[1])); NativeNumberedSaveState state = CaptureNumberedState(); byte[] payload = NativeNumberedSaveCodec.Encode(state); byte[] history = NativeTextHistoryCodec.Encode(TextHistory); NativeSystemTime timestamp = NativeSystemTime.FromLocalDateTime(DateTime.Now); uint playSeconds = AccumulatedPlaySeconds(); _nativeDatStore.SaveNumberedFile(slot, payload, history, timestamp, playSeconds); _sharedProfile.Save(_nativeDatStore, timestamp, playSeconds); Write(a[0], 0); } catch (Exception error) when ( error is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentOutOfRangeException or OverflowException) { Write(a[0], 1); } return pc + 1; } case "load-numbered-slot-data-only": // 0x19f { if (!TryLoadNumberedState(unchecked((int)Read(a[1])), restoreHistory: false)) Write(a[0], 1); else Write(a[0], 0); return pc + 1; } case "load-numbered-slot-and-resume": // 0x1a1 { if (!TryLoadNumberedState(unchecked((int)Read(a[1])), restoreHistory: true)) { Write(a[0], 1); return pc + 1; } throw new NumberedRestoreRequestedException(); } case "continue-save-load-stack-restore": // 0xae { if (_loadedNumberedState == null || _restoreFrameIndex < 0) return pc + 1; NativeSavedScriptFrame saved = _loadedNumberedState.Frames[_restoreFrameIndex]; bool terminal = _restoreFrameIndex == _loadedNumberedState.Frames.Count - 1; if (terminal) { // Native restores the saved top frame as the numbered-save boundary selected by // opcode 0x1ad. Re-establish that identity so a subsequent save excludes transient // SAVE/menu helper frames instead of serializing the currently open modal stack. lock (_debugControlLock) _saveResumeFrame = _cur; _cur.RestoredSaveFrame = null; _loadedNumberedState = null; _restoreFrameIndex = -1; return ResolveTableOffset(_cur.Script, _cur.Script.ReadMessageOffsets, saved.ResumeIndex, pc + 1); } int parentIndex = _restoreFrameIndex; NativeSavedScriptFrame childSaved = _loadedNumberedState.Frames[parentIndex + 1]; Script child = ResolveSavedScript(childSaved); _restoreFrameIndex = parentIndex + 1; FrameOutcome childOutcome = RunFrame( CreateRestoredFrame(child, childSaved), FrameCause.SaveRestore, childSaved.ScriptId); _restoreFrameIndex = parentIndex; if (childOutcome == FrameOutcome.Halted) return HALT; if (childOutcome == FrameOutcome.RootReload) return ROOT_RELOAD; if (childOutcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException(); _cur.RestoredSaveFrame = null; return ResolveTableOffset( _cur.Script, _cur.Script.ScriptCallOffsets, saved.CallTargetIndex, pc) + 1; } case "query-numbered-save-metadata": // 0x1a0 { if (_nativeDatStore == null) { Write(a[0], 1); return pc + 1; } try { NativeSaveMetadata? metadata = _nativeDatStore.QueryNumberedMetadata(unchecked((int)Read(a[1]))); if (metadata == null) { Write(a[0], 1); return pc + 1; } Write(a[2], metadata.Timestamp.Year); Write(a[3], metadata.Timestamp.Month); Write(a[4], metadata.Timestamp.Day); Write(a[5], metadata.Timestamp.Hour); Write(a[6], metadata.Timestamp.Minute); Write(a[7], metadata.Timestamp.Second); Write(a[8], unchecked((int)metadata.AccumulatedPlaySeconds)); Write(a[0], 0); } catch (EndOfStreamException) { Write(a[0], 2); } catch (InvalidDataException) { Write(a[0], 2); } catch (ArgumentOutOfRangeException) { Write(a[0], 2); } catch (IOException) { Write(a[0], 1); } catch (UnauthorizedAccessException) { Write(a[0], 1); } return pc + 1; } case "delete-numbered-save": // 0x1ab try { Write(a[0], _nativeDatStore?.DeleteNumberedPair(unchecked((int)Read(a[1]))) ?? 2); } catch (ArgumentOutOfRangeException) { Write(a[0], 2); } return pc + 1; case "copy-numbered-save": // 0x1ac try { Write(a[0], _nativeDatStore?.CopyNumberedPair( unchecked((int)Read(a[1])), unchecked((int)Read(a[2]))) ?? 2); } catch (ArgumentOutOfRangeException) { Write(a[0], 2); } catch (IOException) { Write(a[0], 2); } catch (UnauthorizedAccessException) { Write(a[0], 2); } return pc + 1; case "mark-save-resume-frame": // 0x1ad lock (_debugControlLock) _saveResumeFrame = _cur; return pc + 1; case "write-numbered-save-thumbnail": // 0x1ae { if (_nativeDatStore == null) { Write(a[0], 1); return pc + 1; } try { var image = _host.CaptureSurfacePixels(unchecked((int)Read(a[2]))); if (image == null) { Write(a[0], 2); return pc + 1; } byte[] encoded = NumberedThumbnailCodec.Encode(image); _nativeDatStore.SaveNumberedThumbnail(unchecked((int)Read(a[1])), encoded); Write(a[0], 0); } catch (ArgumentOutOfRangeException) { Write(a[0], 2); } catch (InvalidDataException) { Write(a[0], 2); } catch (OverflowException) { Write(a[0], 2); } catch (IOException) { Write(a[0], 1); } catch (UnauthorizedAccessException) { Write(a[0], 1); } return pc + 1; } case "load-numbered-save-thumbnail": // 0x1af { if (_nativeDatStore == null) { Write(a[0], 1); return pc + 1; } try { byte[]? encoded = _nativeDatStore.LoadNumberedThumbnail(unchecked((int)Read(a[1]))); if (encoded == null) { Write(a[0], 1); return pc + 1; } var image = NumberedThumbnailCodec.Decode(encoded); Write(a[0], _host.ReplaceSurfacePixels(unchecked((int)Read(a[2])), image) ? 0 : 2); } catch (ArgumentOutOfRangeException) { Write(a[0], 2); } catch (InvalidDataException) { Write(a[0], 2); } catch (OverflowException) { Write(a[0], 2); } catch (IOException) { Write(a[0], 1); } catch (UnauthorizedAccessException) { Write(a[0], 1); } return pc + 1; } case "store-shared-profile-int": // 0x1a2 { if (!TryResolveSharedProfileCell(a[0], isString: false, out int address)) { HaltReason ??= $"shared-profile-int-lvalue-type:{a[0].Type}"; return HALT; } _sharedProfile.StoreInteger(address, Read(a[0])); return pc + 1; } case "load-shared-profile-int": // 0x1a3 { if (!TryResolveSharedProfileCell(a[0], isString: false, out int address)) { HaltReason ??= $"shared-profile-int-lvalue-type:{a[0].Type}"; return HALT; } Write(a[0], _sharedProfile.LoadInteger(address)); return pc + 1; } case "store-shared-profile-string": // 0x1a9 { if (!TryResolveSharedProfileCell(a[0], isString: true, out int address)) { HaltReason ??= $"shared-profile-string-lvalue-type:{a[0].Type}"; return HALT; } _sharedProfile.StoreString(address, ReadStr(a[0])); return pc + 1; } case "load-shared-profile-string": // 0x1aa { if (!TryResolveSharedProfileCell(a[0], isString: true, out int address)) { HaltReason ??= $"shared-profile-string-lvalue-type:{a[0].Type}"; return HALT; } WriteStr(a[0], _sharedProfile.LoadString(address)); return pc + 1; } case "strlen": // 0x2c5: raw strlen(native encoded bytes) Write(a[0], NativeStringByteLength(ReadStr(a[1]))); return pc + 1; case "lookup-array": LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]))); return pc + 1; case "lookup-array-2d": LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]) * Read(a[3]) + Read(a[4]))); return pc + 1; case "take-address": // 0x63: pointer destination <- underlying address of operand 2 if (!TryStoreAddress(a[0], BaseAddr(a[1]))) { HaltReason ??= $"take-address-destination-type:{a[0].Type}"; return HALT; } return pc + 1; case "copy-inline-int-array": // 0x64: count dword followed by plain file values { int offset = checked((int)Read(a[1])); if ((uint)offset >= (uint)_cur.Script.BodyDwords.Count) { HaltReason ??= $"inline-array-offset@0x{offset:x}"; return HALT; } uint rawCount = _cur.Script.BodyDwords[offset]; int available = _cur.Script.BodyDwords.Count - offset - 1; if (rawCount > (uint)available) { HaltReason ??= $"inline-array-length@0x{offset:x}:{rawCount}"; return HALT; } int count = (int)rawCount; for (int i = 0; i < count; i++) WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i])); return pc + 1; } case "copy-dwords": // 0x1b0: memcpy(count * 4) across resolved integer-cell spans { int count = checked((int)Read(a[2])); if (count < 0) { HaltReason ??= $"copy-dwords-negative-count:{count}"; return HALT; } VmAddress source = BaseAddr(a[0]); VmAddress destination = BaseAddr(a[1]); var values = new long[count]; for (int i = 0; i < count; i++) values[i] = unchecked((int)ReadIntCell(source.Offset(i))); for (int i = 0; i < count; i++) WriteIntCell(destination.Offset(i), values[i]); return pc + 1; } case "find-hit-rectangle": // 0x12e: inclusive rectangle intersection over addressed arrays case "u0041E940": { int previous = (int)Read(a[0]); int count = System.Math.Max(0, (int)Read(a[7])); long refLeft = ReadAddressedCell(a[1], 0); long refRight = ReadAddressedCell(a[1], 1); long refTop = ReadAddressedCell(a[1], 2); long refBottom = ReadAddressedCell(a[1], 3); int match = -1; for (int index = previous + 1; index < count; index++) { long x = Read(a[2]) - ReadAddressedCell(a[5], index); long y = Read(a[3]) - ReadAddressedCell(a[6], index); long left = ReadAddressedCell(a[4], index * 4); long right = ReadAddressedCell(a[4], index * 4 + 1); long top = ReadAddressedCell(a[4], index * 4 + 2); long bottom = ReadAddressedCell(a[4], index * 4 + 3); bool isReferenceRectangle = AddressedCellIdentity(a[1], 0) == AddressedCellIdentity(a[4], index * 4); if (!isReferenceRectangle && x + refLeft <= right && x + refRight >= left && y + refTop <= bottom && y + refBottom >= top) { match = index; break; } } Write(a[0], match); return pc + 1; } case "sort-indices-by-key-sum": // 0x12f: stable ascending permutation by signed key sum case "u0041ECB0": { VmAddress output = BaseAddr(a[0]); // Native writes element zero even when count is zero or negative, then builds the // permutation in place with insertion sort. Read the count for each outer iteration: // the handler fetches operand 4 repeatedly rather than caching it. WriteIntCell(output, 0); for (int sourceIndex = 1; sourceIndex < unchecked((int)Read(a[3])); sourceIndex++) { int position = sourceIndex; while (position > 0) { int previousIndex = unchecked((int)ReadIntCell(output.Offset(position - 1))); int previousKey = unchecked( unchecked((int)ReadAddressedCell(a[1], previousIndex)) + unchecked((int)ReadAddressedCell(a[2], previousIndex))); int sourceKey = unchecked( unchecked((int)ReadAddressedCell(a[1], sourceIndex)) + unchecked((int)ReadAddressedCell(a[2], sourceIndex))); if (sourceKey >= previousKey) break; WriteIntCell(output.Offset(position), previousIndex); position--; } WriteIntCell(output.Offset(position), sourceIndex); } return pc + 1; } case "u0041EF00": case "reset-int-queue": // 0x132: 11 safe logical slots; native's admitted id 10 aliases stack 0 { int queueId = unchecked((int)Read(a[0])); if ((uint)queueId >= (uint)_intQueues.Length) { HaltReason ??= $"int-queue-id-out-of-range:{queueId}"; return HALT; } _intQueues[queueId] = new Queue(0x100); return pc + 1; } case "u0041EFF0": case "enqueue-int": // 0x133 (queue_id, value) { int queueId = unchecked((int)Read(a[0])); if ((uint)queueId >= (uint)_intQueues.Length) { HaltReason ??= $"int-queue-id-out-of-range:{queueId}"; return HALT; } if (_intQueues[queueId] is not { } queue) { HaltReason ??= $"int-queue-uninitialized:{queueId}"; return HALT; } queue.Enqueue(unchecked((int)Read(a[1]))); return pc + 1; } case "u0041F050": case "try-dequeue-int": // 0x134 (queue_id, out_success, out_value) { int queueId = unchecked((int)Read(a[0])); if ((uint)queueId >= (uint)_intQueues.Length) { HaltReason ??= $"int-queue-id-out-of-range:{queueId}"; return HALT; } if (_intQueues[queueId] is not { } queue) { HaltReason ??= $"int-queue-uninitialized:{queueId}"; return HALT; } if (queue.TryDequeue(out int value)) { Write(a[1], 1); Write(a[2], value); } else { // Native writes success=0 and an implementation pointer to operand 3. Shipped // callers branch on success before reading it, so retain the prior destination. Write(a[1], 0); } return pc + 1; } case "u0041F1C0": case "reset-int-stack": // 0x137 (stack_id) { int stackId = unchecked((int)Read(a[0])); if ((uint)stackId >= (uint)_intStacks.Length) { HaltReason ??= $"int-stack-id-out-of-range:{stackId}"; return HALT; } _intStacks[stackId] = new Stack(0x100); return pc + 1; } case "u0041F2B0": case "push-int-stack": // 0x138 (stack_id, value) { int stackId = unchecked((int)Read(a[0])); if ((uint)stackId >= (uint)_intStacks.Length) { HaltReason ??= $"int-stack-id-out-of-range:{stackId}"; return HALT; } _intStacks[stackId].Push(unchecked((int)Read(a[1]))); return pc + 1; } case "u0041F310": case "try-pop-int-stack": // 0x139 (stack_id, out_success, out_value) { int stackId = unchecked((int)Read(a[0])); if ((uint)stackId >= (uint)_intStacks.Length) { HaltReason ??= $"int-stack-id-out-of-range:{stackId}"; return HALT; } if (_intStacks[stackId].TryPop(out int value)) { Write(a[1], 1); Write(a[2], value); } else { // Native writes success=0 and leaks an internal EngineCtx pointer through // operand 3. Preserve the destination instead of exposing host garbage. Write(a[1], 0); } return pc + 1; } case "bit-set": { long bit = Read(a[1]); if ((ulong)bit >= 32) { HaltReason ??= $"bit-index-out-of-range:{bit}"; return HALT; } Write(a[0], Read(a[0]) | (1L << (int)bit)); return pc + 1; } case "bit-reset": { long bit = Read(a[1]); if ((ulong)bit >= 32) { HaltReason ??= $"bit-index-out-of-range:{bit}"; return HALT; } Write(a[0], Read(a[0]) & ~(1L << (int)bit)); return pc + 1; } case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1; case "zero-int-range": case "copy-to-global": // pre-reference compatibility for opcode 0x6c { int count = System.Math.Max(0, checked((int)Read(a[1]))); for (int i = 0; i < count; i++) WriteConsecutive(a[0], i, 0); return pc + 1; } case "random-modulo": // 0x60: native CRT rand() % bound case "u0041A270": { long bound = Read(a[1]); if (bound == 0) { Write(a[0], 0); HaltReason ??= "random-modulo-zero"; return HALT; } Write(a[0], System.Random.Shared.Next(0x8000) % bound); return pc + 1; } case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); case "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); case "ret": if (_cur.CallStack.Count > 0) { int r = _cur.CallStack[^1]; _cur.CallStack.RemoveAt(_cur.CallStack.Count - 1); return r; } return FRAME_RETURN; // empty intra-call stack => return from the script frame case "jcc": { long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value; return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1); } case "begin-value-switch": _valueSwitchTargets.Clear(); return pc + 1; case "add-value-switch-case": _valueSwitchTargets[FormatSwitchValue(a[0])] = checked((int)Read(a[1])); return pc + 1; case "value-switch-jump": { int target = _valueSwitchTargets.TryGetValue(FormatSwitchValue(a[0]), out int matched) ? matched : checked((int)Read(a[1])); return _cur.Script.IndexByOffset.GetValueOrDefault(target, pc + 1); } case "u0041ADB0": case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata _cur.CoroutineYieldHandlerA = (int)Read(a[0]); _cur.CoroutineYieldHandlerB = (int)Read(a[1]); return pc + 1; case "u00414D50": case "yield-adv-coroutine": // 0x199: A -> nested service -> B -> 0x7c resume { int? targetOffset; if (!_cur.CoroutineYieldActive) { _cur.CoroutineResumePc = pc + 1; _cur.CoroutineYieldActive = true; _host.SetAdvPagePresentationSuspended(Gfx, true); targetOffset = _cur.CoroutineYieldHandlerA; } else targetOffset = _cur.CoroutineYieldHandlerB; return targetOffset is int offset ? _cur.Script.IndexByOffset.GetValueOrDefault(offset, pc + 1) : pc + 1; } case "u00416A90": case "coroutine-resume": // 0x7c: restore the PC saved by op 0x199 if (_cur.CoroutineResumePc is int resumePc) { _cur.CoroutineResumePc = null; _cur.CoroutineYieldActive = false; _host.SetAdvPagePresentationSuspended(Gfx, false); return resumePc; } return pc + 1; // cold bounded scene-entry path case "u0041F9C0": case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only { if (!IsAdvLabeledYield(_cur.Script, ins)) { if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc)); return pc + 1; } if (!TryGetAdvYieldTerminal(pc, a[0], out long terminal)) { HaltReason ??= $"coroutine-yield-pattern@0x{ins.Offset:x}"; return HALT; } int visits = _cur.CoroutineYieldVisits.GetValueOrDefault(pc); _cur.CoroutineYieldVisits[pc] = visits + 1; // First visit must enter setup even if out retained this same terminal from a prior scene. // Every later visit returns the script-encoded terminal and exits the bounded loop. Write(a[0], visits == 0 ? (terminal == 0 ? 1 : 0) : terminal); return pc + 1; } case "throw-exit-request": if (_o.IgnoreExitRequests) return pc + 1; // Native op 0x1 throws Command_Exit_Exception through callbacks and nested script // frames. The outer engine loop catches it and exits without advancing frame_pc. throw new ProcessExitRequestedException(); case "exit": return FRAME_RETURN; case "exit-script": // Native op 0x9 clears the process-initial flag, disposes every active script frame, // resets scene-owned services, and loads raw script resource 0 as the new root. _initialRootRun = false; return ROOT_RELOAD; case "call-script": { long id = a.Count > 0 ? Read(a[0]) : 0; CallScriptDispatches++; if (_provider == null) { _sink.Emit(TraceEvent.CallScript(id, null)); // stub mode: notify only, no child pushed return pc + 1; } if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; } var child = _provider.GetById(id); _sink.Emit(TraceEvent.CallScript(id, child?.Name)); if (child == null) { HaltReason ??= $"callscript-unresolved:0x{id:x}"; return HALT; } var entry = child.IndexByOffset.TryGetValue(0, out var ci) ? ci : 0; var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id); if (outcome == FrameOutcome.Halted) return HALT; // propagate whole-VM halt up if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD; // discard every caller frame if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException(); return pc + 1; // Returned / RanOff: resume caller } case "u00415FB0": case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero { if (_provider == null) return pc + 1; // Native first scans every mounted selector into its launch queue, then dispatches // those packed scripts serially. Snapshot before running any child so script-side // effects cannot change the current batch. int[] selectors = _provider.MountedAppendSelectors .Where(selector => selector is > 0 and <= 0xff) .Distinct() .Order() .ToArray(); foreach (int selector in selectors) { if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; } long id = (long)selector << 24; CallScriptDispatches++; var child = _provider.GetById(id); _sink.Emit(TraceEvent.CallScript(id, child?.Name)); if (child == null) { HaltReason ??= $"append-autorun-unresolved:0x{id:x}"; return HALT; } int entry = child.IndexByOffset.TryGetValue(0, out int childEntry) ? childEntry : 0; var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id); if (outcome == FrameOutcome.Halted) return HALT; if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD; if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException(); } return pc + 1; } case "u00417E80": case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39 { long id = Read(a[0]); int slot = unchecked((int)Read(a[1])); if ((uint)slot >= 40) { HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}"; return HALT; } if (_provider == null) { HaltReason ??= $"preloaded-script-provider-unavailable:0x{id:x}"; return HALT; } var script = _provider.GetById(id); if (script == null) { HaltReason ??= $"preloaded-script-unresolved:0x{id:x}"; return HALT; } int entry = script.IndexByOffset.TryGetValue(0, out int loadedEntry) ? loadedEntry : 0; _preloadedScriptSlots[slot] = new PreloadedScriptSlot(id, new ExecFrame(script, entry)); return pc + 1; } case "u00417FC0": case "call-preloaded-script-slot": // 0x08 (frame_slot) { int slot = unchecked((int)Read(a[0])); if ((uint)slot >= 40) { HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}"; return HALT; } if (!_preloadedScriptSlots.TryGetValue(slot, out var loaded)) { HaltReason ??= $"preloaded-script-slot-empty:{slot}"; return HALT; } if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; } CallScriptDispatches++; _sink.Emit(TraceEvent.CallScript(loaded.ScriptId, loaded.Frame.Script.Name)); // PC restarts at codebase while the native slot's local banks remain allocated. // Balanced local calls leave this empty; clearing the port-only emission guard makes // each invocation an independent diagnostic activation. loaded.Frame.CallStack.Clear(); loaded.Frame.EmitSeen.Clear(); loaded.Frame.Pc = loaded.Frame.Script.IndexByOffset.TryGetValue(0, out int loadedEntry) ? loadedEntry : 0; var outcome = RunFrame(loaded.Frame, FrameCause.CallScript, loaded.ScriptId); if (outcome == FrameOutcome.Halted) return HALT; if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD; if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException(); return pc + 1; } case "show-text": case "define-adv-text-layout": case "reset-adv-text-layout": case "set-adv-text-reset-cursor": case "set-adv-text-cursor": case "set-adv-text-bounds": case "configure-adv-wait-indicator": case "u0041B9F0": case "set-adv-wait-indicator-enabled": // 0x1ce: explicit marker service start/stop case "u00420CE0": case "publish-adv-text-layout": // 0x20a: publish layout and current marker frame if active case "draw-string": case "u00420A60": // pre-reference compatibility case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags) return StepAdvText(label, ins, pc); case "wait-for-input": RefreshAdvReadSkipState(); // Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions). if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; } // The native ADV chrome is a coroutine: after an earlier 0x93 cancellation its shared // registration pass runs again before a stable message wait. Our blocking host models that // scheduler boundary by re-arming the frame's retained definitions here. bool wakeAtWait = false; lock (_interactiveLock) { if (_cur.Hotspots.HasDefinitions && !_cur.Hotspots.Armed) { _interactiveFrame = _cur; wakeAtWait = _cur.Hotspots.Arm(_pointerX, _pointerY); } } if (wakeAtWait) _host.WakeInputCallbackService(); _host.WaitForInput((int)Read(a[0]), ServiceHotspotCallback, () => new AdvAutoWaitState(_autoMessageEnabled, _autoVoicePending, _autoMessageTime0Ms, _autoMessageTime1Ms)); _sharedProfile.ReadText.QueueMessage( _cur.Script.PackedId, CurrentReadMessageIndex(), _cur.Script.ReadMessageOffsets.Count); return pc + 1; case "u0041BEB0": case "register-hotspot-callbacks": // 0x90: inclusive rect + enter/leave/activate local callbacks lock (_interactiveLock) _cur.Hotspots.Register((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6])); return pc + 1; case "u00415040": case "cancel-hotspot-wait": // 0x93 lock (_interactiveLock) { _cur.Hotspots.Reset(); if (ReferenceEquals(_interactiveFrame, _cur)) _interactiveFrame = null; } return pc + 1; case "u00415090": case "arm-hotspot-wait": // 0x94 { bool wake; lock (_interactiveLock) { _interactiveFrame = _cur; wake = _cur.Hotspots.Arm(_pointerX, _pointerY); } if (wake) _host.WakeInputCallbackService(); return pc + 1; } case "u0041C150": case "bind-hotspot-key": // 0x97: bind a configured logical action to this record lock (_interactiveLock) _cur.Hotspots.BindKey((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4])); return pc + 1; case "u0041B210": case "set-cursor-resource": // 0x86: raw indexed .CUR resource _host.SetCursorResource(Read(a[0])); return pc + 1; case "u00414D10": case "clear-cursor-resource": // 0x87 _host.ClearCursorResource(); return pc + 1; case "mouse_callback": case "register-mouse-callback": // 0xcc (poll interval ms, local target dword offset) _cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0])); _cur.MouseCallbackTarget = (int)Read(a[1]); _cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs; lock (_interactiveLock) _rawInputFrame = _cur; return pc + 1; case "get-input-type": case "dispatch-mouse-callback": // 0xcd { long now = _host.InputClockMilliseconds; if (_cur.MouseCallbackTarget < 0 || now < _cur.MouseCallbackNextAtMs) return pc + 1; _cur.MouseCallbackNextAtMs = now + _cur.MouseCallbackIntervalMs; if (!_cur.Script.IndexByOffset.TryGetValue(_cur.MouseCallbackTarget, out int target)) return pc + 1; _cur.CallStack.Add(pc + 1); return target; } case "joy_callback": case "register-joy-callback": // 0xfb (input index, local target dword offset) { int index = (int)Read(a[0]); if ((uint)index < 32) _cur.InputCallbackTargets[index] = (int)Read(a[1]); return pc + 1; } case "u0041E360": case "set-input-action-count": // 0xfe: actions [0,count), no-input callback at count { int count = unchecked((int)Read(a[0])); if (!InputBindings.SetActionCount(count)) { HaltReason ??= $"input-action-count-out-of-range:{count}"; return HALT; } return pc + 1; } case "u00415A10": case "poll-joy-callback-input": // 0xff _cur.PendingInputCallbackMask = InputBindings.PollActionMask() | Volatile.Read(ref _heldInputCallbackMask) | Interlocked.Exchange(ref _queuedInputCallbackMask, 0); _cur.InputCallbackScanIndex = 0; return pc + 1; case "u00415A60": case "dispatch-joy-callbacks": // 0x100 { int actionCount = InputBindings.ActionCount; if (_cur.PendingInputCallbackMask == 0) { int idleTarget = _cur.InputCallbackTargets[actionCount]; if (idleTarget < 0 || !_cur.Script.IndexByOffset.TryGetValue(idleTarget, out int target)) return pc + 1; _cur.CallStack.Add(pc + 1); return target; } while (_cur.InputCallbackScanIndex < actionCount) { int index = _cur.InputCallbackScanIndex++; if ((_cur.PendingInputCallbackMask & (1 << index)) == 0) continue; int targetOffset = _cur.InputCallbackTargets[index]; if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target)) continue; // Resume on op 0x100 so another simultaneously active input can dispatch. _cur.CallStack.Add(pc); return target; } return pc + 1; } case "u0041E500": case "map-joystick-button": // 0x107: button slot N emits action N+4 InputBindings.MapJoystickButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1]))); return pc + 1; case "u00415E70": case "get-mouse-button-state": // 0x108 Write(a[0], Volatile.Read(ref _mouseButtonState)); return pc + 1; case "u00415F10": case "consume-mouse-wheel-delta": // 0x10d Write(a[0], Interlocked.Exchange(ref _mouseWheelDelta, 0)); return pc + 1; case "u00415EC0": case "get-cursor-virtual": // 0x109 { int x, y; lock (_interactiveLock) { x = _pointerX; y = _pointerY; } Write(a[0], x == int.MinValue ? 0 : x); Write(a[1], y == int.MinValue ? 0 : y); return pc + 1; } case "u0041E540": case "set-cursor-virtual": // 0x10a; retain the virtual position even without OS warping UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1; case "u0041E5A0": case "map-mouse-button": // 0x10b: physical button -> slot, polled action is slot+4 InputBindings.MapMouseButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1]))); return pc + 1; case "u0041E5E0": case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table { int action = unchecked((int)Read(a[0])); int dik = unchecked((int)Read(a[1])); if (!InputBindings.MapKeyboardScanCode(action, dik)) { HaltReason ??= $"keyboard-action-out-of-range:{action}"; return HALT; } return pc + 1; } case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. _host.Sleep(Read(a[0])); return pc + 1; case "u00425960": case "begin-timed-callback-sequence": // 0xd3: clear the frame-local relative schedule _cur.TimedCallbacks.Clear(); _cur.TimedCallbackCursor = 0; _cur.TimedCallbackStartedAtMs = null; _cur.TimedCallbackAbortOffset = -1; return pc + 1; case "u004266F0": case "append-relative-timed-callbacks": // 0xd4: (interval ms, count, on-time PC, catch-up PC) { long intervalMs = Read(a[0]); int count = System.Math.Max(0, unchecked((int)Read(a[1]))); int primaryOffset = unchecked((int)Read(a[2])); int catchUpOffset = unchecked((int)Read(a[3])); long deadlineMs = _cur.TimedCallbacks.Count == 0 ? 0 : _cur.TimedCallbacks[^1].DeadlineMs; for (int i = 0; i < count; i++) { deadlineMs += intervalMs; _cur.TimedCallbacks.Add(new ExecFrame.TimedCallback( deadlineMs, primaryOffset, catchUpOffset)); } return pc + 1; } case "u004262C0": case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret { if (_cur.TimedCallbackStartedAtMs == null) { _cur.TimedCallbackStartedAtMs = _host.InputClockMilliseconds; _cur.TimedCallbackAbortOffset = unchecked((int)Read(a[0])); } // Native op 0xd5 stops at last_index rather than count. The final entry is a // look-ahead sentinel: it supplies the next deadline for the preceding event but // is not itself dispatched. if (_cur.TimedCallbackCursor >= _cur.TimedCallbacks.Count - 1) { _cur.TimedCallbackStartedAtMs = null; return pc + 1; } var callback = _cur.TimedCallbacks[_cur.TimedCallbackCursor]; long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; if (elapsedMs < callback.DeadlineMs) { _host.WaitForTimedCallbackDeadline(callback.DeadlineMs - elapsedMs); elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; } bool fellBehind = _cur.TimedCallbackCursor + 1 < _cur.TimedCallbacks.Count && _cur.TimedCallbacks[_cur.TimedCallbackCursor + 1].DeadlineMs < elapsedMs; int targetOffset = fellBehind ? callback.CatchUpOffset : callback.PrimaryOffset; _cur.TimedCallbackCursor++; if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target)) return pc; _cur.CallStack.Add(pc); return target; } case "u0041B290": case "set-message-skip": // 0x88: persistent all-message fast-forward service state _messageSkipEnabled = Read(a[0]) != 0; _messageSkipServiceActive = _messageSkipEnabled; _host.SetMessageSkipActive(_messageSkipServiceActive); return pc + 1; case "u00414E50": // 0x19a: persistent state used by the SO001 active overlay Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1; case "u00414E80": case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation _messageSkipServiceActive = false; _host.SetMessageSkipActive(false); return pc + 1; case "u00414EC0": case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry _messageSkipServiceActive = _messageSkipEnabled || _advReadSkipState || _host.IsAdvReadSkipActive; _host.SetMessageSkipActive(_messageSkipServiceActive); RefreshPhysicalMessageSkipState(); return pc + 1; case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward // Native persistent state and the independently polled physical action-6 channel both // re-arm the transient run-state bit consumed by this query. Write(a[0], _messageSkipServiceActive || _host.IsMessageSkipActive ? 1 : 0); return pc + 1; case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state case "get-adv-service-state": // compatibility with pre-recovery generated tables Write(a[0], _advReadSkipState || _host.IsAdvReadSkipActive ? 1 : 0); return pc + 1; case "u0041B9B0": case "set-read-message-skip": // 0x1ca: engine setting message:ReadTextSkip _sharedProfile.ReadMessageSkipEnabled = Read(a[0]) != 0; RefreshAdvReadSkipState(); return pc + 1; case "u00414FD0": case "get-read-message-skip": // 0x1cb Write(a[0], _sharedProfile.ReadMessageSkipEnabled ? 1 : 0); return pc + 1; case "u00414F60": case "get-auto-message": // 0x1b6: VM service state used by the ADV redraw callback Write(a[0], _autoMessageEnabled ? 1 : 0); return pc + 1; case "u0041B640": case "set-auto-message": // 0x1b7 _autoMessageEnabled = Read(a[0]) != 0; return pc + 1; case "u0041B670": case "get-auto-message-time": // 0x1b8 (selector 0=post-voice Time0, 1=unvoiced Time1, out) Write(a[1], Read(a[0]) == 0 ? _autoMessageTime0Ms : _autoMessageTime1Ms); return pc + 1; case "u0041B710": case "set-auto-message-time": // 0x1b9 (selector, milliseconds) if (Read(a[0]) == 0) _autoMessageTime0Ms = Read(a[1]); else if (Read(a[0]) == 1) _autoMessageTime1Ms = Read(a[1]); return pc + 1; case "u00415670": 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 "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 "set-font-size": case "set-ruby-font-size": case "u0041B3D0": case "set-text-line-spacing": case "set-font-bold": case "set-text-color": case "set-text-effect-color": case "set-text-render-mode": case "set-text-effect-offset": case "set-adv-text-layout-origin": // 0x198; slot 0 selects the current layout case "u0041B540": return StepAdvText(label, ins, pc); case "get-message-window-alpha": // 0x131: process-owned message:MesWinAlpha setting case "u00415F70": Write(a[0], _messageWindowAlphaSetting); return pc + 1; case "set-message-window-alpha": // 0x141: paired message:MesWinAlpha configuration setter case "u0041FAA0": _messageWindowAlphaSetting = (int)Read(a[0]); _host.SetMessageWindowAlphaSetting(_messageWindowAlphaSetting); return pc + 1; case "set-system-menu-enabled": // 0x142: native AGERC menu reentrancy guard case "u0041FB10": // pre-reference compatibility _systemMenuActionsEnabled = unchecked((int)Read(a[0])); return pc + 1; case "get-system-menu-show-delay": // 0x148: paired TIMER_SHOWMENU getter case "u004160A0": // pre-reference compatibility Write(a[0], unchecked((int)_systemMenuShowDelayMilliseconds)); return pc + 1; case "set-system-menu-show-delay": // 0x149: top-edge dwell threshold in milliseconds case "u0041FCE0": // pre-reference compatibility _systemMenuShowDelayMilliseconds = unchecked((uint)Read(a[0])); return pc + 1; case "get-message-glyph-delay": // 0x7f case "u00414C60": case "set-message-glyph-delay": // 0x1b5 case "u0041B5F0": return StepAdvText(label, ins, pc); case "u00415BF0": case "reset-message-skip-input": // 0x101 clears transient input/run bits, not op 0x88 state return pc + 1; case "end-text-line": case "set-font": case "comment": case "display-furigana": case "dev_ukn": return StepAdvText(label, ins, pc); case "create-texture": case "set-texture": case "u00422E80": // pre-reference compatibility case "set-tiled-surface-edge-length": // 0x248 (edge pixels) case "u00422EB0": // pre-reference compatibility case "load-raw-texture-surface": // 0x249 (packed resource id)(slot)(colorkey) case "draw-texture": return StepSurface(label, a, pc); case "u0041F3A0": case "register-numeric-glyph-style": // 0x13a: (style)(surface)(atlas x/y)(digit w/h) case "u00422460": case "draw-decimal-glyphs": // 0x23b: retained decimal glyph draw return StepAdvText(label, ins, pc); case "get-texture-size": case "fill-surface-rect": case "u00420D50": case "copy-surface-rect": return StepSurface(label, a, pc); case "clear-retained-gfx-objects": return StepRetainedObject(label, a, pc); case "select-render-target": case "clear-render-target": case "release-transient-surfaces": return StepSurface(label, a, pc); case "play-bgm": case "restart-bgm-loop": case "stop-bgm": case "restart-bgm-once": case "get-current-bgm-track": case "play-voice": case "play-history-voice": case "u0041D910": case "set-voice-bgm-duck-control": case "schedule-voice-playback": case "play-sound-effect": case "u0041D050": case "sfx-start-loop": case "u0041D080": case "schedule-sfx-start": case "u0041D2B0": case "get-audio-volume": case "set-audio-volume": case "get-audio-route-enabled": case "set-audio-route-enabled": return StepAudio(label, a, pc); case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000 return pc + 1; case "get-initial-root-run": // 0x130 (out) Write(a[0], _initialRootRun ? 1 : 0); return pc + 1; case "play-modal-movie-to-surface": case "u004221A0": case "play-movie-to-surface": case "u00422B80": case "play-movie-to-surface-at-position": return StepMovie(label, ins, pc); case "query-gfx-object?": case "query-gfx-field?": case "get-gfx-geom3?": case "get-gfx-geom3-b?": case "set-gfx-geom3": case "set-gfx-geom3-b": case "u0041AF00": // 0x80: default object slot substituted by native op 0x1d9 case "set-default-gfx-object-slot": return StepRetainedObject(label, a, pc); // ---- SC0000 anim/transform/spritesheet cluster (docs/engine-re.md §"SC0000 anim ... cluster") ---- case "u00421DD0": // 0x22f set-position: (handle)(op2)(x)(y)(z) -> base position (direct set) case "u004219E0": // pre-reference compatibility case "set-gfx-range-transform": // 0x229 (first)(count)(anchor x/y/z) case "u00421A90": // pre-reference compatibility case "set-gfx-range-scale-current": // 0x22a (sx%)(sy%)(sz%) case "u00421BD0": // pre-reference compatibility case "set-gfx-range-translation-current": // 0x22c (tx)(ty)(tz) case "u00421C60": // pre-reference compatibility case "set-gfx-range-scale-target": // 0x22d (delay)(duration)(sx%)(sy%)(sz%) return StepRetainedObject(label, a, pc); case "u004223C0": // 0x239 spritesheet cell: (handle)(delay)(duration)(frame count)(columns)(cell) case "reset-gfx-cyclic-animations": // 0x230: stop all five retained looping channels case "u00421E70": case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns) case "u00421EF0": // 0x232 cyclic packed ARGB; negative alpha/RGB preserve static obj color case "set-scale-cycle": // 0x233 (handle)(period ms)(target scale x/y/z percent) return StepAnimation(label, a, pc); case "u00421940": // 0x228: (succ)(handle)(outX)(outY)(outZ) <- target translation matrix return StepRetainedObject(label, a, pc); case "u00422930": case "query-surface-stop-time-ms": case "query-movie-surface-active": return StepMovie(label, ins, pc); case "sample-frame-time": return StepAnimation(label, a, pc); case "set-gfx-geom3-c": case "u00420620": // upstream ABI label case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix case "set-current-rotation-axis-angle": return StepRetainedObject(label, a, pc); case "set-adv-wait-indicator-handle": case "set-adv-text-object-range": return StepAdvText(label, ins, pc); case "gfx-elem-erase": return StepRetainedObject(label, a, pc); case "gfx-elem-release": // 0x1fa (surface slot) _host.ReleaseSurface((int)Read(a[0])); Gfx.ClearSurface((int)Read(a[0])); return pc + 1; case "clone-gfx-object": return StepRetainedObject(label, a, pc); case "gfx-blit-color": case "gfx-draw-color": // ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ---- case "set-anim-transform-abs": // 0x220 (handle)(delay)(duration)(tx)(ty)(tz) case "set-anim-transform-norm": // 0x21e (handle)(delay)(duration)(sx%)(sy%)(sz%) case "set-anim-rotation-axis-angle": // 0x21f (handle)(delay)(duration)(axis x/y/z)(angle deg) case "anim-start": // 0x234 legacy name: (handle)(period)(axis x/y/z), cyclic rotation channel case "set-anim-clock": // 0x238 (duration) — global, non-blocking (host advances it per-frame) case "set-object-animation-detached": // 0x242 (handle)(flags): bit 0 is nonblocking/force-proof case "reset-anim-clock": // 0x243: force unprotected one-shots and reset the global service clock case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243 return StepAnimation(label, a, pc); case "play-movie-mask-transition": return StepMovie(label, ins, pc); case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges case "present-frame": // 0x20c: read/message-skip path snaps a queued transition to its endpoint case "fade-surface-in-from-black": // 0x21: blocking black -> captured full-frame surface case "u00418860": case "fade-surface-out-to-black": // 0x22: blocking captured full-frame surface -> black case "u00418920": case "crossfade-surfaces": // 0x25: legacy full-frame surface alpha transition case "u00418B40": case "mark-frame-yield": // 0x21c: normal foreground-transition scheduler/resume boundary case "clear-gfx-command-queue": // 0x224: retained compositor does not use this native queue case "present-gfx-object-range": // 0x222: publish pending retained changes in the selected range case "u004216C0": return StepPresentation(label, a, pc); default: // Stub is per-instruction frequency (the VM handles ~30 ops; the rest hit here, e.g. // 0x258/0x259 stmt markers appear en masse), so gate it with Step — else --trace floods. if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc)); return pc + 1; } } }