diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index de74c5e..74db474 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -23,7 +23,7 @@ Promote to a prose `structures/โ€ฆmd` file when a format needs behavioural notes | Capital-ship part placement | ๐ŸŸก | `sylpheed-formats/src/ship.rs` (static) + [runtime capture](ship-placement-runtime-capture.md) | hull placement static-exact; external parts approximate statically. **Runtime capture** (Canary F10 โ†’ VS-constant WorldView) gives ground truth โ€” validated on `e106` destroyer; not yet baked into the viewer | | 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) | -| Arsenal develop economy | โœ…/โ” | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order โ€” the `No_Equipment โ€ฆ Wep_83` run is exactly **54**, the save blob's length) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000โ€“350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed โ€“ / Weight 0.3 = Light / 4000 P) โ€” later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space: **0โ€“31 confirmed** by twelve concordances, tail โ” | +| Arsenal develop economy | โœ…/โ” | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order โ€” the `No_Equipment โ€ฆ Wep_83` run is exactly **54**, the save blob's length) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000โ€“350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed โ€“ / Weight 0.3 = Light / 4000 P) โ€” later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order โ€” the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, โ€ฆ) โ€” pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered | | 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. **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 | diff --git a/docs/re/arsenal-develop-economy.md b/docs/re/arsenal-develop-economy.md index 32f89db..ee5afe9 100644 --- a/docs/re/arsenal-develop-economy.md +++ b/docs/re/arsenal-develop-economy.md @@ -36,8 +36,11 @@ Wep_84, Wep_85, NullWeapon_Arm1, NullWeapon_Arm2, NullWeapon_Arm3 ``` The ids are grouped by the eight categories in the same order the screen shows -them, which is why `No_Equipment โ€ฆ Wep_83` is exactly **54** entries โ€” the blob's -length. +them โ€” verified item-for-item against the running game (see the all-owned probe +below). Note one **coincidence that cost time**: the run `No_Equipment โ€ฆ Wep_83` +is also 54 entries, the save blob's length, which invites the conclusion that this +list *is* the blob's index space. It is not โ€” the two agree only up to index 32. +The blob follows `strings.tbl`'s order, which carries items this list omits. Each item then has a record: `Size, Weight, Range, Power, Speed, Stage, MissionObjective, Dependency, Points`. For the **first** record all of it reads @@ -88,7 +91,7 @@ economy: `Spitfire BX`) have a conditions *header* and no conditions โ€” โ” cut content or non-player weapons. -## What the blob indexes: the first 32 entries, confirmed +## How the first 32 entries were confirmed Predicting the save state from the conditions and comparing against the actual blob is a strong test, because it is made **before** looking at the blob. At the @@ -98,7 +101,10 @@ Dagger BG2, Pilum BP, Hound SMH, Dart 23 Rocket โ€” and the blob's **six** `2`s at WEAPONS-order indices **1, 5, 10, 12, 27, 31**, which are those six items, in order. Nothing had to be fitted. -| blob | value | item (WEAPONS order) | confirmed by | +Indices 0โ€“32 are the same item in either order, so this table stands unchanged +under the final mapping. + +| blob | value | item | confirmed by | |---|---|---|---| | 0 | 4 | `No_Equipment` | always available | | 1 | 2โ†’4 | `Machiene_Cannon_MG1` | the differential: bought for 4000 P | @@ -115,10 +121,50 @@ order. Nothing had to be fitted. Twelve concordances, no contradiction, over indices 0โ€“31. -## What the blob does **not** index: the tail is unexplained +## The full index space โœ… โ€” it is the **localisation** order, cut items included -Past index 31 the same alignment fails, and it fails against the screen, not -against a theory: +Written after the probes below, which settled it. The blob is **not** indexed by +`weapon.tbl`'s `WEAPONS` id list. It is indexed by the order the items appear in +`strings.tbl` โ€” the same order, *plus* the handful of items that exist only there: +`Adhesive Mine B2A` (a "Conditions to obtain" block whose cost is missing) and the +four late entries with a conditions *header* and no conditions. Those never appear +in the Arsenal, but they **hold slots in the save**, and that is the whole reason +the tail looked broken. + +| blob | block | items (display names, in order) | +|---|---|---| +| 0 | โ€” | `No_Equipment` | +| 1โ€“8 | GUN | Light Machine Gun MG1, Light Swivel MG2H, Heavy MG3, Heavy Swivel MG5H, Broad Sword SG1, Swing Sword SG2H, Twin Sword SG3II, Flail GP37 | +| 9โ€“16 | BEAM | **Stiletto BG1** โŒพ, Dagger BG2, Rapier BG4H, Pilum BP, Multi Pike BP, Spear HBP, Long Spear HBP, Gray Head T76H | +| 17โ€“20 | LASER | Saber LG1, Twin Saber LG2H, Needle L1GP, Fire Arrow L2GP | +| 21โ€“25 | MPM | **Falcon 9AM** โŒพ, Buzzard 10AM, Hawk 75AM, Condor 105AM, Eagle 120AM | +| 26โ€“30 | ASM | Terrier SMH, Hound SMH, Pointer SM, Piranha T53, White Shark T53R | +| 31โ€“38 | BOMB/ROCKET | **Dart 23 Rocket** โŒพ, Arrow 27 Rocket, *Adhesive Mine B2A โ€” never displayed*, EMP Mine B8E, Laser Mine B9L, Cauldron 50 Rocket, Cluster Mine B10, Maelstrom Bomb | +| 39โ€“47 | CANNON | **Tomahawk Alpha Rail Gun** โŒพ, Sling 75KG, Glaive 120mm Cannon, Arbalest 155KG, Monoceros Long Cannon, Grav Cannon XGS, *+ three never-displayed slots* | +| 48โ€“53 | SPECIAL | **Jamming System** โŒพ, Shield Doubler, Booster, Fire Control System, Regenerator, Cartridge Holder | + +โŒพ = pinned by an anchor: `9`, `21`, `39` and `48` were each **written into a save by +hand** and read back off the screen; `31` is the Dart 23 Rocket's price row in the +untouched save. The array ends exactly at 53 with the last SPECIAL item, with +nothing left over โ€” 8+8+4+5+5+8+9+6 plus `No_Equipment` is 54. + +The three never-displayed CANNON-block slots are three of `Ballista GSH`, +`Designator LH`, `Smoke Marker Launcher`, `Bumble UV` โ€” the four cut entries +`strings.tbl` lists between Grav Cannon XGS and Jamming System. **Which three** is +๐ŸŸก undetermined and does not matter for reading a save; nothing displays them. + +**This explains every anomaly in the retail save.** Its `4`s at 33 and at 45/46/47 +are the cut items โ€” `Adhesive Mine B2A` and three of the four above โ€” shipped +flagged as owned. They never render, so no player ever sees them, and they are +exactly the five "unexplained" owned entries that made the tail look wrong. The +Tomahawk reading `Developed` at `blob[39]` while `weapon.tbl` puts it at 38 was +the same off-by-N, seen from the other side. + +## How the tail was actually settled (and what was wrong first) + +Past index 31 the `weapon.tbl` alignment fails, and it fails against the screen, +not against a theory โ€” this is the evidence that sent the investigation to the +localisation order above: - `Tomahawk_a_Rail_Gun` is WEAPONS index **38** and the CANNON list shows it `Developed` (it is free once Stage 1 is complete) โ€” but **blob[38] = 0**. @@ -164,10 +210,28 @@ What it showed: under it the real save's `45/46/47` are Chaff Flare Dispencer / Shield Doubler / Booster, and the SPECIAL tab is empty in that same save. -So the tail is not a shifted copy of the list either. **Status: โ”** โ€” the value -alphabet and indices 0โ€“31 are settled; indices โ‰ฅ32 are not, and must not go into -the reimplementation. The next probe is cheap now that saves can be written: set -one tail index at a time and read which row changes. +Three further probes settled it, and the reasoning is worth keeping because the +first read of the same evidence was wrong. + +1. **Every slot owned.** With all 54 entries set to `4`, every tab filled and the + lists **wrap**, which gives exact counts: GUN 8, BEAM 8, LASER 4, MPM 5, ASM 5, + BOMB/ROCKET **7**, CANNON 6, SPECIAL 6 โ€” 43 items โ€” and the display order is + `weapon.tbl`'s order, item for item. Note what this probe *cannot* do: with + everything owned, any surjective mapping looks the same. It bounds, it does not + pin. It also killed the "gate-unmet flags are suppressed" idea outright โ€” + Maelstrom Bomb (Stage 6) and Grav Cannon XGS (Stage 12) both rendered + `Developed` at a Stage-2 save. +2. **One slot owned: `39`.** `blob = 0` except `9` (control) and `39` โ†’ BEAM showed + `STILETTO BG1 Developed` **and CANNON showed `TOMAHAWK ALPHA RAIL GUN + Developed`**. So the CANNON block starts at 39, not 38: exactly one slot more + than the display between Dart 23 (31) and the Tomahawk. +3. **One slot owned: `48`.** `blob = 0` except `39` and `48` โ†’ SPECIAL showed + `JAMMING SYSTEM Developed`. That fixes the SPECIAL base, and with six SPECIAL + items the array closes exactly at 53. + +The earlier probe's `45 = 4` leaving SPECIAL empty โ€” which had looked like the +mapping failing โ€” is simply a cut item: 45 is in the CANNON block's +never-displayed tail, not in SPECIAL at all. ### Two behaviours the probe exposed diff --git a/docs/re/captures/arsenal-all-owned-br-cannon.png b/docs/re/captures/arsenal-all-owned-br-cannon.png new file mode 100644 index 0000000..053184a Binary files /dev/null and b/docs/re/captures/arsenal-all-owned-br-cannon.png differ diff --git a/docs/re/captures/arsenal-probe-blob39-tomahawk.png b/docs/re/captures/arsenal-probe-blob39-tomahawk.png new file mode 100644 index 0000000..228a4a5 Binary files /dev/null and b/docs/re/captures/arsenal-probe-blob39-tomahawk.png differ diff --git a/docs/re/captures/arsenal-probe-blob48-jamming.png b/docs/re/captures/arsenal-probe-blob48-jamming.png new file mode 100644 index 0000000..1249efb Binary files /dev/null and b/docs/re/captures/arsenal-probe-blob48-jamming.png differ diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index 25c1272..38c6604 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -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 | ๐ŸŸก **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)) | +| +68 (raw 54) | see below | โœ… **per-item Arsenal development state**, one byte per item, `4` = owned, `2` = developable now, `0` = locked ([evidence](#the-develop-differential-one-weapon-three-fields)). Only the `4`s are stored state โ€” `2` is re-derived at load. The index space is **solved**: it is `strings.tbl`'s item order, cut items included ([mapping](../arsenal-develop-economy.md)) | 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): @@ -131,13 +131,13 @@ three-state progression *locked โ†’ developable โ†’ developed*, and the blob is Arsenal's development state, not a bitmask of owned weapons (a bitmask could not represent the middle state). -**Which item each index is** was chased separately and is **half settled** โ€” see -[the Arsenal develop economy](../arsenal-develop-economy.md). In short: the index -space is the `WEAPONS` id list in `GP_HANGAR_ARSENAL.pak :: eng\weapon.tbl`, whose -`No_Equipment โ€ฆ Wep_83` run is exactly 54 long; twelve independent concordances -confirm indices **0โ€“31** (including all six `2`s, predicted from the disc's own -"Conditions to obtain" text before the blob was looked at), while the tail from -index 33 on contradicts the screen and is left โ”. +**Which item each index is** was chased separately and is **solved** โ€” see +[the Arsenal develop economy](../arsenal-develop-economy.md). The index space is +the item order in `GP_HANGAR_ARSENAL.pak :: eng\strings.tbl`, i.e. the display +order **plus the cut items that only the localisation file lists**, pinned by four +hand-written probe saves (indices 9, 21, 39, 48) and closing exactly at 53. The +retail save's five otherwise-unexplained owned entries are those cut items, +shipped flagged as owned and never rendered. One more thing this does **not** settle, and should not be written as if it did: