diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 427dee7..1fe1a05 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -24,7 +24,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes | Weapon fields defaulted on disc | ✅ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv) the disc does not carry. Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check | | Unit (craft/vessel) fields defaulted on disc | ✅/🟡 | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — but a defaulted field is **not** a global constant: `Size_Y` provably inherits `Size_X` (7 independent units, 6 distinct values), and three more sibling rules are recorded ❔, recovering 65 values in units never visited — [values](captures/unit-runtime-fields.csv) | | UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `.t32` sprite has a `.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` (screen-level draw order) not yet decoded | -| Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2 | +| Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet 🟡 *0 locked / 2 developable / 4 developed*. Which item each index is remains open | ## Runtime / dynamic-capture technique diff --git a/docs/re/captures/arsenal-gun-list-predevelop.png b/docs/re/captures/arsenal-gun-list-predevelop.png new file mode 100644 index 0000000..eca3cf7 Binary files /dev/null and b/docs/re/captures/arsenal-gun-list-predevelop.png differ diff --git a/docs/re/captures/arsenal-mg1-developed.png b/docs/re/captures/arsenal-mg1-developed.png new file mode 100644 index 0000000..14fb968 Binary files /dev/null and b/docs/re/captures/arsenal-mg1-developed.png differ diff --git a/docs/re/captures/savedata-game03-developed-mg1.bin b/docs/re/captures/savedata-game03-developed-mg1.bin new file mode 100644 index 0000000..42bf374 Binary files /dev/null and b/docs/re/captures/savedata-game03-developed-mg1.bin differ diff --git a/docs/re/captures/savegame-details-slot03-developed.png b/docs/re/captures/savegame-details-slot03-developed.png new file mode 100644 index 0000000..980b112 Binary files /dev/null and b/docs/re/captures/savegame-details-slot03-developed.png differ diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index 47c36f1..856b723 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -75,12 +75,12 @@ photographed against this exact save (see "Naming the fields", further down). |---|---|---| | +0 | 0 | ❔ | | +4 | **324773** | ✅ **flight time, milliseconds** — the screen shows `Flight Time 000:05:24` and 324773 ms = 5 m 24.773 s | -| +8 | 5 | ✅ **clear ratio, percent** — the screen shows `Clear Ratio 5 %` | +| +8 | 5 | ✅ **clear ratio, percent** — the screen shows `Clear Ratio 5 %`. **It is not a stage counter**: developing one Arsenal weapon stepped it to 6 (see [the develop differential](#the-develop-differential-one-weapon-three-fields)) | | +12 | 0 | ❔ | | +16 | 1 | ❔ | | +20 | 0 | ❔ | -| +24 | **4101** (`0x1005`) | ✅ **Points** — the Details panel shows `Points 4101 P` | -| +28 | **4101** (`0x1005`) | 🟡 Points again — a second copy (running vs. banked total?); one save cannot separate them | +| +24 | **4101** (`0x1005`) | ✅ **Points — the spendable balance.** Named from the Details panel, and **separated from +28** by the develop differential: spending 4000 P moved *only* this field (4101 → 101) and the panel then read `Points 101 P` | +| +28 | **4101** (`0x1005`) | 🟡 **not** the displayed Points — it did **not** move when 4000 P was spent. A lifetime/earned total is the obvious read; unproven until a save is taken after *earning* points | | +32 | 79 | ❔ | | +36 | 2 | 🟡 **difficulty or stage** — the panel shows `Difficulty EASY` *and* `STAGE 02`, and +36/+52/+56 all hold 2, so which is which is **not** decidable from one save | | +40 (u64) | 2014400 | ❔ | @@ -89,7 +89,7 @@ photographed against this exact save (see "Naming the fields", further down). | +56 | 2 | 🟡 see +36 | | +60 | 0 | ❔ | | +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value | -| +68 (raw 54) | see below | ❔ 54 one-byte entries, values only ∈ {0, 2, 4} — histogram `{4: 10, 2: 6, 0: 38}` | +| +68 (raw 54) | see below | 🟡 **per-item Arsenal development state**, 54 one-byte entries, values only ∈ {0, 2, 4} — histogram `{4: 10, 2: 6, 0: 38}`. `4` = developed, `2` = developable now, `0` = still locked ([evidence](#the-develop-differential-one-weapon-three-fields)) | The 54-byte blob is the interesting one — a fixed-length array of small enumerated states, **16 of the 54 non-zero** (ten 4s, six 2s): @@ -100,7 +100,57 @@ enumerated states, **16 of the 54 non-zero** (ten 4s, six 2s): ``` 54 matches no count we have already established (22 stages, 110 units, 126 -weapons, ~61 Arsenal entries), so **do not** assume which list it indexes. +weapons, ~61 Arsenal entries), so **do not** assume which list it indexes. The +develop differential below identifies *what the values mean* without settling +*which list the index runs over* — the two are separate questions, and only the +first is answered. + +### The develop differential: one weapon, three fields + +**2026-08-11, in the running game.** The save state loaded from slot 01 (Stage 02, +"At Standby", 4101 P) was taken into READY ROOM → ARSENAL → GUN, where exactly one +item was developed — **Light Machine Gun MG I, 4000 P** — the mount prompt was +declined so that nothing but the development changed, and the result was saved to +the first empty slot (03). Slots 01/02 were not touched. + +Diffing the 545-byte payload of slot 03 against slot 02 (the same state saved +*before* the development) moves **exactly three things**: + +| field | before | after | what the screen showed | +|---|---|---|---| +| +8 clear ratio | 5 | **6** | slot 03's Details panel: `Clear Ratio 6 %` | +| +24 Points | 4101 | **101** | the Arsenal's `Points` readout, and slot 03's `Points 101 P` | +| +68 blob `[1]`, `[2]` | `2`, `0` | **`4`**, **`2`** | "You obtained Light Machine Gun MG I" and "You can now develop Light Swivel Machine Gun MG2H" | + +Everything else — `+28`, `+4` flight time, `+36/+52/+56`, the `SHAB` table, the +trailer — is byte-identical. That is what makes the reading of the blob more than +a guess: **one action produced exactly two blob transitions, and the game +announced exactly two events, in the same order**. `2 → 4` is the item that was +bought; `0 → 2` is the tech-tree successor it unlocked. So the alphabet is a +three-state progression *locked → developable → developed*, and the blob is the +Arsenal's development state, not a bitmask of owned weapons (a bitmask could not +represent the middle state). + +Two things this does **not** settle, and should not be written as if it did: + +- **Which item each index is.** The index space is 54 wide; the player-arsenal id + lists in `GP_HANGAR_ARSENAL.pak` union to 35 distinct weapon ids, so the blob is + not indexed by that list. MG I is at index **1**, not 0, even though it is the + first row of the first weapon type — so index 0 is something already developed + that the GUN list does not show first. **NEEDS-HUMAN / needs another + differential**: develop a second item in a *different* category and see which + index moves. +- **Whether the +1 on the clear ratio is per item.** One data point. If it is + per item, the ratio is a *collection* metric and not a stage metric, which + matters because it is one of the three numbers the Details panel shows and it + had been read as mission progress. + +Evidence: [`savedata-game03-developed-mg1.bin`](../captures/savedata-game03-developed-mg1.bin) +(compare against [`savedata-game02-samestate.bin`](../captures/savedata-game02-samestate.bin) +with `savegame.py`), and the screenshots +[before](../captures/arsenal-gun-list-predevelop.png) · +[after](../captures/arsenal-mg1-developed.png) · +[Details panel](../captures/savegame-details-slot03-developed.png). ### The 16-record `SHAB` table (16 × 20 bytes) @@ -138,6 +188,15 @@ sent during it are dropped; and the save confirm starts on **YES**, unlike the load confirm which starts on NO — the first attempt sent the load flow's extra `↑` and so pressed **NO**, writing nothing (the md5 stamp is what showed this). +A third trap, found on the Arsenal run: **a 60 ms d-pad tap is ignored inside a +modal yes/no dialog** even though it is exactly one step in the list menus. The +`Do you want to develop this weapon?` cursor did not move until the press was +held ~250 ms (`nav_probe.sh`'s own `step()` duration). Always screenshot the +dialog and confirm which side the cursor is on before pressing A — the develop +confirm starts on **NO**, the "would you like to mount it now?" prompt that +follows it starts on **YES**, so a fixed key sequence cannot be trusted across +two dialogs that appear back to back. + ### Result 1 — the payload is a pure function of game state ✅ Saving the just-loaded state into empty slot 02 produced `game02/savedata`. Its @@ -193,16 +252,20 @@ state, so it does not yet answer the question that motivated it — whether stag progress can be reached without winning missions ([mission outcome](../mission-outcome-stage02.md)). -**The differential has been run once** (see above) and it named three fields, but -a same-state save cannot separate the fields that share a value. The next -differentials, in order of what they would settle: +**Two differentials have been run.** The same-state one named three fields but +could not separate fields that shared a value; the develop one (above) separated +`+24` from `+28` and gave the blob its alphabet. What is left, in order of what +it would settle: 1. **A save at a different difficulty** — start a new game on NORMAL, save, compare: whichever of +36/+52/+56 moves is the difficulty. 2. **A save after clearing a stage** — settles stage vs. difficulty, `Times Cleared`, whether `SHAB[1]` fills, and whether the clear ratio steps by 5 %. -3. **A save after developing one Arsenal item** — should move one entry of the - 54-byte blob and name it. +3. ~~**A save after developing one Arsenal item**~~ — **done**, see + [the develop differential](#the-develop-differential-one-weapon-three-fields). + Its follow-up is a *second* development in a different weapon category, which + is what maps blob indices to items — but it needs Points, and the balance is + now 101 P, so it waits on a mission payout. **Nothing here has been written back to a save.** Editing is a separate decision and needs the user's go-ahead; the round-trip in `savegame.py` exists so that an