Implement diagnostic output opcodes

This commit is contained in:
gamer147
2026-07-29 11:54:09 -04:00
parent 4af0290bbb
commit 137fae848f
16 changed files with 367 additions and 37 deletions

View File

@@ -6,6 +6,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| offset | name | type | note | | offset | name | type | note |
|---|---|---|---| |---|---|---|---|
| `0x4` | `host_ui_interface` | `void*` | host-facing UI interface used by the ctx+4 virtual-dispatch thunks; default slot +4 logs and presents AGE message boxes |
| `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty | | `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty |
| `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 | | `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 |
| `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 | | `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 |
@@ -54,6 +55,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) | | `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) |
| `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key | | `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key |
| `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 | | `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 |
| `0x54fe8` | `owner_window_handle` | `void*` | native owner HWND forwarded to host UI/message-box calls, including op 0x1b4 diagnostic presentation |
| `0x54ff0` | `initial_root_run` | `int` | initialized to 1 at AGE context construction; queried by op 0x130 and cleared by op 0x9 before root script id 0 is reloaded | | `0x54ff0` | `initial_root_run` | `int` | initialized to 1 at AGE context construction; queried by op 0x130 and cleared by op 0x9 before root script id 0 is reloaded |
| `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay | | `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay |
| `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip | | `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip |
@@ -95,6 +97,9 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
| `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation | | `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation |
| `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active | | `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active |
| `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero | | `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero |
| `0x6ddac` | `opcode_debug_metadata_table` | `void*` | base of 512 optional per-opcode debug-metadata pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic COMMAND names render as '-' |
| `0x6f798` | `script_source_line_table` | `void*` | base of 41 optional per-frame source-line-map pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic LINE values render as -1 |
| `0x6f880` | `diagnostic_text_buffer` | `int` | base of an embedded 28-byte MSVC string accumulator; op 0x1b2 appends formatted operand text, op 0x1b3 appends CRLF, and op 0x1b4 presents then clears it |
| `0x6f89c` | `script_launch_queue` | `int` | embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow | | `0x6f89c` | `script_launch_queue` | `int` | embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow |
| `0x6f8a0` | `script_launch_queue_data` | `void*` | dword storage for packed script ids or negative frame-resume ids | | `0x6f8a0` | `script_launch_queue_data` | `void*` | dword storage for packed script ids or negative frame-resume ids |
| `0x6f8a4` | `script_launch_queue_read_cursor` | `int` | next queued launch consumed by script_launch_queue_dispatch_next | | `0x6f8a4` | `script_launch_queue_read_cursor` | `int` | next queued launch consumed by script_launch_queue_dispatch_next |

View File

@@ -1376,6 +1376,51 @@ scale, rotation, and source-rectangle sampling; clears all ten native timing dwo
numbered-save record, including the unmodeled second cyclic matrix; and leaves base/current transforms, numbered-save record, including the unmodeled second cyclic matrix; and leaves base/current transforms,
cyclic targets, and finite one-shot channels intact. cyclic targets, and finite one-shot channels intact.
#### Diagnostic accumulator opcodes `0x1b2``0x1b4` (2026-07-29)
The three remaining diagnostic-looking instructions are one native family. The real dispatch registrations
are `ctx+0x9b914 = 0x41f4b0`, `ctx+0x9b918 = 0x41a400`, and `ctx+0x9b91c = 0x419240`; the legacy Kelebek
labels do not identify these handlers correctly.
- `op_0x1b2_append_diagnostic_value@0x41f4b0` formats operand 1 through
`vm_operand_format_as_string@0x41af90`. Integer operand forms become signed decimal text, while string
forms return their byte strings; the helper also covers AGE's other operand families. The handler
appends the exact returned bytes to the embedded 28-byte MSVC string at `ctx+0x6f880`, now
`EngineCtx::diagnostic_text_buffer`.
- `op_0x1b3_append_diagnostic_newline@0x41a400` appends the two bytes at `0x57082c`, `0d 0a`, to that
same accumulator. It neither presents nor clears anything.
- `op_0x1b4_show_and_clear_diagnostic@0x419240` passes owner HWND
`EngineCtx::owner_window_handle` (`ctx+0x54fe8`), the accumulator's current byte pointer, and flags
`0x10004` through `engine_host_show_message@0x403820`. The thunk loads
`EngineCtx::host_ui_interface` from `ctx+4` and dispatches virtual slot `+4`; `ctx+0x54fe8` is an
explicit argument, not the interface object.
- The installed default vslot, `default_host_show_message@0x431520`, writes the raw diagnostic between
`"\r\n致命的エラー[\r\n"` / `"]\r\n"` trace markers, then calls
`age_show_message_box@0x409370`. Flag `0x10000` appends the current interpreter coordinate using
`"\n\nデバック情報\nFILE=%s ADDRESS=%X LINE=%d COMMAND=%s(%d) DEPTH=%d\n"`. Low mode `4`
formats body `%s%s`, selects caption `エラーが発生しました`, and invokes USER32 `MessageBoxA`
synchronously with style `0x40` (`MB_OK | MB_ICONINFORMATION`) and the supplied owner HWND. The
opcode ignores the `IDOK` result and only then erases the whole string with
`msvc_string_erase(buffer, 0, 0xffffffff)`.
- The optional command-metadata table at `ctx+0x6ddac` and per-frame source-line maps at
`ctx+0x6f798` are both zeroed by the constructor. Exhaustive immediate-offset xrefs show reads plus
that initialization only—no release-image writer—so Himegari deterministically emits
`LINE=-1 COMMAND=-(436)` for `0x1b4`. `FILE`, dword `ADDRESS`, and zero-based frame `DEPTH` remain
live values.
Himegari contains three `0x1b2` sites. FIELD appends the literal invalid-mutual-destruction-state text and
then CRLF. SYSTEM4 appends the literal invalid-execution-mode prefix, appends global
`system_flow_request` in decimal, presents and clears the assembled diagnostic, then appends CRLF to the
now-empty buffer. The trio therefore owns three effectful gaps totaling six instructions and is a bounded
next implementation slice; native ordering, including SYSTEM4's apparently odd post-clear newline, should
be preserved. A compatible host seam therefore needs synchronous modal presentation on the UI thread and
the current script coordinate; it is not a general shell-command interface.
The port implements the trio with a `GameSession`-shared `DiagnosticOutputState`. Godot marshals the
completed prompt to the main thread through `OS.Alert` and parks the VM until dismissal; headless
`CaptureHost` records the same effect. The accumulator clears after the synchronous call returns, so
FIELD's unpresented line and SYSTEM4's unusual post-clear CRLF retain their native lifetime and ordering.
**Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with **Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with
affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts
a type-0 command-map record keyed by arg 1: start `+4`, delay/duration `+8/+0xc`, target surface slot `+0x10`, a type-0 command-map record keyed by arg 1: start `+4`, delay/duration `+8/+0xc`, target surface slot `+0x10`,

View File

@@ -633,6 +633,30 @@ Both copies are attempted with overwrite allowed. Status is 0 when both succeed,
Port status (2026-07-24): implemented as an active ExecFrame identity marker. It survives nested calls and clears when its owning frame unwinds; the following full numbered-payload slice will consume the exposed zero-based cutoff. Port status (2026-07-24): implemented as an active ExecFrame identity marker. It survives nested calls and clears when its owning frame unwinds; the following full numbered-payload slice will consume the exposed zero-based cutoff.
### 0x1b2 `append-diagnostic-value` (u00425790, argc 1)
- **summary:** Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text.
- **grounding:** source=investigation, confidence=high
- **depended on by:** 0x1b3, 0x1b4
- **evidence:** The real /v2 dispatch slot ctx+0x9b914 registers op_0x1b2_append_diagnostic_value@0x41f4b0. It formats operand 1 through vm_operand_format_as_string@0x41af90, measures the returned NUL-terminated bytes, and appends them to the embedded MSVC string at ctx+0x6f880. Himegari has two literal-string sites and one SYSTEM4 global-integer site.
Implemented with an EngineCtx-lifetime accumulator shared across GameSession scene VMs. String operands append decoded text, integer and pointer forms append signed decimal, and float forms reproduce native `%lf`'s six fractional digits.
### 0x1b3 `append-diagnostic-newline` (u004257D0, argc 0)
- **summary:** Append the literal CRLF byte pair to the context diagnostic accumulator without presenting or clearing it.
- **grounding:** source=investigation, confidence=high
- **depends on:** 0x1b2
- **evidence:** The real /v2 dispatch slot ctx+0x9b918 registers op_0x1b3_append_diagnostic_newline@0x41a400. Its one operation appends two bytes from DAT_0057082c to the same embedded string at ctx+0x6f880; memory at that address is 0d 0a 00. Both Himegari sites immediately follow diagnostic construction.
Implemented as an exact `\r\n` append to the shared diagnostic accumulator, including FIELD's persistent line and SYSTEM4's native post-clear newline.
### 0x1b4 `show-and-clear-diagnostic` (u004237C0, argc 0)
- **summary:** Show the accumulated text plus native script-location context in a synchronous owner-window information message box, then erase the complete accumulator.
- **grounding:** source=investigation, confidence=high
- **depends on:** 0x1b2
- **evidence:** The real /v2 dispatch slot ctx+0x9b91c registers op_0x1b4_show_and_clear_diagnostic@0x419240. It resolves the MSVC string bytes at ctx+0x6f880 and calls engine_host_show_message@0x403820 with owner HWND ctx+0x54fe8 and flags 0x10004. The thunk dispatches host_ui_interface vslot +4; the default host implementation at 0x431520 logs the raw text and calls age_show_message_box@0x409370. Flag 0x10000 appends `FILE=%s ADDRESS=%X LINE=%d COMMAND=%s(%d) DEPTH=%d`; low mode 4 formats `%s%s`, uses caption `エラーが発生しました`, and calls USER32 MessageBoxA with style 0x40 (MB_OK|MB_ICONINFORMATION). The optional command metadata at ctx+0x6ddac and source-line maps at ctx+0x6f798 are zero-initialized and have no writer in Himegari's release image, yielding `LINE=-1 COMMAND=-(436)` at this opcode. The opcode ignores the return, then calls msvc_string_erase(buffer,0,0xffffffff). SYSTEM4's sole site presents its invalid-execution-mode text and value; a following 0x1b3 appends CRLF to the now-empty accumulator.
Implemented as a synchronous host effect. Himegari's release-only null debug tables make the native suffix deterministic (`LINE=-1 COMMAND=-(436)`); FILE, dword ADDRESS, and zero-based frame DEPTH come from the live instruction. Godot marshals `OS.Alert` to the main thread and parks the VM until dismissal; CaptureHost records the effect. The accumulator clears only after the host call returns.
### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1) ### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1)
- **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c. - **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
@@ -1973,18 +1997,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table. - **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.
### 0x1b2 `u00425790` (u00425790, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1b3 `u004257D0` (u004257D0, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1b4 `u004237C0` (u004237C0, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1be `u0041D9D0` (u0041D9D0, argc 2) ### 0x1be `u0041D9D0` (u0041D9D0, argc 2)
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -857,7 +857,22 @@ sites (five DEBUGADV demonstrations and one FIELD movement setup), reducing the
inventory to 15 distinct opcodes / 22 instructions. All 479 engine tests, opcode build/lint, the inventory to 15 distinct opcodes / 22 instructions. All 479 engine tests, opcode build/lint, the
zero-warning Godot build, and the Himegari-targeted threaded selftest pass. zero-warning Godot build, and the Himegari-targeted threaded selftest pass.
**NEXT:** investigate `0x1b2`, the widest remaining unknown at three sites. **Diagnostic-output trio implemented (2026-07-29):** `0x1b2(value)` now formats string, signed-integer,
pointer, and float operand families into an EngineCtx-lifetime accumulator; `0x1b3` appends exact CRLF;
and `0x1b4` appends the native interpreter coordinate, synchronously presents it, then clears only after
the host returns. The release image's optional debug-name/line tables are never populated, so the port
matches its deterministic `LINE=-1 COMMAND=-(436)` suffix while retaining live `FILE`, dword `ADDRESS`,
and zero-based frame `DEPTH`.
`GameSession` carries the accumulator across fresh scene VMs, preserving FIELD's unpresented invalid-state
line and SYSTEM4's post-clear newline. Interactive Godot dispatches `OS.Alert` on the main thread and parks
the VM until dismissal; headless `CaptureHost` records rather than drops the effect. Focused regressions
cover operand formatting, append/show/clear ordering, context formatting, headless presentation, and
cross-scene lifetime. All 482 engine tests, opcode/EngineCtx build and lint, the zero-warning Godot build,
and the Himegari-targeted threaded selftest pass. The six instructions are closed, reducing the remaining
effectful inventory from 15 distinct opcodes / 22 instructions to 12 / 16.
**NEXT:** rerank the remaining 12 effectful opcode gaps and investigate the widest coherent slice.
## Later Phase B breadth ## Later Phase B breadth

View 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);
}
}

View File

@@ -59,6 +59,8 @@ internal class RecordingHost : IHost
public readonly List<bool> MessageSkipChanges = new(); public readonly List<bool> MessageSkipChanges = new();
public readonly List<bool> PhysicalMessageSkipChanges = new(); public readonly List<bool> PhysicalMessageSkipChanges = new();
public readonly List<string> Warnings = 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<long> CursorResources = new();
public readonly List<bool> AdvPagePresentationSuspended = new(); public readonly List<bool> AdvPagePresentationSuspended = new();
public readonly List<(AdvLiveTextRun Run, int GlyphDelayMilliseconds)> LiveTextRuns = new(); public readonly List<(AdvLiveTextRun Run, int GlyphDelayMilliseconds)> LiveTextRuns = new();
@@ -67,6 +69,11 @@ internal class RecordingHost : IHost
public int CursorClearCount; public int CursorClearCount;
public int SceneContextResets; public int SceneContextResets;
public void ReportWarning(string message) => Warnings.Add(message); 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(int offset, string text) => Lines.Add((offset, text));
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds) public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
{ {

View File

@@ -2,6 +2,8 @@ namespace Age.Engine.Hosting;
public sealed class CaptureHost : IHost public sealed class CaptureHost : IHost
{ {
public List<(int Offset, string Text)> Emitted { get; } = new(); 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 ShowText(int offset, string text) => Emitted.Add((offset, text));
public void WaitForInput() { } public void WaitForInput() { }
public void Sleep(long duration) { } public void Sleep(long duration) { }

View File

@@ -28,6 +28,9 @@ public readonly record struct SurfaceRectCopy(
int SourceSurface, int DestinationSurface, int SourceX, int SourceY, int SourceSurface, int DestinationSurface, int SourceX, int SourceY,
int Width, int Height, int DestinationX, int DestinationY); 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 public enum SurfaceBlackFadeDirection
{ {
FromBlack, FromBlack,
@@ -38,6 +41,9 @@ public interface IHost
{ {
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary> /// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
void ReportWarning(string message) => System.Console.Error.WriteLine(message); 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. // Script context is retained for diagnostics/page location; resource operands are universal packed ids.
void EnterScriptContext(string scriptName) { } void EnterScriptContext(string scriptName) { }
void ExitScriptContext() { } void ExitScriptContext() { }

View 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();
}

View File

@@ -32,6 +32,8 @@ public sealed class GameSession
public AudioMixerSettings AudioMixerSettings { get; } public AudioMixerSettings AudioMixerSettings { get; }
/// <summary>The live retained ADV backlog shared by every VM run in this session.</summary> /// <summary>The live retained ADV backlog shared by every VM run in this session.</summary>
public AdvTextHistory TextHistory { get; } = new(); 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, public GameSession(SharedProfile? sharedProfile = null, INativeDatStore? nativeDatStore = null,
AudioMixerSettings? audioMixerSettings = null) AudioMixerSettings? audioMixerSettings = null)
@@ -51,7 +53,7 @@ public sealed class GameSession
{ {
var vm = new VirtualMachine( var vm = new VirtualMachine(
script, table, host, options, provider, sink, TextHistory, SharedProfile, NativeDatStore, 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 Globals) vm.Globals[kv.Key] = kv.Value;
foreach (var kv in GlobalFloats) vm.GlobalFloats[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; foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;

View File

@@ -29,9 +29,10 @@ public sealed class VirtualMachine
private const int HOTSPOT_RETURN = int.MinValue + 2; private const int HOTSPOT_RETURN = int.MinValue + 2;
private const int ROOT_RELOAD = int.MinValue + 3; private const int ROOT_RELOAD = int.MinValue + 3;
private const int SceneEntryCoroutineGate = 0xaba5c; 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_GSTRPTR = 8, T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12,
T_LSTRPTR = 14; T_LSTRPTR = 14;
private const string DiagnosticCaption = "エラーが発生しました";
private readonly Script _s; private readonly Script _s;
private readonly OpcodeTable _t; private readonly OpcodeTable _t;
@@ -41,6 +42,7 @@ public sealed class VirtualMachine
private readonly IScriptProvider? _provider; private readonly IScriptProvider? _provider;
private readonly SharedProfile _sharedProfile; private readonly SharedProfile _sharedProfile;
private readonly AudioMixerSettings _audioMixerSettings; private readonly AudioMixerSettings _audioMixerSettings;
private readonly DiagnosticOutputState _diagnosticOutput;
private readonly INativeDatStore? _nativeDatStore; private readonly INativeDatStore? _nativeDatStore;
private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1"; private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1";
private ExecFrame _cur = null!; private ExecFrame _cur = null!;
@@ -108,6 +110,7 @@ public sealed class VirtualMachine
public long Steps { get; private set; } public long Steps { get; private set; }
public bool AutoMessageEnabled => _autoMessageEnabled; public bool AutoMessageEnabled => _autoMessageEnabled;
public bool MessageSkipEnabled => _messageSkipEnabled; public bool MessageSkipEnabled => _messageSkipEnabled;
public string PendingDiagnosticText => _diagnosticOutput.PendingText;
/// <summary> /// <summary>
/// Zero-based active-frame cutoff selected by opcode 0x1ad, or null when no surviving marker /// 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. /// 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, IScriptProvider? provider = null, ITraceSink? sink = null,
AdvTextHistory? textHistory = null, SharedProfile? sharedProfile = null, AdvTextHistory? textHistory = null, SharedProfile? sharedProfile = null,
INativeDatStore? nativeDatStore = 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; _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -159,6 +163,7 @@ public sealed class VirtualMachine
_sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory(); _sink = sink ?? NullTraceSink.Instance; TextHistory = textHistory ?? new AdvTextHistory();
_sharedProfile = sharedProfile ?? new SharedProfile(); _sharedProfile = sharedProfile ?? new SharedProfile();
_audioMixerSettings = audioMixerSettings ?? new AudioMixerSettings(); _audioMixerSettings = audioMixerSettings ?? new AudioMixerSettings();
_diagnosticOutput = diagnosticOutput ?? new DiagnosticOutputState();
_nativeDatStore = nativeDatStore; _nativeDatStore = nativeDatStore;
_messageWindowAlphaSetting = host.MessageWindowAlphaSetting; _messageWindowAlphaSetting = host.MessageWindowAlphaSetting;
_messageGlyphDelayMilliseconds = System.Math.Max(0, host.MessageGlyphDelayMilliseconds); _messageGlyphDelayMilliseconds = System.Math.Max(0, host.MessageGlyphDelayMilliseconds);
@@ -644,6 +649,33 @@ public sealed class VirtualMachine
? ReadStr(operand) ? ReadStr(operand)
: unchecked((int)Read(operand)).ToString(System.Globalization.CultureInfo.InvariantCulture); : 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 RootReloadRequestedException : Exception { }
private sealed class NumberedRestoreRequestedException : Exception { } private sealed class NumberedRestoreRequestedException : Exception { }
private sealed class ProcessExitRequestedException : Exception { } private sealed class ProcessExitRequestedException : Exception { }
@@ -1194,6 +1226,19 @@ public sealed class VirtualMachine
case "halve-strlen": // 0x1a6: strlen(native encoded bytes) >> 1 case "halve-strlen": // 0x1a6: strlen(native encoded bytes) >> 1
Write(a[0], NativeStringByteLength(ReadStr(a[1])) >> 1); Write(a[0], NativeStringByteLength(ReadStr(a[1])) >> 1);
return pc + 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 case "is-catalog-resource-unlocked": // 0x19d
Write(a[0], _sharedProfile.IsCatalogResourceUnlocked(Read(a[1])) ? 1 : 0); Write(a[0], _sharedProfile.IsCatalogResourceUnlocked(Read(a[1])) ? 1 : 0);
return pc + 1; return pc + 1;

View File

@@ -26,6 +26,7 @@ public sealed class GodotAdvHost : IHost
private readonly Stack<string> _scriptContexts = new(); private readonly Stack<string> _scriptContexts = new();
private readonly ScriptPresentationBarrier _presentationBarrier = new(); private readonly ScriptPresentationBarrier _presentationBarrier = new();
private readonly AutoResetEvent _presentationRequestConsumed = new(false); private readonly AutoResetEvent _presentationRequestConsumed = new(false);
private readonly AutoResetEvent _diagnosticMessageCompleted = new(false);
private readonly bool _synchronizeExplicitPresentation; private readonly bool _synchronizeExplicitPresentation;
private long _explicitPresentationRequestGeneration; private long _explicitPresentationRequestGeneration;
private long _consumedPresentationRequestGeneration; private long _consumedPresentationRequestGeneration;
@@ -117,6 +118,20 @@ public sealed class GodotAdvHost : IHost
public void ReportWarning(string message) => System.Console.Error.WriteLine(message); public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
public void ShowDiagnosticMessage(DiagnosticMessage message)
{
if (_stopping) return;
_timeline?.Event("diagnostic-message", new()
{
["caption"] = message.Caption,
["text"] = message.Text,
});
_main.CallDeferred("ShowAgeDiagnostic", message.Text, message.Caption);
while (!_stopping && !_diagnosticMessageCompleted.WaitOne(50)) { }
}
public void CompleteDiagnosticMessage() => _diagnosticMessageCompleted.Set();
private string CurrentScene private string CurrentScene
{ {
get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; } get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; }
@@ -950,6 +965,7 @@ public sealed class GodotAdvHost : IHost
if (_gate.CurrentCount == 0) _gate.Release(); if (_gate.CurrentCount == 0) _gate.Release();
_inputCallbackSignal.Set(); _inputCallbackSignal.Set();
_frameSignal.Set(); _frameSignal.Set();
_diagnosticMessageCompleted.Set();
} }
public void ResetSceneContext() public void ResetSceneContext()

View File

@@ -912,6 +912,22 @@ public partial class Main : Godot.Control
_ageCursorTexture = null; _ageCursorTexture = null;
} }
public void ShowAgeDiagnostic(string text, string caption)
{
try
{
OS.Alert(text, caption);
}
catch (System.Exception error)
{
GD.PushError($"[diagnostic] native alert failed: {error.Message}");
}
finally
{
_host?.CompleteDiagnosticMessage();
}
}
public override void _ExitTree() public override void _ExitTree()
{ {
bool vmStopped = true; bool vmStopped = true;

View File

@@ -54,6 +54,9 @@ INFERRED: dict[int, dict] = {
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'), 0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'),
0x1a6: dict(name='half-byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write half the resolved string's byte length, using integer truncation."), 0x1a6: dict(name='half-byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write half the resolved string's byte length, using integer truncation."),
0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."), 0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."),
0x1b2: dict(name='append-diagnostic-value', category='control', noop=False, confidence='high', source='investigation', summary="Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text."),
0x1b3: dict(name='append-diagnostic-newline', category='control', noop=False, confidence='high', source='investigation', summary='Append the literal CRLF byte pair to the context diagnostic accumulator without presenting or clearing it.'),
0x1b4: dict(name='show-and-clear-diagnostic', category='control', noop=False, confidence='high', source='investigation', summary='Show the accumulated text plus native script-location context in a synchronous owner-window information message box, then erase the complete accumulator.'),
0x1b5: dict(name='set-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(milliseconds) - set the per-glyph ADV text reveal delay; zero makes retained UI text publish immediately.'), 0x1b5: dict(name='set-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(milliseconds) - set the per-glyph ADV text reveal delay; zero makes retained UI text publish immediately.'),
0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'), 0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'),
0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'), 0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'),

View File

@@ -7,6 +7,11 @@
struct_name = "EngineCtx" struct_name = "EngineCtx"
size = 0xa1000 size = 0xa1000
[[field]]
offset = 0x4
name = "host_ui_interface"
type = "void*"
note = "host-facing UI interface used by the ctx+4 virtual-dispatch thunks; default slot +4 logs and presents AGE message boxes"
[[field]] [[field]]
offset = 0x814 offset = 0x814
name = "input_action_count" name = "input_action_count"
@@ -248,6 +253,11 @@ name = "frame_instruction_word_count"
type = "int" type = "int"
note = "current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4" note = "current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4"
[[field]] [[field]]
offset = 0x54fe8
name = "owner_window_handle"
type = "void*"
note = "native owner HWND forwarded to host UI/message-box calls, including op 0x1b4 diagnostic presentation"
[[field]]
offset = 0x54ff0 offset = 0x54ff0
name = "initial_root_run" name = "initial_root_run"
type = "int" type = "int"
@@ -453,6 +463,21 @@ name = "message_skip_queued_voice_arg"
type = "int" type = "int"
note = "second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero" note = "second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero"
[[field]] [[field]]
offset = 0x6ddac
name = "opcode_debug_metadata_table"
type = "void*"
note = "base of 512 optional per-opcode debug-metadata pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic COMMAND names render as '-'"
[[field]]
offset = 0x6f798
name = "script_source_line_table"
type = "void*"
note = "base of 41 optional per-frame source-line-map pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic LINE values render as -1"
[[field]]
offset = 0x6f880
name = "diagnostic_text_buffer"
type = "int"
note = "base of an embedded 28-byte MSVC string accumulator; op 0x1b2 appends formatted operand text, op 0x1b3 appends CRLF, and op 0x1b4 presents then clears it"
[[field]]
offset = 0x6f89c offset = 0x6f89c
name = "script_launch_queue" name = "script_launch_queue"
type = "int" type = "int"

View File

@@ -3904,18 +3904,19 @@ argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00425790" name = "append-diagnostic-value"
category = "unknown" category = "control"
summary = "" summary = "Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text."
details = "Implemented with an EngineCtx-lifetime accumulator shared across GameSession scene VMs. String operands append decoded text, integer and pointer forms append signed decimal, and float forms reproduce native `%lf`'s six fractional digits."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = "The real /v2 dispatch slot ctx+0x9b914 registers op_0x1b2_append_diagnostic_value@0x41f4b0. It formats operand 1 through vm_operand_format_as_string@0x41af90, measures the returned NUL-terminated bytes, and appends them to the embedded MSVC string at ctx+0x6f880. Himegari has two literal-string sites and one SYSTEM4 global-integer site."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "value to format and append"
observed_types = ["string", "g-int"] observed_types = ["string", "g-int"]
[[opcode]] [[opcode]]
@@ -3925,14 +3926,15 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u004257D0" name = "append-diagnostic-newline"
category = "unknown" category = "control"
summary = "" summary = "Append the literal CRLF byte pair to the context diagnostic accumulator without presenting or clearing it."
details = "Implemented as an exact `\\r\\n` append to the shared diagnostic accumulator, including FIELD's persistent line and SYSTEM4's native post-clear newline."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = [0x1b2]
evidence = "" evidence = "The real /v2 dispatch slot ctx+0x9b918 registers op_0x1b3_append_diagnostic_newline@0x41a400. Its one operation appends two bytes from DAT_0057082c to the same embedded string at ctx+0x6f880; memory at that address is 0d 0a 00. Both Himegari sites immediately follow diagnostic construction."
[[opcode]] [[opcode]]
op = 0x1b4 op = 0x1b4
@@ -3941,14 +3943,15 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u004237C0" name = "show-and-clear-diagnostic"
category = "unknown" category = "control"
summary = "" summary = "Show the accumulated text plus native script-location context in a synchronous owner-window information message box, then erase the complete accumulator."
details = "Implemented as a synchronous host effect. Himegari's release-only null debug tables make the native suffix deterministic (`LINE=-1 COMMAND=-(436)`); FILE, dword ADDRESS, and zero-based frame DEPTH come from the live instruction. Godot marshals `OS.Alert` to the main thread and parks the VM until dismissal; CaptureHost records the effect. The accumulator clears only after the host call returns."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = [0x1b2]
evidence = "" evidence = "The real /v2 dispatch slot ctx+0x9b91c registers op_0x1b4_show_and_clear_diagnostic@0x419240. It resolves the MSVC string bytes at ctx+0x6f880 and calls engine_host_show_message@0x403820 with owner HWND ctx+0x54fe8 and flags 0x10004. The thunk dispatches host_ui_interface vslot +4; the default host implementation at 0x431520 logs the raw text and calls age_show_message_box@0x409370. Flag 0x10000 appends `FILE=%s ADDRESS=%X LINE=%d COMMAND=%s(%d) DEPTH=%d`; low mode 4 formats `%s%s`, uses caption `エラーが発生しました`, and calls USER32 MessageBoxA with style 0x40 (MB_OK|MB_ICONINFORMATION). The optional command metadata at ctx+0x6ddac and source-line maps at ctx+0x6f798 are zero-initialized and have no writer in Himegari's release image, yielding `LINE=-1 COMMAND=-(436)` at this opcode. The opcode ignores the return, then calls msvc_string_erase(buffer,0,0xffffffff). SYSTEM4's sole site presents its invalid-execution-mode text and value; a following 0x1b3 appends CRLF to the now-empty accumulator."
[[opcode]] [[opcode]]
op = 0x1b5 op = 0x1b5