The container's derived fields turned out to be reproducible -- length+10 at
+0x30, payload length at +0x8c, adler32(payload) at +0x8e, everything else
copied -- and savegame_edit.py re-wraps a real save BYTE-IDENTICALLY, which is
the check that those three are the only ones. A hand-written save then loaded.
That replaced a blocked experiment (the tail question needed a mission payout,
and none of the currently developable items even sit in the disputed range) with
a direct one: write the blob, read the Arsenal.
- controls: 4 at index 9 -> STILETTO BG1 Developed, 21 -> FALCON 9AM
Developed. A hand-written 4 reaches the screen.
- tail: 4 at 33 and 45 left their rows dashed (both on screen, not below the
fold), and 38 left TOMAHAWK ALPHA RAIL GUN at "0 P" -- not owned. So the
tail is not the weapon.tbl order continued.
- clearing the real save's {22,26,39,46,47} cost the Tomahawk its Developed
status, which puts its flag in that set (39 positionally) -- but a uniform
+1 fails for SPECIAL, so no shift is asserted. Indices >=32 stay marked.
Two behaviours fell out. The title RE-DERIVES developable state on load and
announces it ("You can now develop Broad Sword ..."), so only the 4s are stored
state and a written 2 is pointless. And a no-cost item is bought for 0 P rather
than granted -- TOMAHAWK at "0 P" is what unowned looks like -- which is the
actual reason items read Developed in a save where nothing was spent.
Slot 03 was restored from its archived original (md5 verified); slots 01/02 were
never touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
192 lines
10 KiB
Markdown
192 lines
10 KiB
Markdown
# The Arsenal develop economy, and what the save's blob indexes
|
||
|
||
**2026-08-11.** The save file's 54-byte blob was shown to be per-item development
|
||
state by [a runtime differential](structures/savegame-format.md#the-develop-differential-one-weapon-three-fields) —
|
||
one weapon bought, two entries moved, `2 → 4` for the item and `0 → 2` for the
|
||
successor. That named the *values*. This note names the *index space*, and in
|
||
passing recovers the whole develop economy from the disc.
|
||
|
||
## Where the Arsenal gets its data
|
||
|
||
`dat/GP_HANGAR_ARSENAL.pak` carries one screen-config record per language whose
|
||
fields point at the rest:
|
||
|
||
```
|
||
PATH = dat\GP_HANGAR_ARSENAL.pak+eng\
|
||
WEAPON = weapon.tbl STRINGS = strings.tbl
|
||
WEAPON_CATEGORIES = 8 Detail_Window_Known / Detail_Window_Unknown
|
||
ConditionToDevelop = 757,228 WeaponDesc = 734,502 Range = 891,73 …
|
||
```
|
||
|
||
`WEAPON_CATEGORIES = 8` is the eight tabs the screen cycles with LB/RB (GUN,
|
||
BEAM, LASER, MULTIPURPOSE MISSILE, ANTI-SHIP MISSILE, BOMB/ROCKET, CANNON,
|
||
SPECIAL). The `…= x,y` fields are screen positions for that screen's labels —
|
||
`ConditionToDevelop` is *where the conditions text is drawn*, not a condition.
|
||
|
||
### `eng\weapon.tbl` — the item table (IDXD, schema `0x03c244b5`, 66 records)
|
||
|
||
Its string pool opens with a stage list, then the literal key `WEAPONS`
|
||
followed by the item ids **in table order**:
|
||
|
||
```
|
||
No_Equipment, Machiene_Cannon_MG1, Machiene_Cannon_MG2H, Machiene_Cannon_MG3,
|
||
Machiene_Cannon_MG5H, Broad_Sword_SG1, Swing_Sword_SG2H, Twin_Sword_SG3II,
|
||
Frail_GP37, Stiletto_BG1, Dagger_BG2, Rapier_BG4H, Pilum_BP, … , Wep_83,
|
||
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.
|
||
|
||
Each item then has a record: `Size, Weight, Range, Power, Speed, Stage,
|
||
MissionObjective, Dependency, Points`. For the **first** record all of it reads
|
||
straight out, and it matches the running game exactly:
|
||
|
||
| field | disc | in-game DATA SHEET for Light Machine Gun MG I |
|
||
|---|---|---|
|
||
| Range | `D` | `Range Class D` |
|
||
| Power | `E` | `Damage Class E` |
|
||
| Speed | `-` | `Speed Class –` |
|
||
| Weight | `0.3` | `Weight Class Light` |
|
||
| Points | `4000` | `4000 P` on the develop row |
|
||
|
||
**Caveat, and it is a real one:** IDXD string pools store each distinct string
|
||
*once*, so from the second record onward every value that repeats an earlier
|
||
string is absent from the token stream. Record 2 (MG2H) shows only its unique
|
||
values — `0.4` and `3000` — and record 6 shows no number at all because its cost
|
||
had already been seen. So `weapon.tbl`'s per-item fields cannot be read from the
|
||
pool alone; they need the binary node/index region, which is still undecoded
|
||
(see the module docs in `idxd.rs`). The prose table below is the practical
|
||
substitute.
|
||
|
||
### `eng\strings.tbl` — the economy in prose (IXUD, UTF-16BE)
|
||
|
||
The same pak's string table holds, per item: the internal id, the display name,
|
||
the description, and a **"Conditions to obtain …"** block that spells out the
|
||
gate and the price:
|
||
|
||
> Conditions to obtain Light Swivel Machine Gun MG2H — Completed development of
|
||
> the Light Machine Gun MG1 — Must spend 3000 points to develop
|
||
|
||
All 60 of them are extracted to
|
||
[`captures/arsenal-develop-conditions.csv`](captures/arsenal-develop-conditions.csv)
|
||
with the stage gate, the cost and the prerequisite split out. The shape of the
|
||
economy:
|
||
|
||
- gates are **stage completion** (`Complete Stage 1 … 15`), **a predecessor
|
||
item** (`Completed development of the …`), or **an ace kill** (`Shoot down the
|
||
ace pilot in Stage 11/13/14`);
|
||
- costs run 3 000 → 350 000 points, and **twenty items have no cost at all** —
|
||
they are granted the moment their gate opens (`Tomahawk Alpha Rail Gun —
|
||
Complete Stage 1`), which is why they read as `Developed` in a list the player
|
||
never spent points in;
|
||
- two items are anomalies worth flagging: `Adhesive Mine B2A` says "Must spend
|
||
points to develop" with **no number**, and ten late entries (`Ballista GSH`,
|
||
`Designator LH`, `Smoke Marker Launcher`, `Bumble UV`, `Ballista 24 Rocket
|
||
Launcher`, `Thrush 220AM`, `Divider L3GP`, `Mace GP25H`, `Thor Gun System`,
|
||
`Spitfire BX`) have a conditions *header* and no conditions — ❔ cut content or
|
||
non-player weapons.
|
||
|
||
## What the blob indexes: the first 32 entries, 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
|
||
captured state (Stage 1 complete, Stage 02 at standby, 4101 P) the conditions say
|
||
exactly six items are developable — Light Machine Gun MG1, Broad Sword SG1,
|
||
Dagger BG2, Pilum BP, Hound SMH, Dart 23 Rocket — and the blob's **six** `2`s sit
|
||
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 |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `No_Equipment` | always available |
|
||
| 1 | 2→4 | `Machiene_Cannon_MG1` | the differential: bought for 4000 P |
|
||
| 2 | 0→2 | `Machiene_Cannon_MG2H` | the differential: announced as newly developable |
|
||
| 5 | 2 | `Broad_Sword_SG1` | `BROAD SWORD SG1 5000P`, **below the fold** in the GUN list |
|
||
| 9 | 4 | `Stiletto_BG1` | `Developed`; "Initially Mounted"; mounted as NOSE WEAPON |
|
||
| 10 | 2 | `Dagger_BG2` | `DAGGER BG2 5000P` |
|
||
| 12 | 2 | `Pilum_BP` | `PILUM BP 6000P` |
|
||
| 21 | 4 | `Falcon_9AM` | `Developed`; "Initially Mounted"; mounted as MAIN WEAPON 1 |
|
||
| 22 | 4 | `Buzzard_10AM` | `Developed`; free at Stage 1 |
|
||
| 26 | 4 | `Terrier_SMH` | `Developed`; free at Stage 1 |
|
||
| 27 | 2 | `Hound_SMH` | `HOUND SMH 10000P` |
|
||
| 31 | 2 | `Dirt_23_Rocket` | `DART 23 ROCKET 3500P` ("Dart" is the localised "Dirt") |
|
||
|
||
Twelve concordances, no contradiction, over indices 0–31.
|
||
|
||
## What the blob does **not** index: the tail is unexplained
|
||
|
||
Past index 31 the same alignment fails, and it fails against the screen, not
|
||
against a theory:
|
||
|
||
- `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**.
|
||
- The blob's remaining `4`s are at **33, 39, 45, 46, 47**. In WEAPONS order those
|
||
are `EMP_Mine_B8E`, `Sling_75KG`, `Shield_Doubler`, `Booster`,
|
||
`FireControlSystem` — and **every one of them is shown as not developed**:
|
||
BOMB/ROCKET lists only `DART 23 ROCKET 3500P`, CANNON only the Tomahawk, and
|
||
**the SPECIAL tab is entirely empty** (all rows dashed). Their conditions agree
|
||
— Shield Doubler needs Stage 12, Booster an ace kill in Stage 11, FCS Stage 6.
|
||
|
||
So five `4`s in the tail correspond to nothing the Arsenal reports as owned, and
|
||
one genuinely-owned item reads `0`. A single `+1` shift does not repair it
|
||
either: `blob[39] = 4` would put the Tomahawk right, but then `blob[33] = 4`
|
||
lands on `Arrow_27_Rocket` (Stage 2 + 5000 P, unreachable here) and 45/46/47
|
||
still land on locked SPECIAL items.
|
||
|
||
### Tested actively, with a hand-written save
|
||
|
||
Waiting for a mission payout was not necessary. The save container's derived
|
||
fields turned out to be reproducible (see
|
||
[`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) — the whole
|
||
container re-wraps **byte-identically**), so the blob could be *written* and the
|
||
result read off the screen. A probe save was built in our own throwaway slot 03
|
||
with the blob zeroed except `0, 9, 21, 33, 38, 45 = 4` and `39, 46 = 2`, and it
|
||
**loaded**.
|
||
|
||
What it showed:
|
||
|
||
- **The controls rendered.** `9` → BEAM shows `STILETTO BG1 Developed`; `21` →
|
||
MULTIPURPOSE MISSILE shows `FALCON 9AM Developed`. So a hand-written `4` does
|
||
reach the screen, and indices 9 and 21 are those items — now by experiment,
|
||
not by correlation.
|
||
- **The tail probes rendered nothing.** `33` should have made a BOMB/ROCKET row
|
||
`Developed` (row 3 of that list, on screen, not below the fold) — it stayed
|
||
dashed. `45` should have made a SPECIAL row `Developed` — the SPECIAL tab
|
||
stayed entirely empty. And `38` should have been the Tomahawk — instead the
|
||
CANNON list showed `TOMAHAWK ALPHA RAIL GUN — 0 P`, i.e. *not owned*.
|
||
- **Clearing the real save's tail `4`s cost the Tomahawk its status.** In the
|
||
unedited save the Tomahawk reads `Developed` while `blob[38] = 0`; the probe
|
||
cleared `{22, 26, 39, 46, 47}` and the Tomahawk stopped being owned — so its
|
||
flag is one of the cleared entries, with **39** the positional candidate. A
|
||
uniform `+1` shift past index 32 would explain that, but it fails elsewhere:
|
||
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.
|
||
|
||
### Two behaviours the probe exposed
|
||
|
||
- **`2` is derived, not stored.** On load the title recomputes which items are
|
||
developable from its own conditions and *announces the difference* — zeroing
|
||
Light Machine Gun MG1 and Broad Sword SG1 produced "You can now develop Broad
|
||
Sword …" and both came back priced in the list. Only the owned (`4`) entries
|
||
are authoritative state; writing `2` is pointless, and writing `0` over an
|
||
item whose conditions are met is undone at load.
|
||
- **The free items are bought for zero, not granted.** `TOMAHAWK ALPHA RAIL GUN
|
||
— 0 P` is what an unowned no-cost item looks like. That is the real reason a
|
||
save where the player never spent points still shows items as `Developed`
|
||
(this note previously said "granted" — the mechanism is a 0 P purchase).
|
||
|
||
## Evidence
|
||
|
||
- [`captures/arsenal-develop-conditions.csv`](captures/arsenal-develop-conditions.csv) — all 60 conditions blocks.
|
||
- [`captures/arsenal-categories-all.png`](captures/arsenal-categories-all.png) — the eight category tabs.
|
||
- [`captures/arsenal-gun-below-fold.png`](captures/arsenal-gun-below-fold.png) — Broad Sword SG1, the entry that is not visible without scrolling.
|
||
- [`captures/arsenal-probe-save-result.png`](captures/arsenal-probe-save-result.png) — the hand-written save's result: controls at 9/21 render `Developed`, the tail probes render nothing, and the Tomahawk shows `0 P`.
|
||
- [`captures/arsenal-gun-list-predevelop.png`](captures/arsenal-gun-list-predevelop.png) · [`captures/arsenal-mg1-developed.png`](captures/arsenal-mg1-developed.png) — the differential itself.
|