Fix Extra Room catalog unlock handling

This commit is contained in:
gamer147
2026-07-28 13:14:28 -04:00
parent 93d9f58758
commit f4bf78d7d1
11 changed files with 539 additions and 56 deletions

View File

@@ -1720,9 +1720,29 @@ generic save operation:
| `0x1ae(status,slot,surface)` | write `SAVE%02d.STH` from a surface |
| `0x1af(status,slot,surface)` | load `SAVE%02d.STH` into a surface |
Opcode `0x19d`, adjacent in number and used by CGMODE/ED/HMODE/MMODE, is not persistence: its handler is a
resource/compatibility lookup. It is deliberately excluded rather than named from proximity. The actual
persistence cluster has calls in `SAVE`, `SELSTAGE`, `GAMESTART`, `GAMECLEAR`, `INPUTNAME`, and INIT2.
Opcode `0x19d`, adjacent in number and used by CGMODE/ED/HMODE/MMODE, queries the shared-profile catalog
unlock database:
- `op_0x19d_is_catalog_resource_unlocked@0x427810` fetches `(out, packed_resource_id)`. Packed append
ids return zero when the configured save version is older than 3.10; otherwise it calls
`asset_catalog_is_resource_unlocked@0x415920` on `EngineCtx+0x9c24c`.
- Base ids use the marker table at asset-catalog `+0x41c`. A nonzero high byte selects the append
pointer at `+0x3844 + selector*4`; the low 24 bits are the table index.
- The marker is valid exactly when its low word equals
`low16(index * 0x053d6f99 + 0xb0b0b0b0)`. `asset_catalog_mark_resource_opened@0x44e410`, called only
after `asset_open_indexed_entry` succeeds, writes the plain marker and its modular-exponent encrypted
SAVE.DAT counterpart.
- `shared_profile_load` decrypts base and flattened append marker arrays through
`modular_exponent_u32@0x471e60`, then
`asset_catalog_restore_resource_unlock_marker@0x404ca0` installs and re-encrypts them under the
current session key. The two leading table DWORDs are private-exponent XOR `0x87912345` and modulus.
CGMODE passes each CGINIT full-image AGF id; HMODE passes each SPINIT scene-script id. The copied port
profile validates all 851 gallery images and all 118 H-scene scripts. The former all-locked presentation
was therefore an opcode/runtime integration defect, not missing save data. The port now decodes and
queries the native markers, marks successful VFS opens, and writes a native-decodable encrypted table.
The actual numbered/selected-cell persistence cluster remains the adjacent `0x19e` through `0x1af`
family described above.
**Numbered operation status contracts.** Save/load open failure is `1`; metadata uses `0=valid`,
`1=absent/open failure`, and `2=invalid/incompatible`. Delete/copy attempt both members of the pair and use

View File

@@ -323,6 +323,13 @@ Implemented as FIFO TryDequeue: nonempty writes success=1 plus the signed dword;
This is the logical inverse of op 0x194 string-equals. Skipping it is stateful: the destination is not cleared. At BUNKI@0x905, local 0x99 still contains a nonzero graphics handle, so the missing write falsely reserves a 30-pixel title row and shifts every choice down. The C# VM implements ordinal inequality through the shared string resolver; focused tests cover literal/global/local/pointer operands and the exact empty-title stale-handle overwrite.
### 0x19d `is-catalog-resource-unlocked` (is-catalog-resource-unlocked, argc 2)
- **summary:** (out)(packed_resource_id) - return 1 when the shared SAVE.DAT catalog marker for the selected base or append resource decodes to AGE's deterministic per-index unlock stamp, otherwise 0.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19d_handler@0x427810 fetches operand 2, rejects packed append ids when `set:SaveVersion1/2` is older than 3.10, calls asset_catalog_is_resource_unlocked@0x415920 on ctx+0x9c24c, and writes the bool to operand 1. The helper selects base table +0x41c or append pointer table +0x3844 and compares the stored low word to `(short)index * 0x6f99 - 0x4f50`. asset_catalog_mark_resource_opened@0x44e410 writes the equivalent full stamp and encrypted profile marker after asset_open_indexed_entry succeeds. shared_profile_load decodes the catalog arrays with modular exponentiation before populating those tables. Corpus: four calls, in CGMODE, HMODE, MMODE, and ED. The installed port SAVE.DAT decodes to 7,966/13,208 base markers and 75/81 append markers; all 851 CGINIT gallery images and all 118 SPINIT H-scene scripts validate unlocked.
Base ids use the low 24-bit index in the primary catalog marker table. A nonzero high-byte selector addresses that append catalog's independent table; selectors require SaveVersion 3.10 or later, and AGE's signed selector lookup only supports 1..127. Successful native catalog opens write the stamp `low16(index * 0x053d6f99 + 0xb0b0b0b0)` and its encrypted SAVE.DAT form. CGMODE tests CGINIT image ids and HMODE tests SPINIT scene-script ids through this predicate. Port status (2026-07-28): shared-profile import decodes both catalog sections, successful VFS opens mark resources, shared SAVE.DAT writes native-decodable encrypted markers, and the opcode returns the live profile predicate.
### 0x1a6 `half-byte-string-length` (halve-strlen, argc 2)
- **summary:** Write half the resolved string's byte length, using integer truncation.
- **grounding:** source=investigation, confidence=high
@@ -1332,10 +1339,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
- **summary:** —
- **grounding:** source=kelebek, confidence=med
### 0x19d `u0041C680` (u0041C680, argc 2)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1a5 `set-font` (set-font, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=med

View File

@@ -272,6 +272,22 @@ voice ids were reaching `play-voice` but failing the port's SC-section-only look
section. The initial compatibility fix added a raw fallback, but later native RE proved the broader rule:
voice and frontend texture operands are already universal packed catalog ids and never receive a scene base.
**Extra Room CG/H-scene unlock database (`CGMODE.BIN` / `HMODE.BIN`, 2026-07-28).** The copied
`user://SAVE/SAVE.DAT` was intact: native decode finds 7,966/13,208 valid base-catalog markers and
75/81 append markers, including all 851 CGINIT gallery-image ids and all 118 SPINIT H-scene script ids.
The encyclopedia and New Game settings looked correct because they restore selected integer cells through
`0x1a3`; CGMODE/HMODE instead filter their entries through opcode `0x19d`, which the port still stubbed and
therefore left as zero/locked.
Native RE identifies `0x19d` as the profile catalog-resource unlock predicate. Successful catalog opens
write a deterministic per-index stamp into a live marker table and its modular-exponent encrypted
SAVE.DAT table; shared-profile load decrypts base and selector-keyed append arrays before the query runs.
`SharedProfile` now owns that decoded set, the VM implements the query, and a shared VFS wrapper marks only
successful script/texture/audio/movie/cursor opens. Saving emits native-decodable base and append tables,
so imported unlocks display immediately and newly encountered resources persist. Focused regressions cover
exact native ciphertexts, base/append queries, successful-open timing, round-trip import/export, and known
installed CG/H-scene entries.
**Natural Game Start diagnostic gate (2026-07-21; captured).** The opt-in `-StartupDiagnostics` route kept
the persistent `SYSTEM4` root and native exit semantics and added no seeds, boot prefix, timing changes, or
input automation. A user-driven cold boot traversed the complete initialization family, `LOGO`/`LOGO.AGF`,

View File

@@ -468,7 +468,9 @@ JSON inspection/export, migrations, and namespaced mod state are additive extend
replacement for compatibility-mode import/export. The recovered native contract lives in
`docs/engine-re.md`. The common container codec/store and typed shared `SAVE.DAT` payload landed on
2026-07-24. Profile-owned selected integer/string cells now survive across scene VMs and are wired to
their four native opcodes while opaque catalog/version sections round-trip unchanged. Native `RT.DAT`
their four native opcodes. The base/append catalog sections are now decoded as encrypted resource-seen
markers: opcode `0x19d` queries them, successful VFS opens add them, and compatibility saves write
native-decodable tables. 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
is now implemented in native layout 3: metadata query, paired `.DAT`/`.STH` lifecycle, exact native BMP

View File

@@ -111,11 +111,11 @@ After decoding the common container, the shared payload is sequential:
| Order | Field |
|---:|---|
| 1 | `catalog_count:u32`, then `catalog_values[catalog_count]:u32` |
| 1 | `catalog_count:u32`, then the encrypted base-catalog unlock table |
| 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` |
| 4 | for version 3.10 or later, `selector_counts[256]:u32` for append-catalog unlock tables |
| 5 | for version 3.10 or later, `extended_count:u32`, then the flattened encrypted append tables |
| 6 | `reserved_tail[9]:u32` |
An integer entry is:
@@ -135,14 +135,35 @@ type byte `0x05` plus the same eight-digit address and NUL; values are CP932. Th
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 catalog sections are AGE's profile-wide **resource-seen/unlock database**. Each encrypted table begins
with two header DWORDs:
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.
| Table word | Meaning |
|---:|---|
| `0` | private exponent XOR `0x87912345` |
| `1` | modular-exponentiation modulus |
| `index + 2` | zero when locked/unseen; otherwise the encrypted resource marker |
Loading computes `plain = cipher^private_exponent mod modulus`. A marker is available when its low word
equals `low16(index * 0x053d6f99 + 0xb0b0b0b0)`. Native `asset_open_indexed_entry` writes that marker
only after successfully opening the resource, and opcode `0x19d` queries it. Thus ordinary CG assets
unlock when their AGF is opened and H-scene entries unlock when their SP script is opened.
The base table maps universal resource ids whose high byte is zero. For version 3.10 or later,
`selector_counts[selector]` gives the slot count of each append catalog; the extended array has its own
two-word crypto header followed by those selector tables concatenated in selector order. Packed resource
ids use the high byte as selector and the low 24 bits as the per-catalog index. Runtime lookup accepts
selectors 1 through 127.
The nine-DWORD tail consists of one explicit terminator plus eight DWORDs produced by the native
allocation formula. It remains compatibility-owned rather than mod storage.
The installed Himegari profile is a complete oracle: version 3.10 decodes to 13,210 base-table DWORDs
(13,208 resource slots), 24,070 integer cells, 605 string cells, selector entry 1 equal to 81, 83
extended-table DWORDs (two header plus 81 resource slots), and nine tail DWORDs. The port copy contains
7,966 valid base markers and 75 valid append markers. Those include all 851 CGINIT gallery images and
all 118 SPINIT H-scene scripts; a different encryption key can change every nonzero stored DWORD without
changing the decoded unlock set.
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
@@ -345,7 +366,7 @@ 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;
- some shared selected cells and the nine-DWORD tail remain 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