Implement numbered save pair lifecycle
This commit is contained in:
@@ -104,7 +104,7 @@ 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,
|
||||
│ profile-owned state, and shared/numbered DAT lifecycle seam
|
||||
│ numbered 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/)
|
||||
|
||||
@@ -1610,12 +1610,11 @@ Corpus placement agrees with the native dataflow: 1,928 executions appear across
|
||||
return paths from `HISTORY`, `MENU`, `HIDEWIN`, and `INPUTNAME`. Those calls re-establish the enclosing ADV
|
||||
frame as the safe numbered-save resume point after modal/nested scripts finish.
|
||||
|
||||
**Port implication:** the current port-owned JSON session snapshot persists only global integer/string banks
|
||||
and deliberately has no active-frame or numbered-save backend. Treating `0x1ad` as a no-op is behaviorally
|
||||
neutral only under that present limitation; counting it as faithfully implemented would be misleading. Its
|
||||
real implementation belongs in the future unified save architecture, where the VM must serialize the active
|
||||
`ExecFrame` chain and remember which frame is the resume boundary. This is the same architectural deferral as
|
||||
the already-deferred profile/read-state work, not a reason to invent a seed or offset-specific shortcut.
|
||||
**Port implication:** the port-owned JSON session snapshot still persists only global integer/string banks,
|
||||
but the VM now tracks `0x1ad` as an identity reference to the active `ExecFrame`. The marker survives nested
|
||||
calls and clears when that same frame unwinds, exposing the inclusive zero-based cutoff for the native
|
||||
serializer. The following numbered-payload slice must consume this boundary while serializing the frame
|
||||
chain; no script seed or offset-specific shortcut is involved.
|
||||
|
||||
### Native persistence opcode family and file layouts (resolved 2026-07-24)
|
||||
|
||||
@@ -1653,6 +1652,11 @@ Save paths are native engine policy, not script-provided strings. Every numbered
|
||||
`SAVE%2.2d.STH`, `SAVE.DAT`, `SAVE.BAK`, `RT.DAT`, and their `$$` temporary names) under that root.
|
||||
The script operands select the operation and numbered slot only.
|
||||
|
||||
The port preserves that ownership boundary while intercepting the root: Godot supplies `user://SAVE`, and
|
||||
`DirectoryNativeDatStore` owns the fixed native names beneath it. This isolates authored port saves from the
|
||||
original installation while retaining compatible file structure; another root can be injected without
|
||||
changing script semantics.
|
||||
|
||||
The resolver reads two per-game settings from the SYS4INI-backed settings registry:
|
||||
`set:UseAppDataFolder` and `set:SavePath`. When `UseAppDataFolder == 1`, the modern-Windows branch
|
||||
loads `SHGetFolderPathA` and requests `CSIDL_LOCAL_APPDATA | CSIDL_FLAG_CREATE` (`0x801c`); it then
|
||||
@@ -1719,6 +1723,10 @@ Both shared and numbered `.DAT` payloads use the same native container. The fixe
|
||||
| `0x11c` | 4 | `SaveVersion1` / logical state-layout version |
|
||||
| `0x120` | 4 | `SaveVersion2` / payload-codec subversion |
|
||||
|
||||
The wrapper algorithm is shared, but Himegari's identities are domain-specific: shared `SAVE.DAT` and
|
||||
`RT.DAT` use compatibility id `0x4a343234`, while installed numbered `SAVE##.DAT` files use
|
||||
`0x42323234`. Both carry game id `姫狩りダンジョンマイスター` and versions 3.10.
|
||||
|
||||
`save_container_read_and_validate_header@0x4306f0` is the metadata-only path used by `0x1a0`.
|
||||
The full writer/reader are `save_container_encode_and_write@0x42fac0` and
|
||||
`save_container_read_and_decode@0x42ff80`. Immediately after the header is this exact 20-byte codec frame:
|
||||
@@ -1763,7 +1771,9 @@ resources, retained state, history, and—when requested by `0x1a1`—the active
|
||||
|
||||
The thumbnail is a separate file, never part of that logical state. Both renderer paths prove `.STH` is an
|
||||
ordinary bottom-up 24-bit BMP under a nonstandard extension: `BM`, pixel offset `0x36`, a 40-byte info
|
||||
header, BGR pixels, and four-byte row padding. The handle-based path is
|
||||
header, BGR pixels, and four-byte row padding. Installed Himegari thumbnails are 112x84 and 28,278 bytes.
|
||||
The native writer records `bfSize=28,264`—DIB header plus pixel bytes, omitting the 14-byte BMP file header
|
||||
even though that header and the `0x36` pixel offset are physically present. The handle-based path is
|
||||
`gfx_surface_write_bmp24_to_handle@0x434bf0`; backend 1 reads back supported D3D surface formats and passes
|
||||
them to `gfx_surface_write_bmp24_to_path@0x475420`. The load side uses the renderer image decoder. A
|
||||
compatibility implementation should therefore preserve the paired-file lifecycle and BMP payload rather than
|
||||
@@ -1774,7 +1784,7 @@ inventing a second save container.
|
||||
a profile/save service so extended mode can later add JSON inspection/export, namespaced mod state, migrations,
|
||||
or a friendlier editor without changing compatibility-mode opcode semantics or the native import/export path.
|
||||
|
||||
**Port correspondence (2026-07-24, shared payload and selected cells implemented):**
|
||||
**Port correspondence (2026-07-24, numbered pair/thumbnail layer implemented):**
|
||||
`Age.Engine.Persistence.NativeSaveContainerCodec` now reads and writes the common header, Shift-JIS game id,
|
||||
SYSTEMTIME/playtime/version metadata, both CRC layers, version-2 compression wrapper, and exact reversible
|
||||
DWORD transform. `Sys4.LzssEncoder` emits the same 4 KiB-ring token dialect already consumed by
|
||||
@@ -1786,8 +1796,13 @@ DWORD transform. `Sys4.LzssEncoder` emits the same 4 KiB-ring token dialect alre
|
||||
lossless opaque-section preservation. `SharedProfile` owns selected integer/string maps and explicit
|
||||
load/save lifecycle; `GameSession` injects it into every fresh VM. Opcodes `0x1a2`/`0x1a3` and
|
||||
`0x1a9`/`0x1aa` now implement native upsert and missing-value defaults for direct cells and resolved
|
||||
global pointers. `RT.DAT` is now implemented as the separate S3RT layer described below. Thumbnails and
|
||||
numbered active-frame payloads remain later layers; the existing `GameSession` JSON snapshot is unchanged.
|
||||
global pointers. `RT.DAT` is now implemented as the separate S3RT layer described below. The numbered store
|
||||
validates its distinct compatibility id, queries metadata without decoding payloads, and performs exact
|
||||
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.
|
||||
|
||||
### Opcode `0xae` continues numbered-save stack restoration (2026-07-20)
|
||||
|
||||
|
||||
@@ -490,7 +490,7 @@ This is the data-only companion to full-resume opcode 0x1a1. It selects the conf
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1a0_query_numbered_save_metadata@0x427ba0 calls save_container_read_and_validate_header@0x4306f0. It reads SYSTEMTIME WORDs at header +0x108/+0x10a/+0x10e/+0x110/+0x112/+0x114 and DWORD accumulated playtime at +0x118. Corpus: three calls in SAVE.BIN.
|
||||
|
||||
Status 0 means valid metadata was written, 1 means the file could not be opened, and 2 means its native header was invalid or incompatible. Metadata comes from the fixed 0x124-byte S3SD/S4SD container header; no payload decode is needed.
|
||||
Status 0 means valid metadata was written, 1 means the file could not be opened, and 2 means its native header was invalid or incompatible. Metadata comes from the fixed 0x124-byte S3SD/S4SD container header; no payload decode is needed. Himegari numbered files use compatibility id 0x42323234, distinct from shared SAVE.DAT/RT.DAT id 0x4a343234. Port status (2026-07-24): implemented through the native directory store with fixed-header-only reads.
|
||||
|
||||
### 0x1a1 `load-numbered-slot-and-resume` (load-numbered-slot-and-resume, argc 2)
|
||||
- **summary:** (status_out)(slot) - fully load `SAVE%02d.DAT`, including saved script frames and text history, then resume through `CALLBACK_LOAD.BIN` and opcode 0xae.
|
||||
@@ -532,20 +532,22 @@ This is the paired reader for opcode 0x1a9 and the string counterpart to integer
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1ab_delete_numbered_save@0x427ed0 formats and calls DeleteFileA for the numbered .DAT and .STH paths with the layered status convention. Corpus: one call in SAVE.BIN.
|
||||
|
||||
Both deletes are attempted. Status is 0 when both succeed, 1 when only the DAT delete fails, and 2 whenever the STH delete fails (taking precedence over a DAT failure).
|
||||
Both deletes are attempted. Status is 0 when both succeed, 1 when only the DAT delete fails, and 2 whenever the STH delete fails (taking precedence over a DAT failure). Port status (2026-07-24): implemented against the paired native filenames.
|
||||
|
||||
### 0x1ac `copy-numbered-save` (copy-numbered-save, argc 3)
|
||||
- **summary:** (status_out)(source_slot)(destination_slot) - copy both numbered `.DAT` state and `.STH` thumbnail, replacing destination files.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1ac_copy_numbered_save@0x427fb0 formats source/destination SAVE%2.2d.DAT and SAVE%2.2d.STH paths and invokes CopyFileA with fail-if-exists false. Corpus: two calls in SAVE.BIN.
|
||||
|
||||
Both copies are attempted with overwrite allowed. Status is 0 when both succeed, 1 when only the DAT copy fails, and 2 whenever the STH copy fails (taking precedence over a DAT failure).
|
||||
Both copies are attempted with overwrite allowed. Status is 0 when both succeed, 1 when only the DAT copy fails, and 2 whenever the STH copy fails (taking precedence over a DAT failure). Port status (2026-07-24): implemented against the paired native filenames.
|
||||
|
||||
### 0x1ad `mark-save-resume-frame` (mark-save-resume-frame, argc 0)
|
||||
- **summary:** Mark the current script context as the highest frame serialized by numbered-save layouts 2/3. The native serializer saves frames 0 through this boundary and strips the boundary frame's return target so loading resumes it as the top frame. This opcode performs no file I/O itself.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1ad_mark_save_resume_frame@0x416b70 writes decoded instruction size 1 and ctx+0x9928c=cur_ctx_index. context_state_serialize@0x40d320 uses that field (or cur_ctx_index when -1) as the inclusive frame cutoff for save layouts 2/3, serializes frames 0..cutoff, and forces the cutoff frame's saved return entry to -1. op_0x2_exit_or_return_frame@0x417940 clears the mark when unwinding below it. Corpus: 1,928 calls in 304 scripts; SC0000's six calls are at startup and immediately after HISTORY/MENU/HIDEWIN/INPUTNAME returns.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -596,14 +598,14 @@ The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcod
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1ae_write_numbered_save_thumbnail@0x428100 creates SAVE%2.2d.STH with CREATE_ALWAYS and serializes operand 3's surface via the active renderer backend. Corpus: two calls in SAVE.BIN and SELSTAGE.BIN.
|
||||
|
||||
Status is 0 on success, 1 when the file cannot be created/opened, and 2 when surface encoding or writing fails. Renderer backend selects a handle-based or path-based native worker.
|
||||
Status is 0 on success, 1 when the file cannot be created/opened, and 2 when surface encoding or writing fails. Renderer backend selects a handle-based or path-based native worker. Himegari writes an ordinary bottom-up 24-bit BMP (112x84 in installed files), BGR rows with four-byte padding, under the .STH extension. Its bfSize field historically omits the 14-byte BITMAPFILEHEADER. Port status (2026-07-24): implemented with exact native BMP output and host surface capture.
|
||||
|
||||
### 0x1af `load-numbered-save-thumbnail` (load-numbered-save-thumbnail, argc 3)
|
||||
- **summary:** (status_out)(slot)(surface_slot) - decode the numbered save's separate `SAVE%02d.STH` thumbnail into a surface slot.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1af_load_numbered_save_thumbnail@0x428240 opens SAVE%2.2d.STH and passes it plus operand 3's surface slot to the active renderer decoder. Corpus: one call in SAVE.BIN.
|
||||
|
||||
Status is 0 on success, 1 when the file cannot be opened, and 2 when image decoding fails. The thumbnail format is owned by the renderer codec and is not embedded in the numbered `.DAT` payload.
|
||||
Status is 0 on success, 1 when the file cannot be opened, and 2 when image decoding fails. The thumbnail format is owned by the renderer codec and is not embedded in the numbered `.DAT` payload. Port status (2026-07-24): implemented with 24-bit BMP decode and host surface replacement.
|
||||
|
||||
### 0x1f6 `clear-retained-gfx-objects` (clear-retained-gfx-objects, argc 0)
|
||||
- **summary:** Clear the complete retained gfx-object registry while preserving allocated surface resources. Subsequent object queries return absent until draw/geometry operations recreate records.
|
||||
|
||||
@@ -3426,14 +3426,48 @@ current per-message state. Six focused tests cover exact bytes, malformed files,
|
||||
the installed native oracle, queue/growth behavior, paired filesystem transactions, and fresh-VM opcode
|
||||
continuity.
|
||||
|
||||
**Next persistence step:** implement numbered active-frame SAVE##.DAT layouts and paired BMP `.STH`
|
||||
thumbnail lifecycle. JSON inspection/export and mod-owned namespaced state remain extended-mode work.
|
||||
**Next persistence step:** implement the numbered pair/thumbnail/frame-boundary layer, then use that proven
|
||||
surface to implement the larger layout-3 active-frame/global payload. JSON inspection/export and mod-owned
|
||||
namespaced state remain extended-mode work.
|
||||
|
||||
Validation: all 370 engine tests pass, including the installed read-only RT.DAT round-trip oracle;
|
||||
opcode generator tests/lint and the Godot C# build are clean. SC0000 is now 127/129 distinct opcodes
|
||||
handled (98.4%); its only remaining effectful gaps are numbered-save restore boundaries `0x1ad`×6 and
|
||||
`0xae`×1.
|
||||
|
||||
### Persistence implementation step 4 — numbered pair, metadata, thumbnail, and frame boundary (2026-07-24)
|
||||
|
||||
The bounded outer numbered-save lifecycle is now implemented before the much larger logical state payload.
|
||||
Himegari's numbered `.DAT` compatibility id is `0x42323234`, distinct from shared `SAVE.DAT`/`RT.DAT`
|
||||
id `0x4a343234`; `NativeSaveIdentity` and the directory store now validate those domains independently.
|
||||
Opcode `0x1a0` reads and validates only the fixed header, while `0x1ab`/`0x1ac` attempt both native
|
||||
`SAVE%02d.DAT` and `SAVE%02d.STH` members and reproduce DAT-failure/STH-precedence statuses.
|
||||
|
||||
`NumberedThumbnailCodec` reproduces the installed native `.STH` dialect: bottom-up 24-bit BGR BMP,
|
||||
four-byte row padding, 112x84 installed dimensions, and the historical `bfSize` value that omits the
|
||||
physically present 14-byte file header. Host capture/replacement seams connect this to Godot surfaces;
|
||||
`0x1ae` and `0x1af` expose the native status contract. Godot redirects the engine-owned save root to
|
||||
isolated `user://SAVE` while retaining the native names and formats, so it never mutates the original
|
||||
installation's AppData saves.
|
||||
|
||||
Opcode `0x1ad` now marks the current `ExecFrame` by identity. The boundary survives nested calls and clears
|
||||
when its owning frame unwinds, matching AGE's single context-index marker and providing the exact cutoff
|
||||
the next serializer slice needs. It is no longer counted as a no-op, but this step deliberately does not
|
||||
claim load-compatible numbered `.DAT` bodies: layout-3's fixed state, six global banks, active frames,
|
||||
history, resources, surfaces, and retained graphics remain to be serialized together.
|
||||
|
||||
Seven focused tests cover exact BMP bytes and round-trip, the installed read-only thumbnail oracle,
|
||||
header-only metadata, separate compatibility ids, pair failure precedence, VM opcode integration, and both
|
||||
resume-marker unwind cases.
|
||||
|
||||
Validation: all 377 engine tests pass; the Godot C# build has zero warnings and the threaded headless
|
||||
self-test reports `SELFTEST OK`. SC0000 now has 128/129 distinct opcodes handled (99.2%); only the active
|
||||
numbered-load restoration rendezvous `0xae` remains effectful and unimplemented there.
|
||||
|
||||
**Next persistence step:** implement complete numbered logical layout 3 and wire `0x19e`, `0x1a1`, and
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -15,6 +15,8 @@ The VM and content pipeline are already mostly platform-neutral:
|
||||
- Effectful bytecode operations cross `Hosting/IHost.cs`; the VM does not call native OS APIs.
|
||||
- Movie payloads arrive from `IAssetStore` as owned bytes and decoded frames enter the compositor as the
|
||||
platform-neutral `RgbaImage` type.
|
||||
- Native-compatible persistence uses managed streams behind `INativeDatStore`; Godot redirects AGE's
|
||||
engine-owned root to `user://SAVE` while retaining its fixed portable DAT/STH filenames.
|
||||
|
||||
The selected movie path now uses the project-owned FFmpeg C ABI rather than a Windows multimedia API, but only a
|
||||
Windows-x64 native bundle is built and staged today. The retired-live DirectShow implementation remains in-tree
|
||||
@@ -31,6 +33,7 @@ replaced before claiming portable exports.
|
||||
| Movie audio | FFmpeg detects the audio stream but the current ABI returns video frames only | MPEG movie audio remains intentionally silent | Extend the ABI with timestamped PCM and select an audio/presentation clock; separate feature slice |
|
||||
| ADV font discovery | `godot/Main.cs` probes `C:/Windows/Fonts` for Japanese fonts | Harmless fallback today, but appearance depends on host fonts | Bundle/configure a redistributable font or add platform-specific discovery |
|
||||
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
|
||||
| Save/profile storage | Managed `DirectoryNativeDatStore` under Godot `user://SAVE`; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails | No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target |
|
||||
| Install/repository discovery | `engine/Age.Engine/Sys4/Paths.cs` finds `age-reimpl` above `AppContext.BaseDirectory` and assumes the current workspace sibling layout | Suitable for development, not packaged exports on any OS | Replace runtime discovery with a user-selected game root/profile; retain repository paths only for developer tools/tests |
|
||||
| Archive parity oracle | One integration test launches `bin/BinExtractALF.exe` | Windows-only test helper, not a shipped runtime dependency | Skip/replace on non-Windows CI; runtime ALF/AAI readers do not depend on it |
|
||||
| Native RE tools | Frida/Ghidra helpers target the original `AGE.EXE`; supporting utilities include Windows executables and Windows command conventions | Development/research only | Keep separate from export requirements; document platform prerequisites per tool |
|
||||
|
||||
@@ -471,7 +471,9 @@ replacement for compatibility-mode import/export. The recovered native contract
|
||||
their four native opcodes while opaque catalog/version sections round-trip unchanged. Native `RT.DAT`
|
||||
import/export and the packed-script/T1 ReadTextDB queue/commit/query lifecycle are also implemented,
|
||||
including `message:ReadTextSkip` ops `0x1ca`/`0x1cb` and state query `0x1cc`. Numbered active-frame state
|
||||
and thumbnails remain later Phase B work.
|
||||
remains later Phase B work. The outer numbered layer is now live: metadata query, paired `.DAT`/`.STH`
|
||||
copy/delete, exact native BMP thumbnail I/O, and the active-frame boundary marker. Full layout-3 payload
|
||||
serialization/restoration is the next persistence slice.
|
||||
|
||||
### Phase C — Externalize & modding foundation
|
||||
- Add **editable named data overlays** mapped explicitly onto the VM's `*INIT`-produced state; external
|
||||
|
||||
Reference in New Issue
Block a user