diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index b827038..6001786 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -200,6 +200,9 @@ resources and virtual position, raw mouse/joystick callback registration and dis physical-input mapping; public host-thread input entry points and shared synchronization remain in the coordinator. `engine/Age.Engine/Vm/VirtualMachine.Timing.cs` owns the monotonic-time query, host sleep, relative timed-callback schedule construction, deadline/catch-up selection, and callback resumption opcode handler. +`engine/Age.Engine/Vm/VirtualMachine.Persistence.cs` owns catalog-unlock lookup, numbered save/load and nested +restore continuation, metadata/copy/delete, thumbnail persistence, and shared-profile integer/string opcode +dispatch; capture/apply helpers and persistent coordinator 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 2a036af..b8dd642 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -694,6 +694,12 @@ do not mix mechanical moves with semantic changes. their existing positions and routes the separated groups through guarded `StepTiming`; animation-frame sampling remains with `StepAnimation`. Runtime validation remains green. + The twelfth bounded `VirtualMachine.Step` extraction moved catalog-unlock lookup, numbered save/load and + nested restore continuation, metadata/copy/delete, thumbnail persistence, and shared-profile integer/string + dispatch into `engine/Age.Engine/Vm/VirtualMachine.Persistence.cs`. The top-level dispatcher retains all + labels at their existing positions and routes them through guarded `StepPersistence`; capture/apply helpers + and persistent coordinator state remain in `VirtualMachine.cs`. 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. @@ -1067,7 +1073,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, and timing `VirtualMachine.Step` families routed through domain handlers, -extract numbered-save/shared-profile persistence dispatch next without replacing the proven dispatcher or -changing public types, commands, and generated output. +with numbered-save/shared-profile persistence dispatch now isolated as well, extract the memory/collection utility +opcode family 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.Persistence.cs b/engine/Age.Engine/Vm/VirtualMachine.Persistence.cs new file mode 100644 index 0000000..8429677 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.Persistence.cs @@ -0,0 +1,242 @@ +using Age.Engine.Diagnostics; +using Age.Engine.Model; +using Age.Engine.Persistence; + +namespace Age.Engine.Vm; + +public sealed partial class VirtualMachine +{ + private int StepPersistence(string label, IReadOnlyList a, int pc) + { + switch (label) + { + 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; + } + default: + throw new InvalidOperationException($"Non-persistence opcode routed to persistence handler: {label}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index dec90ab..e556470 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1292,231 +1292,25 @@ public sealed partial class VirtualMachine _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 "is-catalog-resource-unlocked": 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(); - } + return StepPersistence(label, a, pc); 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; - } + return StepPersistence(label, a, pc); 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; - } + return StepPersistence(label, a, pc); 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; - } + return StepPersistence(label, a, pc); case "strlen": // 0x2c5: raw strlen(native encoded bytes) Write(a[0], NativeStringByteLength(ReadStr(a[1]))); return pc + 1;