# 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 — 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
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**
  (`Tomahawk Alpha Rail Gun — Complete Stage 1`). Those are still *bought*, for
  **0 P** — see the probe below, which caught one at `0 P` — 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.

## 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
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.

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 |
| 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.

## The full index space ✅ — it is the **localisation** order, cut items included

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**.
- 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.

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

- **`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.
