Implement native layout-3 save restoration

This commit is contained in:
gamer147
2026-07-24 16:23:01 -04:00
parent 8fe610b66d
commit 001a5706f6
21 changed files with 1659 additions and 81 deletions

View File

@@ -104,7 +104,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── Age.Engine/Sys4/ runtime catalog parser, loose-first bounded ALF asset store,
│ script provider, AGF/LZSS and Windows CUR decoders, and resource facade
│ └── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
numbered DAT/STH pair + BMP codec, and profile-owned state
layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
│ and profile-owned state
├── native/ authored native runtime boundaries
│ └── age_movie_ffmpeg/ project-owned FFmpeg C ABI, immutable Windows dependency manifest,
│ and bootstrap/build scripts (outputs stay under disposable build/)

View File

@@ -1801,8 +1801,44 @@ validates its distinct compatibility id, queries metadata without decoding paylo
paired `.DAT`/`.STH` copy/delete status layering. `NumberedThumbnailCodec` reads and writes the native BMP
dialect through host surface capture/replacement, and ops `0x1a0`, `0x1ab``0x1af` (including `0x1ad`'s
frame marker) are wired. Godot injects the store at `user://SAVE`. Full layout-3 global/frame/history/
resource/retained-gfx serialization and the `0x19e`/`0x1a1`/`0xae` save-resume path remain the next layer;
the existing `GameSession` JSON snapshot is unchanged.
`NativeNumberedSaveCodec`, `NativeTextHistoryCodec`, and `NativeGfxPersistenceCodec` now own the complete
layout-3 numbered body and appended history tail. The VM wires `0x19e`, data-only `0x19f`, full load
`0x1a1`, and the active branch of `0xae`; the existing `GameSession` JSON snapshot remains a separate
diagnostic/extended-mode surface.
#### Layout-3 byte map and port correspondence (2026-07-24)
The decoded layout-3 body begins with `cutoff` at `+0x00`; its fixed length is
`0x5718 + cutoff*0x414`. The fixed prefix contains two saved engine/context words, ten state DWORDs,
100 three-DWORD resource reload records, 1,000 20-byte surface reload records, then frames beginning at
`+0x5304`. Each `0x414`-byte frame contains parent context, packed script id, up to 256 saved local-return
indices, and two terminal control indices. The three SYS4 footer tables now have explicit runtime roles:
- d259 indexes T1/F7-F8, the op-`0x71` read-message reset sites;
- d260 indexes T2/F9-F10, resumable `call-script` sites (forced to `-1` on the cutoff frame);
- each local return index addresses T3/F11-F12, whose stored target plus three is the live return offset.
After the fixed bytes are six bank counts followed by the ordinary (de-protected) integer bank, float
storage, a DWORD-sized CP932 string blob, the pointer-family banks, and retained graphics. Graphics begin
with record size `0x2d4` and object count; every object is `{handle,0x2d4 raw record}`. The range-transform
first/count and another `0x2d4` record follow. AGE overallocates this region
(`0x2e1 + objectCount*0x2d8` DWORDs) relative to the records it writes; the codec preserves the native
logical sizing and zeroes the unused allocation slack.
The text-history tail is outside the S4SD container. Its 12-byte header is
`{logicalBytes,logicalBytes,storedBytes}` followed by the same 4 KiB-ring LZSS/verbatim dialect. Logical
history is entry count + 8-byte entries, record count + the first 11 DWORDs of each retained record, then
a DWORD-counted CP932 string blob whose DWORDs are bitwise inverted. Installed `SAVE00.DAT` validates the
whole boundary read-only: cutoff 1, six counts `[402459,1,789,1,1,1]`, 211 gfx objects, a 21-byte history
tail, and exact consumption of the S4SD container before that tail.
The port tracks the native global banks separately at runtime, captures the marked frame chain, serializes
live surfaces and retained objects, and reloads host textures from the 20-byte surface records. Full load
first replaces banks/history/gfx, unwinds the obsolete managed call chain, runs `CALLBACK_LOAD.BIN` when
the mounted script provider resolves it, starts the saved root at its `0xae` rendezvous, recursively
reconstructs child frames, resumes parents after their saved T2 call sites,
and finally resumes the terminal frame at its T1 boundary. Successful `0x19e` also flushes shared
`SAVE.DAT`/`RT.DAT`, matching `context_state_serialize`.
### Opcode `0xae` continues numbered-save stack restoration (2026-07-20)
@@ -1815,14 +1851,13 @@ loads `CALLBACK_LOAD.BIN` or the saved entry script, and resets the current cont
On an active restore, the handler reads `set:SaveVersion1`/`set:SaveVersion2`, selects the matching saved
frame layout, replaces the current frame PC with its saved resume or call target, and advances through the
serialized contexts. At the saved terminal context it clears the restore flag and reinstates the saved
context/return state. The corpus placement supports that control-flow role: 305 calls overwhelmingly follow
serialized contexts. `script_frame_restore_saved_layout@0x40f2d0` loads each packed script and expands its
T1/T2/T3 indices back into live PC/call/return offsets. At the saved terminal context the handler clears
the restore flag and reinstates the saved context/return state. The corpus placement supports that control-flow role: 305 calls overwhelmingly follow
coroutine-resume or call boundaries, including SC0000's main-loop resume sequence.
The opcode remains an effectful port gap. It is a no-op during every currently reachable port execution,
but its actual branch cannot be implemented until numbered saves serialize and restore the active
`ExecFrame` chain. Counting an unconditional no-op as coverage would conceal that dependency, so it stays
grouped with `0x1ad` rather than receiving a placeholder VM case.
The port now implements both branches: an ordinary one-instruction no-op outside restoration, and the
T1/T2/T3-driven managed-frame reconstruction described above while a full numbered load is active.
### ADV read-message Skip and shared `RT.DAT` history (2026-07-18)
@@ -1947,10 +1982,9 @@ finds typed metadata; op `0x1d4` finds the voice pair. `HISTORY.BIN` dispatches
History is independent of `RT.DAT`, but native full save fidelity does serialize the live backlog.
`text_history_serialize@0x451d00` writes the index and packed records/strings after context/numbered-save
serialization, and `text_history_deserialize@0x456130` restores them on the matching load path. This is a
future numbered-save integration seam, not a reason to choose a shared-profile backend now: an in-memory
History button can be complete first, while save/load restoration stays deferred with the wider storage
architecture decision.
serialization, and `text_history_deserialize@0x456130` restores them on the matching load path. The port's
`NativeTextHistoryCodec` now implements that appended tail directly against the same live
`AdvTextHistory` model; it remains independent of shared profile state and `RT.DAT`.
History's display support consists of the ordinary presentation operations: primary/ruby font sizes
(`0x75/0x197`), font weight (`0x2bd`), colors/effect mode/offset (`0x76/0x77/0x78/0x1a4`), layout origin

View File

@@ -400,7 +400,9 @@ Implemented as a whole-stack root-reload boundary in the persistent VM. A reques
### 0xae `continue-save-load-stack-restore` (u00415130, argc 0)
- **summary:** () - during serialized save restoration, replace the current frame PC with its saved resume/call target and advance through the saved script-context stack; otherwise a no-op.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with FUN_0040f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack.
- **evidence:** Ghidra /v2: op_0xae_continue_save_load_stack_restore@0x416790 first tests ctx+0x53d24 (set by save_data_deserialize_and_begin_restore@0x40fd10). When clear it returns. When set, it selects the serialized frame layout through set:SaveVersion1/2, restores the current PC from that layout's saved return/call target, advances through contexts with script_frame_restore_saved_layout@0x40f2d0, and clears the restore flag on reaching the saved terminal context. Its 305 corpus sites overwhelmingly follow coroutine-resume/call boundaries, which provide the rendezvous points used while reconstructing the stack.
Layout 3 frame d259 indexes SYS4 T1 read-message reset sites, d260 indexes T2 call-script sites, and the saved local return stack indexes T3 local-call sites. Port status (2026-07-24): the active path reconstructs the saved recursive frame chain and resumes the terminal frame at its T1 boundary.
### 0xc8 `sleep` (sleep, argc 1)
- **summary:** Pause the current script for <duration> milliseconds while retained presentation continues.
@@ -476,14 +478,14 @@ record-zero failure. Natural SYSTEM4 boot proves BTANINIT2 -> `$1$AUTORUN.BIN` -
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19e_save_numbered_slot@0x4278b0 formats SAVE%2.2d.DAT, checks an existing header and prompts before replacing an incompatible file, opens CREATE_ALWAYS, reads set:SaveVersion2 then set:SaveVersion1, and calls context_state_serialize@0x40d320. Corpus: two calls in SAVE.BIN and SELSTAGE.BIN.
Uses `set:SaveVersion1` and `set:SaveVersion2` to choose the numbered payload layout. Status is 0 on success and 1 on refusal, open/create failure, or serializer failure. Numbered `.DAT` files do not use the shared profile's temp/backup replacement scheme.
Uses `set:SaveVersion1` and `set:SaveVersion2` to choose the numbered payload layout. Status is 0 on success and 1 on refusal, open/create failure, or serializer failure. Numbered `.DAT` files do not use the shared profile's temp/backup replacement scheme. Port status (2026-07-24): layout 3 writes fixed state, six native banks, T1/T2/T3 frame records, surface/resource reload state, retained gfx records, the appended history tail, and then flushes shared SAVE.DAT/RT.DAT.
### 0x19f `load-numbered-slot-data-only` (load-numbered-slot-data-only, argc 2)
- **summary:** (status_out)(slot) - decode `SAVE%02d.DAT` without restoring the active script-frame chain or text history.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19f_load_numbered_slot_data_only@0x427a40 opens SAVE%2.2d.DAT and calls save_data_deserialize_and_begin_restore@0x40fd10(handle, SaveVersion1, SaveVersion2, 0, 0), then decodes protected integer globals. Missing/open failure writes status 1; otherwise the decoder result is returned. Corpus count: 0.
This is the data-only companion to full-resume opcode 0x1a1. It selects the configured SaveVersion layout and restores serialized state with both runtime/history restore flags clear. Himegari's shipped script corpus does not call it, but it belongs to the shared SYS4 persistence ABI.
This is the data-only companion to full-resume opcode 0x1a1. It selects the configured SaveVersion layout and restores serialized state with both runtime/history restore flags clear. Himegari's shipped script corpus does not call it, but it belongs to the shared SYS4 persistence ABI. Port status (2026-07-24): implemented for layout 3 through the same bank/resource/gfx decoder without activating history or frame restoration.
### 0x1a0 `query-numbered-save-metadata` (query-numbered-save-metadata, argc 9)
- **summary:** (status_out)(slot)(year)(month)(day)(hour)(minute)(second)(playtime_seconds) - validate a numbered `.DAT` header and return its timestamp and accumulated playtime.
@@ -497,7 +499,7 @@ Status 0 means valid metadata was written, 1 means the file could not be opened,
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1a1_load_numbered_slot_and_resume@0x427d30 calls save_data_deserialize_and_begin_restore@0x40fd10(handle, SaveVersion1, SaveVersion2, 1, 1), closes the file, and decodes protected integer globals. The loader activates the saved-frame state consumed by op_0xae. Corpus: one call in SAVE.BIN.
The caller pre-seeds status to zero. A missing/open failure writes 1; success starts the asynchronous native stack-restoration rendezvous and does not overwrite that zero. This is the ordinary load-game path, unlike data-only opcode 0x19f.
The caller pre-seeds status to zero. A missing/open failure writes 1; success starts the asynchronous native stack-restoration rendezvous and does not overwrite that zero. This is the ordinary load-game path, unlike data-only opcode 0x19f. Port status (2026-07-24): layout 3 restores banks, history, surfaces/resources, and retained gfx, unwinds the obsolete managed call chain, then reconstructs saved frames through opcode 0xae.
### 0x1a2 `store-shared-profile-int` (store-shared-profile-int, argc 1)
- **summary:** 0x1a2 (cell) — snapshot the selected global integer cell into AGE's shared SAVE.DAT profile table. Its native key is raw type byte `0x03` followed by eight lowercase ASCII hex digits for the lvalue's resolved global-bank index; the stored value is the cell's current raw 32-bit value. Insert-or-assign semantics replace an existing entry.

View File

@@ -3468,6 +3468,37 @@ numbered-load restoration rendezvous `0xae` remains effectful and unimplemented
the active branch of `0xae`; retain `0x19f` as the mapped but corpus-unused data-only load path. JSON
inspection/export remains an additive extended-mode feature.
### Persistence implementation step 5 — complete layout-3 numbered save/load (2026-07-24)
The numbered body and resume path are now implemented as native binary compatibility mode.
`NativeNumberedSaveCodec` reads/writes the `0x5718 + cutoff*0x414` fixed state, six separate native
global banks, CP932 string blob, 100 resource records, 1,000 surface records, retained `0x2d4`-byte gfx
objects, and range transform. `NativeTextHistoryCodec` owns the separate 12-byte-header LZSS history tail.
The installed read-only `SAVE00.DAT` oracle decodes end to end: its container/body boundary, cutoff,
`[402459,1,789,1,1,1]` banks, 211 retained objects, and 21-byte history tail all agree with the native
serializer allocation and write order.
SYS4 parsing now retains all three footer tables. T1 is the saved message/reset resume table, T2 is the
saved call-script table, and T3 maps intra-script returns. `ExecFrame.Pc` is kept current at opcode
boundaries so `0x19e` captures frames root-through-`0x1ad` cutoff accurately. Full `0x1a1` load restores
the global banks, history, reloadable surfaces, retained objects, and playtime, then deliberately unwinds
the obsolete C# recursion, executes `CALLBACK_LOAD.BIN` when available, and enters the restore rendezvous.
The active `0xae` path rebuilds the saved nested frames and resumes the terminal frame through T1;
inactive `0xae` stays a no-op. Corpus-unused `0x19f` shares the state decoder without
activating frame/history restoration. Successful `0x19e` appends history and flushes shared SAVE.DAT/RT.DAT.
The VM now carries integer, float, string, integer-pointer, and string-pointer banks separately across
`GameSession`, correcting the prior flat-bank approximation while keeping compatibility fallbacks for
older synthetic pointer tests. Focused coverage includes logical byte round-trips, CP932/LZSS history,
container-tail preservation, native installed-save import, opcode save integration, retained gfx/surface
restore, and a two-frame `0xae` reconstruction.
Validation: all 383 engine tests pass, including the installed read-only layout-3 oracle; SC0000 is now
129/129 distinct opcodes handled. Opcode sources were regenerated after recording the T1/T2/T3 and
implementation status. The remaining 1.0 persistence work is
gameplay validation through Himegari's real SAVE.BIN UI and any corrections that reveals; JSON inspection,
namespaced mod data, and migrations remain extended-mode work.
## Data-semantics sidebar: focused append EBINIT inspection (2026-07-24)
The static INIT surface now accepts a universal packed script id for focused append inspection.

View File

@@ -39,12 +39,12 @@ Verified across all 481 files.
```
off field meaning evidence
0x00 magic "SYS4422 " (0x53 59 53 34 34 32 32 20) 481/481 identical
0x08 F0 scenario/script id or flags 98 distinct; 0x5 dominant (252×)
0x0C F1 = 1 always 481/481 == 1
0x10 F2 = 1 (457×) or 2 (20×); 4 outliers format/feature flag
0x14 F3 0x06 (268×),0x04,0x01,0x05... 15 distinct — minor version?
0x18 F4 = 1 always 481/481 == 1
0x1C F5 = 2 (319×) or 1 (144×); rarely 3/5 flag
0x08 F0 local integer count
0x0C F1 local float count
0x10 F2 local string count
0x14 F3 local integer-pointer count
0x18 F4 second pointer-family count (exact subtype unresolved)
0x1C F5 local string-pointer count
0x20 F6 = 0x1C always 481/481 == 0x1C (header-size marker)
0x24 F7 table-1 entry count see below
0x28 F8 table-1 offset == code-section length ordering F8<=F10<=F12<=EOF
@@ -65,8 +65,8 @@ consistent with the SYS4/SYS5 family.
```
body[0 .. F8) CODE bytecode instruction stream
body[F8 .. F10) TABLE-1 (F7 entries, 1 dword each) -> read-message boundaries (op 0x71)
body[F10 .. F12) TABLE-2 (F9 entries, 1 dword each) -> targets of type 0x03
body[F12 .. EOF) TABLE-3 (F11 entries, 1 dword each) -> targets of type 0x8F
body[F10 .. F12) TABLE-2 (F9 entries, 1 dword each) -> call-script sites (op 0x03)
body[F12 .. EOF) TABLE-3 (F11 entries, 1 dword each) -> local-call sites (op 0x8F)
```
Every table entry is exactly **1 dword** — a pointer (dword index into body).
@@ -81,16 +81,15 @@ tag identifying the pointed-to construct:
| Table | count/off | Target dword tag | Hits | Meaning (inferred) |
|---|---|---|---|---|
| T1 | F7 / F8 | **0x71** | 26,445/26,445 | per-script read-message boundary index |
| T2 | F9 / F10 | **0x03** | 3,018/3,018 | data/variable entries (operand at +2 large, e.g. addresses) |
| T3 | F11 / F12| **0x8F** | 72,941/72,941 | instruction/line entries (largest table; operand at +2 huge) |
| T2 | F9 / F10 | **0x03** | 3,018/3,018 | resumable call-script sites |
| T3 | F11 / F12| **0x8F** | 72,941/72,941 | local-call sites used to reconstruct return stacks |
100% type purity — not a single target had a different tag. Native
`read_text_db_find_message_index@0x468f50` searches T1 for the code DWORD coordinate most recently
snapshotted by op `0x71`; its zero-based entry index and F7 count are the message index/count stored in
shared `RT.DAT`. Thus T1 is not a generic label table even though every entry is a control-structure
site. T3 is the big one
(~73k entries corpus-wide), consistent with it being a per-instruction or
per-source-line index (a debug/line table). T1 ≈ labels, T2 ≈ a smaller symbol set.
site. Numbered layout-3 frame serialization proves the remaining roles: saved d259 indexes T1,
d260 indexes T2, and each intra-script return maps through T3 (restored as `T3[index]+3`).
## Instruction stream — PARTIAL
@@ -161,7 +160,6 @@ The header format is identical (same magic/layout) so tooling is copy-agnostic.
- Opcode dispatch — confirm tagged-operand model, enumerate opcodes
- Meaning of F0/F2/F3/F5 flag fields
- Exact operand grammar per instruction (how many dwords each opcode consumes)
- Semantics of T1/T2/T3 beyond "label/data/line" guesses
## Loader — DONE