Document native persistence file formats
This commit is contained in:
@@ -1683,86 +1683,24 @@ the analogous `$$RT.DAT` → `RT.DAT` / `RT.BAK` update for ReadTextDB. `shared_
|
||||
falls back from `SAVE.DAT` to `SAVE.BAK` and loads `RT.DAT` independently. Numbered `.DAT` writes do not
|
||||
use this backup transaction, but a successful numbered serialization flushes the shared profile too.
|
||||
|
||||
After the common container decode, the shared logical payload is:
|
||||
The complete shared-payload byte structure, installed-file counts, and its separation from `RT.DAT` and
|
||||
numbered slot state are canonical in `sys4-format-notes.md` under "Native persistence files." The
|
||||
selected-cell address meanings are canonical in `vm-map/globals.toml`. Native writer/reader behavior
|
||||
establishes that the fixed integer/string maps are profile-wide services rather than whole-bank snapshots;
|
||||
their opaque catalog and extended sections are preserved by the port.
|
||||
|
||||
1. a DWORD catalog/compatibility count followed by that many DWORDs;
|
||||
2. a DWORD integer-entry count, then fixed 16-byte entries `{typed_key[12], raw_value_u32}`;
|
||||
3. a DWORD string-entry count and a string-blob **DWORD count**, then concatenated
|
||||
`typed_key\0value\0` pairs with DWORD padding;
|
||||
4. for shared save version at least 3.10, a 256-DWORD selector/count table, a DWORD extra-count, and that
|
||||
many extra DWORDs;
|
||||
5. one explicit trailing zero DWORD plus eight historical allocation-slack DWORDs (nine reserved tail
|
||||
DWORDs in decoded native Himegari files).
|
||||
|
||||
The key prefix is binary, not the ASCII character suggested by shorthand such as `3%08x`:
|
||||
integer-cell ops `0x1a2`/`0x1a3` use raw byte `0x03` followed by eight lowercase ASCII hex digits;
|
||||
string-cell ops `0x1a9`/`0x1aa` use raw byte `0x05` plus the same address text. In both cases the
|
||||
hexadecimal portion is the VM lvalue's resolved global-bank index. Integer fixed-key bytes 10 and 11 are
|
||||
unused native stack padding and cannot be required to be zero when importing an original file. The string
|
||||
table is therefore not an incidental settings blob: it is the paired profile-wide selected-cell service
|
||||
for string globals, with insert-or-assign and empty-on-miss behavior.
|
||||
|
||||
The installed native Himegari `SAVE.DAT` provides a direct compatibility oracle for this shape:
|
||||
version 3.10 decodes to 13,210 opaque catalog values, 24,070 integer cells, 605 string cells, selector
|
||||
entry 1 equal to 81, 83 extended values, and the nine-DWORD reserved tail. The port decoder imports that
|
||||
file exactly; catalog/extended/tail sections remain opaque and are preserved losslessly rather than
|
||||
reinterpreted as mod state.
|
||||
|
||||
`SAVE.BIN` also proves why a numbered `.DAT`/`.STH` pair alone cannot reproduce a complete menu row.
|
||||
The numbered fixed header supplies only timestamp and accumulated playtime, and `.STH` supplies the
|
||||
112x84 screenshot. The remaining preview record is maintained through selected-cell ops in shared
|
||||
`SAVE.DAT`: three parallel 210-cell string banks hold chapter/title (`GStr[0x0+]`), location
|
||||
(`GStr[0xd2+]`), and protagonist name (`GStr[0x1a4+]`); eight 210-cell integer banks beginning at
|
||||
`GInt[0x5]`, `0xd7`, `0x1a9`, `0x27b`, `0x34d`, `0x41f`, `0x4f1`, and `0x5c3` hold portrait entity,
|
||||
level, training count, growth, personality, difficulty/mode badge, cleared-ending mask, and installed
|
||||
append mask respectively. Save and stage-select paths snapshot these cells with `0x1a2`/`0x1a9`;
|
||||
the menu restores them with `0x1a3`/`0x1aa`. For the NG+ history display, `SAVE.BIN` scans bits 0..14
|
||||
of the restored `GInt[0x4f1 + slot]` mask and draws one 17x17 ending badge for every set bit.
|
||||
`SAVE.BIN` provides the game-level consumer proof for the menu split: it reads timestamp/playtime from
|
||||
the numbered header, loads the `.STH` screenshot, restores the remaining row fields with selected-cell
|
||||
ops, and scans the restored cleared-ending mask to draw the NG+ badges. Save and stage-select paths
|
||||
snapshot the same preview cells before persistence.
|
||||
|
||||
#### Common `.DAT` container
|
||||
|
||||
Both shared and numbered `.DAT` payloads use the same native container. The fixed header is exactly
|
||||
`0x124` bytes:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `0x000` | 4 | little-endian magic `S3SD` or `S4SD` |
|
||||
| `0x004` | 4 | compatibility id |
|
||||
| `0x008` | `0x100` | NUL-terminated game id area |
|
||||
| `0x108` | `0x10` | Win32 `SYSTEMTIME` |
|
||||
| `0x118` | 4 | accumulated playtime seconds |
|
||||
| `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:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `+0x00` | 4 | encoded DWORD count |
|
||||
| `+0x04` | 4 | MSB-first CRC-32 of encoded bytes |
|
||||
| `+0x08` | 4 | reflected CRC-32 of encoded bytes |
|
||||
| `+0x0c` | 4 | random rolling XOR seed |
|
||||
| `+0x10` | 4 | random odd multiplier (low 16 bits used) |
|
||||
|
||||
The encoded byte length is `encoded_dword_count * 4`. Each source DWORD is XORed with the current seed;
|
||||
its high and low 16-bit halves are independently multiplied by the current odd multiplier and stored as two
|
||||
DWORD products. Per source DWORD the seed advances by `0x0b0b0b0b` and the multiplier by `0x0b02`.
|
||||
The inverse requires both products to divide exactly, providing another corruption check. The decoded logical
|
||||
payload itself starts with two more DWORDs: MSB-first and reflected CRC-32 values over all following logical
|
||||
bytes.
|
||||
|
||||
For `SaveVersion2 < 2`, that checked logical buffer is transformed directly. For version 2 or later, the
|
||||
buffer is first passed through the native 4 KiB LZSS codec: a zero-filled 4096-byte ring starting at `0xfee`,
|
||||
groups of eight tokens under an LSB-first flag byte (`1=literal`), and two-byte matches containing a 12-bit
|
||||
offset plus a four-bit `length-3`. Incompressible data is stored verbatim. The transformed inner buffer begins
|
||||
with three DWORDs recording original byte length, consumed byte length, and stored byte length, followed by
|
||||
the compressed/verbatim bytes and native padding. The mapped helpers are `lzss_4k_compress@0x42ed30`,
|
||||
`save_container_read_and_decode@0x42ff80`. The canonical header, codec-frame, CRC, LZSS, and reversible
|
||||
DWORD-transform specification is in `sys4-format-notes.md`. The mapped helpers are
|
||||
`lzss_4k_compress@0x42ed30`,
|
||||
`lzss_4k_decompress@0x42f050`, `save_payload_expand_multiply_transform@0x42f400`,
|
||||
`save_payload_inverse_multiply_transform@0x42f4a0`, `crc32_msb_first@0x42f360`, and
|
||||
`crc32_reflected@0x42f300`.
|
||||
@@ -1772,30 +1710,21 @@ the compressed/verbatim bytes and native padding. The mapped helpers are `lzss_4
|
||||
`context_state_serialize@0x40d320` chooses numbered logical layout 1, 2, or 3 from `SaveVersion1`;
|
||||
layout 1 retains legacy `SaveVersion2` sublayouts 10 and 20. Layouts 2 and 3 serialize script contexts
|
||||
`0..save_frame_boundary_index` inclusive (falling back to the current context), clear the terminal frame's
|
||||
return target, and append `text_history_serialize`. Their frame record is `0x414` bytes (`0x105` DWORDs);
|
||||
layout 2's frame-zero/fixed prefix is `0x8f8` bytes and layout 3's is `0x5718` bytes. Layout 3's larger
|
||||
prefix adds a 20,000-byte surface/resource state block and retained graphics-object state; each retained
|
||||
object record carries its handle plus the native `0xb5`-DWORD object record. The state also carries six
|
||||
global-bank counts, raw integer banks, packed strings/other banks, and resource registrations (100 in the
|
||||
modern layouts). The matching `save_data_deserialize_and_begin_restore@0x40fd10` reconstructs those banks,
|
||||
resources, retained state, history, and—when requested by `0x1a1`—the active frame chain consumed by `0xae`.
|
||||
return target, and append `text_history_serialize`. The matching
|
||||
`save_data_deserialize_and_begin_restore@0x40fd10` reconstructs the banks, resources, retained state,
|
||||
history, and—when requested by `0x1a1`—the active frame chain consumed by `0xae`.
|
||||
|
||||
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. 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
|
||||
inventing a second save container.
|
||||
The complete Himegari layout-3 body, frame record, six-bank sequence, retained-gfx region, appended
|
||||
text-history tail, and `.STH` BMP format are canonical in `sys4-format-notes.md`. Native thumbnail
|
||||
helpers are `gfx_surface_write_bmp24_to_handle@0x434bf0` and
|
||||
`gfx_surface_write_bmp24_to_path@0x475420`; the load side uses the renderer image decoder.
|
||||
|
||||
**1.0 implementation boundary:** reproduce these native binary domains and lifecycle first: shared
|
||||
`SAVE.DAT`/`SAVE.BAK`, `RT.DAT`/`RT.BAK`, numbered `.DAT`, and paired BMP `.STH`. Keep the ownership behind
|
||||
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, numbered pair/thumbnail layer implemented):**
|
||||
**Port correspondence (2026-07-24):**
|
||||
`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
|
||||
@@ -1811,37 +1740,14 @@ global pointers. `RT.DAT` is now implemented as the separate S3RT layer describe
|
||||
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/
|
||||
`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
|
||||
frame marker) are wired. Godot injects the store at `user://SAVE`.
|
||||
`NativeNumberedSaveCodec`, `NativeTextHistoryCodec`, and `NativeGfxPersistenceCodec` own the complete
|
||||
layout-3 numbered body, global/frame/gfx state, 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.
|
||||
#### Layout-3 restore mechanics and port correspondence (2026-07-24)
|
||||
|
||||
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
|
||||
|
||||
@@ -3529,6 +3529,23 @@ extended-mode work.
|
||||
Validation: all 386 engine tests pass, global/opcode lint reports zero errors/warnings, the Godot C#
|
||||
build has zero warnings, and the threaded headless run reports `SELFTEST OK`.
|
||||
|
||||
### Persistence documentation closeout — canonical file-format specification (2026-07-24)
|
||||
|
||||
`docs/sys4-format-notes.md` now contains the canonical byte-level persistence specification for
|
||||
Himegari 3.10. It separates structural ownership from game semantics and documents the common
|
||||
S3SD/S4SD container and transform, shared `SAVE.DAT` selected-cell payload, standalone `RT.DAT`
|
||||
ReadTextDB, numbered layout-3 fixed/dynamic state and appended History stream, and paired `.STH` BMP.
|
||||
Installed-file oracle counts and exact known limits are recorded alongside each layout.
|
||||
|
||||
Overlapping byte-layout prose was removed from `docs/engine-re.md`; that reference now retains native
|
||||
function provenance, opcode behavior, save-root/lifecycle policy, and restoration mechanics while linking
|
||||
to the format specification. Game-global meanings remain sourced from `vm-map/globals.toml` and its
|
||||
generated reference. The spec explicitly leaves legacy numbered layouts 1/2, opaque shared sections,
|
||||
and unnamed retained-gfx fields outside the confirmed Himegari compatibility claim.
|
||||
|
||||
This documentation-only closeout does not change runtime persistence. The next functional check remains
|
||||
continuing from the installed slot through the restored gameplay frame.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# SYS4 Script Format — Reverse-Engineering Notes (hex-first)
|
||||
# SYS4 Binary Format Notes (hex-first)
|
||||
|
||||
Derived purely from byte-pattern analysis of the 481 DATA1 `.BIN` scripts, before
|
||||
any Ghidra work on `AGE.EXE`. Confidence levels flagged per finding. Probe scripts
|
||||
live in `tools/probe_*.py`.
|
||||
The script sections were derived initially from byte-pattern analysis of the 481 DATA1 `.BIN`
|
||||
files, before Ghidra work on `AGE.EXE`; their probe scripts live in `tools/probe_*.py`. The
|
||||
persistence sections combine native-code reconstruction, shipped-script consumers, installed
|
||||
read-only files, and port codec round trips. Confidence and remaining semantic limits are stated
|
||||
per format.
|
||||
|
||||
> **UPDATE 2026-07-05 — opcode set solved via Kelebek1's table.** Everything below
|
||||
> under "Instruction stream — PARTIAL" is now resolved: code = instructions of
|
||||
@@ -31,6 +33,287 @@ The same Himegari trailer also contains `USEAPPDATAFOLDER=1` and
|
||||
settings to select `%LOCALAPPDATA%` plus that relative path on modern Windows. Filename patterns remain
|
||||
native policy rather than trailer or script strings; see `engine-re.md` under "Save-root resolution."
|
||||
|
||||
## Native persistence files — Himegari 3.10
|
||||
|
||||
This section is the canonical binary-format specification for the persistence files used by Himegari.
|
||||
Native function provenance, opcode behavior, filesystem policy, and restore control flow remain in
|
||||
`engine-re.md`; game-global meanings remain sourced from `vm-map/globals.toml` and its generated
|
||||
`global-reference.md`.
|
||||
|
||||
Unless noted otherwise, offsets are byte offsets, integers are little-endian DWORDs, and strings are
|
||||
NUL-terminated CP932. Himegari separates four physical domains:
|
||||
|
||||
| File | Scope | Structural owner | Semantic owner |
|
||||
|---|---|---|---|
|
||||
| `SAVE.DAT` / `SAVE.BAK` | whole profile | AGE's typed selected-cell payload inside the common save container | scripts choose which integer/string global cells to persist |
|
||||
| `RT.DAT` / `RT.BAK` | whole profile | AGE's standalone `S3RT` ReadTextDB | script resource ids and T1 message boundaries identify the flags |
|
||||
| `SAVE%02d.DAT` | one numbered slot | AGE's common container, full VM-state layout, and appended text-history tail | scripts populate live state, choose the frame cutoff, and request a slot save |
|
||||
| `SAVE%02d.STH` | one numbered slot | AGE's BMP writer/reader | scripts choose the captured surface and slot |
|
||||
|
||||
The menu-preview record is not read from `RT.DAT`. Its timestamp/playtime comes from the numbered
|
||||
`.DAT` header, its screenshot comes from `.STH`, and its title/location/name/stats/difficulty/ending
|
||||
history come from slot-indexed selected cells in shared `SAVE.DAT`. The numbered `.DAT` still contains
|
||||
the full restorable VM state; `SAVE.DAT` holds presentation-oriented copies so `SAVE.BIN` can list slots
|
||||
without decoding every numbered body.
|
||||
|
||||
### Common `S3SD` / `S4SD` `.DAT` container — CONFIRMED
|
||||
|
||||
Shared `SAVE.DAT` and numbered `SAVE%02d.DAT` use the same wrapper. `RT.DAT` does not: it has the
|
||||
standalone `S3RT` layout documented below. The fixed `.DAT` header is `0x124` bytes:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `0x000` | 4 | ASCII magic `S3SD` or `S4SD` |
|
||||
| `0x004` | 4 | compatibility id |
|
||||
| `0x008` | `0x100` | NUL-terminated CP932 game id area |
|
||||
| `0x108` | `0x10` | Win32 `SYSTEMTIME`: eight little-endian WORDs |
|
||||
| `0x118` | 4 | accumulated playtime in seconds |
|
||||
| `0x11c` | 4 | signed `SaveVersion1`, selecting the logical state layout |
|
||||
| `0x120` | 4 | signed `SaveVersion2`, selecting the payload-codec subversion |
|
||||
|
||||
Himegari uses game id `姫狩りダンジョンマイスター` and versions 3.10. Shared `SAVE.DAT` uses
|
||||
compatibility id `0x4a343234`; installed numbered files use the distinct id `0x42323234`.
|
||||
|
||||
A `0x14`-byte codec frame begins at file offset `0x124`, and expanded data begins at `0x138`:
|
||||
|
||||
| Frame offset | File offset | Size | Field |
|
||||
|---:|---:|---:|---|
|
||||
| `+0x00` | `0x124` | 4 | expanded-data DWORD count; always even |
|
||||
| `+0x04` | `0x128` | 4 | MSB-first CRC-32 of expanded bytes |
|
||||
| `+0x08` | `0x12c` | 4 | reflected CRC-32 of expanded bytes |
|
||||
| `+0x0c` | `0x130` | 4 | rolling XOR seed |
|
||||
| `+0x10` | `0x134` | 4 | nonzero odd multiplier; only the low 16 bits are valid |
|
||||
|
||||
The logical payload is protected in this order:
|
||||
|
||||
1. Prefix the payload with two DWORD CRCs over the payload: MSB-first CRC-32, then reflected CRC-32.
|
||||
2. For `SaveVersion2 >= 2`, optionally compress that checked buffer with the 4 KiB LZSS dialect and
|
||||
wrap it as `{original_bytes, consumed_bytes, stored_bytes, stored_data, padding}`. The first two
|
||||
lengths agree. If compression does not shrink the data, `stored_bytes == original_bytes` and the
|
||||
bytes are verbatim.
|
||||
3. XOR each source DWORD with the current seed. Multiply its high and low 16-bit halves separately by
|
||||
the current odd multiplier, emitting two DWORD products. Advance the seed by `0x0b0b0b0b` and the
|
||||
16-bit multiplier by `0x0b02` for each source DWORD.
|
||||
4. Store CRCs of the expanded products in the outer codec frame.
|
||||
|
||||
The inverse requires both products to divide exactly by the current multiplier and both quotients to
|
||||
fit 16 bits. This supplies an integrity check in addition to the two outer and two inner CRCs.
|
||||
|
||||
The LZSS stream uses a zero-filled 4096-byte ring starting at `0xfee`. Each LSB-first flag byte controls
|
||||
eight tokens (`1=literal`); a match token stores a 12-bit ring offset and four-bit `length-3`, allowing
|
||||
lengths 3 through 18. The container's expanded-data count determines where it ends. Numbered files may
|
||||
append another domain after that boundary, so a reader must retain the consumed byte count rather than
|
||||
assuming the container occupies the entire physical file.
|
||||
|
||||
### Shared `SAVE.DAT` logical payload — CONFIRMED
|
||||
|
||||
After decoding the common container, the shared payload is sequential:
|
||||
|
||||
| Order | Field |
|
||||
|---:|---|
|
||||
| 1 | `catalog_count:u32`, then `catalog_values[catalog_count]:u32` |
|
||||
| 2 | `integer_count:u32`, then `integer_entries[integer_count]`, 16 bytes each |
|
||||
| 3 | `string_count:u32`, `string_blob_dwords:u32`, then the DWORD-padded string blob |
|
||||
| 4 | for version 3.10 or later, `selector_counts[256]:u32` |
|
||||
| 5 | for version 3.10 or later, `extended_count:u32`, then `extended_values[extended_count]:u32` |
|
||||
| 6 | `reserved_tail[9]:u32` |
|
||||
|
||||
An integer entry is:
|
||||
|
||||
| Entry offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `+0x00` | `0x0c` | typed key field |
|
||||
| `+0x0c` | 4 | raw integer value |
|
||||
|
||||
The meaningful key bytes are `{type_tag, address_hex[8], NUL}`. Integer cells use raw type byte
|
||||
`0x03`; the address is eight ASCII hexadecimal digits naming the resolved VM global-bank index.
|
||||
Bytes 10 and 11 in the fixed integer key field are unused native stack residue and cannot be required
|
||||
to be zero when importing original data.
|
||||
|
||||
The string blob repeats `{typed_key_cstring, value_cstring}` `string_count` times. String keys use raw
|
||||
type byte `0x05` plus the same eight-digit address and NUL; values are CP932. The stored blob length is
|
||||
a DWORD count. AGE rounds with `(unpadded_bytes / 4) + 1`, deliberately adding a whole zero DWORD when
|
||||
the last value already ends on a DWORD boundary.
|
||||
|
||||
The nine-DWORD tail consists of one explicit terminator plus eight DWORDs produced by the native
|
||||
allocation formula. Catalog, selector, extended, and tail sections are structurally bounded but not
|
||||
fully assigned game-level meanings, so compatibility import/export preserves them rather than treating
|
||||
them as mod storage.
|
||||
|
||||
The installed Himegari profile is a complete oracle: version 3.10 decodes to 13,210 catalog values,
|
||||
24,070 integer cells, 605 string cells, selector entry 1 equal to 81, 83 extended values, and nine tail
|
||||
DWORDs.
|
||||
|
||||
The integer/string maps are AGE's generic selected-cell service, not copies of whole global banks.
|
||||
Himegari scripts assign their meanings through opcodes `0x1a2`/`0x1a3` and `0x1a9`/`0x1aa`. Known
|
||||
slot-preview banks include chapter/title, location, protagonist name, portrait entity, level, training
|
||||
count, growth, personality, difficulty, cleared-ending mask, and installed-append mask. Their canonical
|
||||
addresses and semantics are generated in `global-reference.md` from `vm-map/globals.toml`; the
|
||||
cleared-ending value is a 15-bit NG+/inheritance mask whose set bits select the displayed ending badges.
|
||||
|
||||
### Shared `RT.DAT` (`S3RT`) — CONFIRMED
|
||||
|
||||
`RT.DAT` is not wrapped, transformed, compressed, or checksummed like the `.DAT` save container. It is
|
||||
a standalone ReadTextDB with a `0x114`-byte header:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `0x000` | 4 | ASCII magic `S3RT` |
|
||||
| `0x004` | 4 | compatibility id (`0x4a343234` for Himegari) |
|
||||
| `0x008` | `0x100` | NUL-terminated CP932 game id area |
|
||||
| `0x108` | 4 | major version (`1`) |
|
||||
| `0x10c` | 4 | minor version (`0`) |
|
||||
| `0x110` | 4 | script-record count |
|
||||
|
||||
At `0x114` are all 12-byte script records:
|
||||
|
||||
| Record offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `+0x00` | 4 | raw packed SYS4/AAI script resource id |
|
||||
| `+0x04` | 4 | message count |
|
||||
| `+0x08` | 4 | serialized native heap pointer residue |
|
||||
|
||||
After the complete record table, each record's `message_count` DWORD flags follows in record order.
|
||||
Zero means unread and nonzero means read. The third record word is process-local residue rather than a
|
||||
portable offset; AGE allocates and replaces it on load. Readers accept original nonzero values, while
|
||||
portable writers emit zero.
|
||||
|
||||
Message indices are positions in that script's T1/F7 table, whose entries point to op-`0x71`
|
||||
read-message boundaries. Thus the file is profile-wide engine state derived from game scripts, not an
|
||||
arbitrary script-selected persistence table and not numbered-slot state.
|
||||
|
||||
The installed Himegari file is exactly 76,752 bytes: 192 records plus 18,543 flag DWORDs consume it
|
||||
without trailing data. Base script `SC0000` has packed id `0x22` and 320 flags, exactly matching its
|
||||
T1 count.
|
||||
|
||||
### Numbered `SAVE%02d.DAT`, logical layout 3 — CONFIRMED FOR HIMEGARI
|
||||
|
||||
Himegari's numbered file begins with the common container using compatibility id `0x42323234`,
|
||||
`SaveVersion1=3`, and `SaveVersion2=10`. The header supplies the save-menu timestamp and accumulated
|
||||
playtime without decoding the body.
|
||||
|
||||
The decoded layout-3 body begins:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `0x000` | 4 | terminal saved-frame index, called `cutoff`; frame count is `cutoff + 1` |
|
||||
| `0x004` | 4 | saved frame-owner/context word |
|
||||
| `0x008` | 4 | engine-state word |
|
||||
| `0x00c` | `0x28` | ten engine/context state DWORDs |
|
||||
| `0x034` | `0x4b0` | 100 resource-reload records of three DWORDs |
|
||||
| `0x4e4` | `0x4e20` | 1,000 surface-reload records of 20 bytes |
|
||||
| `0x5304` | `(cutoff + 1) * 0x414` | saved script-frame records |
|
||||
| `0x5718 + cutoff * 0x414` | variable | global banks and retained graphics |
|
||||
|
||||
Each `0x414`-byte frame is:
|
||||
|
||||
| Frame offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `+0x000` | 4 | parent context |
|
||||
| `+0x004` | 4 | packed script resource id |
|
||||
| `+0x008` | 4 | saved local-return count, at most 256 |
|
||||
| `+0x00c` | up to `0x400` | T3 local-return indices |
|
||||
| `+0x40c` | 4 | T1 resume/message-boundary index |
|
||||
| `+0x410` | 4 | T2 resumable call-script index; forced to `-1` on the cutoff frame |
|
||||
|
||||
The T1/T2/T3 indices refer to the three footer tables in the saved SYS4 script. T1 restores the active
|
||||
message boundary, T2 restores the inter-script call site, and T3 entries restore local returns as
|
||||
`T3[index] + 3`.
|
||||
|
||||
The variable section starts with six DWORD counts in this order:
|
||||
|
||||
1. integer globals;
|
||||
2. raw float storage;
|
||||
3. string globals;
|
||||
4. integer-pointer globals;
|
||||
5. string-pointer globals;
|
||||
6. local pointer scratch.
|
||||
|
||||
The integer and float arrays follow their counts. String count comes from bank count 3, but its storage
|
||||
begins with an additional `string_blob_dwords` followed by that many bytes of concatenated NUL-terminated
|
||||
CP932 strings and DWORD padding. The three pointer-family DWORD arrays follow.
|
||||
|
||||
Retained graphics then uses:
|
||||
|
||||
```text
|
||||
0x00 gfx_record_size:u32 # 0x2d4
|
||||
+0x04 gfx_object_count:u32
|
||||
+0x08 repeated { handle:u32, record[0x2d4] }
|
||||
... range_first:u32
|
||||
... range_count:i32
|
||||
... range_transform_record[0x2d4]
|
||||
... native allocation slack
|
||||
```
|
||||
|
||||
The 1,000 surface records preserve resource reload state; mapped fields include resource id at `+0x00`,
|
||||
packed color key at `+0x04`, and presence at `+0x08`. A retained `0x2d4` record is structurally complete
|
||||
but not every internal graphics field is semantically named. The native allocation is larger than the
|
||||
records actually written (`0x2e1 + object_count * 0x2d8` DWORDs in the graphics sizing term), leaving
|
||||
zero/slack bytes after the meaningful range record.
|
||||
|
||||
The installed `SAVE00.DAT` validates the complete layout-3 decode: cutoff 1, global-bank counts
|
||||
`[402459,1,789,1,1,1]`, and 211 retained graphics objects.
|
||||
|
||||
#### Appended text-history tail
|
||||
|
||||
The common container's expanded-data count ends before the numbered file ends. Layout 3 appends a
|
||||
separate text-history stream immediately at that consumed boundary:
|
||||
|
||||
| Tail offset | Size | Field |
|
||||
|---:|---:|---|
|
||||
| `+0x00` | 4 | logical byte length |
|
||||
| `+0x04` | 4 | duplicate logical byte length |
|
||||
| `+0x08` | 4 | stored byte length |
|
||||
| `+0x0c` | variable | the same 4 KiB-ring LZSS stream, or verbatim bytes when lengths match |
|
||||
|
||||
The decoded history data is:
|
||||
|
||||
1. `entry_count:u32`, then `entry_count` pairs `{layout_slot:i32, first_record_index:i32}`;
|
||||
2. `record_count:u32`, then `record_count` records of 11 DWORDs;
|
||||
3. `string_blob_dwords:u32`, then a DWORD-aligned CP932 string blob whose DWORDs are bitwise inverted.
|
||||
|
||||
Each 11-DWORD record stores layout slot, origin X/Y, width/height, primary value, auxiliary value,
|
||||
primary font size, text color, cursor Y, and flags. The string blob contains one NUL-terminated string
|
||||
per record. The installed save has a 21-byte physical history tail, and container plus tail consume the
|
||||
file exactly.
|
||||
|
||||
### Numbered `SAVE%02d.STH` thumbnail — CONFIRMED
|
||||
|
||||
The thumbnail is a separate ordinary BMP under a nonstandard extension:
|
||||
|
||||
- `BM` file signature and `0x36` pixel offset;
|
||||
- 14-byte bitmap file header plus 40-byte `BITMAPINFOHEADER`;
|
||||
- uncompressed 24-bit BGR pixels;
|
||||
- bottom-up positive height;
|
||||
- rows padded to four-byte boundaries.
|
||||
|
||||
Installed Himegari thumbnails are 112x84 and 28,278 physical bytes. The native writer stores
|
||||
`bfSize=28,264`, omitting the physically present 14-byte file header from that field while retaining the
|
||||
correct physical header and pixel offset. Compatibility writers reproduce this harmless historical quirk.
|
||||
|
||||
Copy, move, and delete operations treat `.DAT` and `.STH` as a pair, but neither file contains the other.
|
||||
|
||||
### Scope and remaining uncertainty
|
||||
|
||||
The following are complete enough for compatible import/export:
|
||||
|
||||
- the common S3SD/S4SD header, CRCs, optional LZSS wrapper, and DWORD transform;
|
||||
- Himegari's shared `SAVE.DAT` 3.10 section boundaries and typed selected cells;
|
||||
- `RT.DAT` 1.0 records and flag arrays;
|
||||
- Himegari's numbered layout-3 fixed state, global banks, frames, graphics region, and history tail;
|
||||
- `.STH` BMP encoding.
|
||||
|
||||
The remaining uncertainty is deliberately narrower:
|
||||
|
||||
- numbered layouts 1 and 2 are structurally identified but are not the implemented or installed
|
||||
Himegari compatibility target;
|
||||
- the shared catalog/extended arrays and some selected cells are still opaque at the game-semantic level;
|
||||
- the `0x2d4` retained-graphics record is byte-bounded, but not every field is named.
|
||||
|
||||
This distinction is important: AGE owns most numbered-save and ReadTextDB structure, while Himegari
|
||||
scripts primarily own the values in live VM globals, the save cutoff/invocation, and the semantic choice
|
||||
of selected `SAVE.DAT` cells.
|
||||
|
||||
## Header — CONFIRMED
|
||||
|
||||
Fixed 60-byte (0x3C) header: 8-byte magic + thirteen little-endian u32 fields.
|
||||
|
||||
Reference in New Issue
Block a user