re: developing one Arsenal weapon separates Points from its twin and names the save blob
A third save, taken in the running game after developing exactly one item
(Light Machine Gun MG I, 4000 P) and declining the mount prompt, moves exactly
three things against the same-state save:
+24 Points 4101 -> 101 and +28 does NOT move, which separates the
pair one save could not tell apart; the
Details panel then reads Points 101 P, so
+24 is the spendable balance
+8 clear ratio 5 -> 6 so the ratio counts collection, not stages
+68 blob[1],[2] 2->4, 0->2 the item bought, and the successor the game
announced as newly developable
One action, two blob transitions, two on-screen events in the same order — which
is what makes 0 locked / 2 developable / 4 developed a reading rather than a
guess, and rules out a plain owned-bitmask (it could not hold the middle state).
Which item each of the 54 indices is stays open: the arsenal id lists in
GP_HANGAR_ARSENAL.pak union to 35, and MG I sits at index 1, not 0.
Also recorded: inside a modal yes/no dialog a 60 ms d-pad tap is ignored (the
list menus accept it), and the develop confirm starts on NO while the mount
prompt right after it starts on YES.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
| 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) |
|
| 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 `<name>.t32` sprite has a `<name>.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 |
|
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.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
|
## Runtime / dynamic-capture technique
|
||||||
|
|
||||||
|
|||||||
BIN
docs/re/captures/arsenal-gun-list-predevelop.png
Normal file
BIN
docs/re/captures/arsenal-gun-list-predevelop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 764 KiB |
BIN
docs/re/captures/arsenal-mg1-developed.png
Normal file
BIN
docs/re/captures/arsenal-mg1-developed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
BIN
docs/re/captures/savedata-game03-developed-mg1.bin
Normal file
BIN
docs/re/captures/savedata-game03-developed-mg1.bin
Normal file
Binary file not shown.
BIN
docs/re/captures/savegame-details-slot03-developed.png
Normal file
BIN
docs/re/captures/savegame-details-slot03-developed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 908 KiB |
@@ -75,12 +75,12 @@ photographed against this exact save (see "Naming the fields", further down).
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| +0 | 0 | ❔ |
|
| +0 | 0 | ❔ |
|
||||||
| +4 | **324773** | ✅ **flight time, milliseconds** — the screen shows `Flight Time 000:05:24` and 324773 ms = 5 m 24.773 s |
|
| +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 | ❔ |
|
| +12 | 0 | ❔ |
|
||||||
| +16 | 1 | ❔ |
|
| +16 | 1 | ❔ |
|
||||||
| +20 | 0 | ❔ |
|
| +20 | 0 | ❔ |
|
||||||
| +24 | **4101** (`0x1005`) | ✅ **Points** — the Details panel shows `Points 4101 P` |
|
| +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`) | 🟡 Points again — a second copy (running vs. banked total?); one save cannot separate them |
|
| +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 | ❔ |
|
| +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 |
|
| +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 | ❔ |
|
| +40 (u64) | 2014400 | ❔ |
|
||||||
@@ -89,7 +89,7 @@ photographed against this exact save (see "Naming the fields", further down).
|
|||||||
| +56 | 2 | 🟡 see +36 |
|
| +56 | 2 | 🟡 see +36 |
|
||||||
| +60 | 0 | ❔ |
|
| +60 | 0 | ❔ |
|
||||||
| +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value |
|
| +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
|
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):
|
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
|
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)
|
### 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
|
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).
|
`↑` 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 ✅
|
### Result 1 — the payload is a pure function of game state ✅
|
||||||
|
|
||||||
Saving the just-loaded state into empty slot 02 produced `game02/savedata`. Its
|
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
|
progress can be reached without winning missions
|
||||||
([mission outcome](../mission-outcome-stage02.md)).
|
([mission outcome](../mission-outcome-stage02.md)).
|
||||||
|
|
||||||
**The differential has been run once** (see above) and it named three fields, but
|
**Two differentials have been run.** The same-state one named three fields but
|
||||||
a same-state save cannot separate the fields that share a value. The next
|
could not separate fields that shared a value; the develop one (above) separated
|
||||||
differentials, in order of what they would settle:
|
`+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,
|
1. **A save at a different difficulty** — start a new game on NORMAL, save,
|
||||||
compare: whichever of +36/+52/+56 moves is the difficulty.
|
compare: whichever of +36/+52/+56 moves is the difficulty.
|
||||||
2. **A save after clearing a stage** — settles stage vs. difficulty, `Times
|
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 %.
|
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
|
3. ~~**A save after developing one Arsenal item**~~ — **done**, see
|
||||||
54-byte blob and name it.
|
[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
|
**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
|
and needs the user's go-ahead; the round-trip in `savegame.py` exists so that an
|
||||||
|
|||||||
Reference in New Issue
Block a user