From 950d9088cbc87973748f46fbbb560390dba074cd Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 09:29:47 -0400 Subject: [PATCH] Extract VM script lifecycle handler --- docs/PROJECT-STRUCTURE.md | 4 +- docs/remake-architecture-and-roadmap.md | 13 +- .../Vm/VirtualMachine.ScriptLifecycle.cs | 142 ++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 119 +-------------- 4 files changed, 157 insertions(+), 121 deletions(-) create mode 100644 engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index f61a1e7..a21398f 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -208,7 +208,9 @@ inline arrays, rectangle search and stable index sorting, bounded integer queues and native-style random-modulo dispatch; shared storage/address helpers remain in the coordinator. `engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs` owns local jumps/calls/returns, value-switch construction, ADV coroutine handler save/yield/resume, and bounded labeled-yield dispatch; process/root exit and cross-script -lifecycle remain in `VirtualMachine.cs`. +lifecycle are routed separately. `engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs` owns process/frame/root +exit, ordinary cross-script calls, mounted append autoruns, and preloaded script-slot load/call dispatch; frame +execution, script-provider access, and shared lifecycle state remain in `VirtualMachine.cs`. The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 7b5467c..5da7583 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -714,6 +714,13 @@ do not mix mechanical moves with semantic changes. available for labeled-yield opcode/offset diagnostics. Process/root exit and cross-script lifecycle remain in the coordinator. Runtime validation remains green. + The fifteenth bounded `VirtualMachine.Step` extraction moved process/frame/root exit, ordinary cross-script + calls, mounted append autoruns, and preloaded script-slot load/call dispatch into + `engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs`. The top-level dispatcher retains all labels and + aliases at their existing positions and routes them through guarded `StepScriptLifecycle`; frame execution, + provider access, call-depth enforcement, and shared lifecycle state remain in the coordinator. Runtime + validation remains green. + **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -1087,8 +1094,8 @@ Continue step 2 of the **codebase consolidation** maintenance slice: behavior-ne by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState` domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, text-history, ADV-service, input, timing, persistence, and memory/collection `VirtualMachine.Step` families routed -through domain handlers, with control-flow/coroutine dispatch now isolated as well, extract process/root-exit and -cross-script lifecycle dispatch next without replacing the proven dispatcher or changing public types, commands, -and generated output. +through domain handlers, with control-flow/coroutine and process/root-exit/cross-script lifecycle dispatch now +isolated as well, extract scalar arithmetic and string-value dispatch next without replacing the proven +dispatcher or changing public types, commands, and generated output. Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs b/engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs new file mode 100644 index 0000000..cd3fd25 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs @@ -0,0 +1,142 @@ +using Age.Engine.Diagnostics; +using Age.Engine.Model; + +namespace Age.Engine.Vm; + +public sealed partial class VirtualMachine +{ + private int StepScriptLifecycle(string label, IReadOnlyList a, int pc) + { + switch (label) + { + 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; + } + default: + throw new InvalidOperationException($"Non-script-lifecycle opcode routed to script-lifecycle handler: {label}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 5f9585b..eb66de2 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1361,131 +1361,16 @@ public sealed partial class VirtualMachine case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only return StepControlFlow(label, ins, pc); 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": 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; - } + return StepScriptLifecycle(label, a, pc); case "show-text": case "define-adv-text-layout": case "reset-adv-text-layout":