diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index b79ac2a2..131d4a46 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -827,6 +827,42 @@ unknown, what evidence exists, and what the first step would be. Move an item in field; the stat keys in records not opened here (`MegaTons`, `BulletLimit`, `CrewCount`, `HP_CLASS`); why the roster lists 59 of 131. +* ✅✅ **(2026-08-27) THE 59-OF-131 ARSENAL QUESTION IS CLOSED — an Arsenal item + names a HARDPOINT SLOT, not a weapon.** + [structures/arsenal-item-weapon-chain](structures/arsenal-item-weapon-chain.md), + artefact `data/arsenal-chain.txt`, regenerator + `tools/re-capture/arsenal_chain.py`. + Open since the weapon datasheet was decoded, and one wrong answer was + refuted yesterday. The link is **three hops**: + `Arbalest_155KG.PlayerWeapon` → **`Turret_050`** (a slot on + `UN_f001_TCAF_DeltaSaber_T_Player`) → `.WeaponID` → + `Weapon_DSaber_P_wep_50_Cannon`. + 🧪 The controls, both run in the same loop: **0 / 59** distinct `PlayerWeapon` + values are a `Weapon.ID`; **59 / 59** are a `Turret_NNN` slot id; the full + chain lands on a `Weapon.ID` **59 / 59**. `WingmanWeapon` resolves + identically. The `WEAPONS` roster's 59 = 55 item names + 4 empty-slot + sentinels (`No_Equipment`, `NullWeapon_Arm1/2/3`). + 🔑 **Wingmen fly a cheaper gun.** Following the same 59 slots across craft + variants: the `_Player` tables give each item **59 distinct** weapon records + (`Weapon_DSaber_P_wep_NN_*`); the AI tables (`…DeltaSaber_T`, `_W`, `_EX5`) + collapse all 59 onto **10** generic classes (`Weapon_TCAF_DeltaSaber_Beam`, + `…_Gun`, …). That is most of the 131: 59 player + 10 AI-Saber + ship/ADAN + turret guns. + ✅ **Upgrades yesterday's 🟡** — the "hardpoint catalogue" reading of the + player craft's 63 `Turret_NNN` slots is now ADOPTED, proved from an + independent file. ⚠️ And it corrects a number in that entry: the "10 distinct + `WeaponID`s" was measured on the **AI** variant; `_Player` has 59. + An item record also carries `Model`/`Package`/`SilhouetteModel` (the meshes + the corpus decodes), `Points` + `Stage` (the develop economy + `arsenal-develop-economy.md` recovered from the screen side), the Hangar stat + bars `Power`/`Range`/`Size`/`Speed`/`Weight`, and `Type` = a slot class. + 🟡 Not settled: `Type` and the per-slot allow-lists (`STANDARD_ARM1/2/3`, + `PlayerSET_*`, `ExSET_*`, `SNNSET_*` — records whose only field is `Type`); + the **wingman loadout** records (`Bird1-Sandra`, `Rhino1-Katana`, … with + `Arm1`/`Arm2`/`Arm3`/`Nose`/`UnitID`) — named here, not opened; empty + `Dependency`/`MissionObjective`; which weapons the 26 never-on-a-turret ones + are. + * ✅ **(2026-08-27) THE DESTRUCTIBLE-SUBSYSTEM MODEL — a unit's sub-records, opened at last. 835 turrets, and every turret's weapon resolves.** [structures/unit-substructure-records](structures/unit-substructure-records.md), diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 33381d0d..b2a970db 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -99,6 +99,7 @@ files, which is how the same ground got covered twice. | [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files | | [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways | | [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer — two sequential countdowns, not a limit and a warning | ✅ CONFIRMED — read from `sub_822639B8`; the threshold reading is | +| [`structures/arsenal-item-weapon-chain.md`](structures/arsenal-item-weapon-chain.md) | The 59-of-131 arsenal question — an item names a hardpoint slot, not a weapon | ✅✅ CONFIRMED — 0/59 `PlayerWeapon` are a `Weapon.ID`, 59/59 are a `Turret_NNN` slot | | [`structures/unit-substructure-records.md`](structures/unit-substructure-records.md) | The destructible-subsystem model — a unit's `Turret`/`Bridge`/`Hatch`/`ShieldGenerator`/`Thruster` sub-records | ✅ CONFIRMED — 835 turrets, 835/835 `WeaponID` resolve, `StructureCount` control 667/684 | | [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model — `Generic`, `Maneuver`, `Effect` | ✅ CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read | | [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet — `Weapon`, `Shell`, `AssortMissileParam` | ✅ CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read | diff --git a/docs/re/data/arsenal-chain.txt b/docs/re/data/arsenal-chain.txt new file mode 100644 index 00000000..665f8958 --- /dev/null +++ b/docs/re/data/arsenal-chain.txt @@ -0,0 +1,72 @@ +# The Arsenal item -> hardpoint slot -> Weapon record chain +# Regenerate: python3 tools/re-capture/arsenal_chain.py +# See docs/re/structures/arsenal-item-weapon-chain.md + +## the roster and the item records + WEAPONS roster values 59 + arsenal ITEM records 59 + item names that are roster values 55 + roster values with no item record: ['No_Equipment', 'NullWeapon_Arm1', 'NullWeapon_Arm2', 'NullWeapon_Arm3'] + Weapon.ID values in the datasheet 131 + +## CONTROL item.PlayerWeapon is NOT a Weapon.ID + distinct PlayerWeapon values 59 ; how many are a Weapon.ID: 0 + +## CONTROL item.PlayerWeapon IS a Turret_NNN slot id + UN_f001_TCAF_DeltaSaber_T slots 63 Player 59/59 Wingman 59/59 + UN_f001_TCAF_DeltaSaber_T_EX5 slots 63 Player 59/59 Wingman 59/59 + UN_f001_TCAF_DeltaSaber_T_EX5_el slots 63 Player 59/59 Wingman 59/59 + UN_f001_TCAF_DeltaSaber_T_Player slots 63 Player 59/59 Wingman 59/59 + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 slots 5 Player 3/59 Wingman 3/59 + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 slots 5 Player 1/59 Wingman 1/59 + UN_f001_TCAF_DeltaSaber_T_Ttrl slots 4 Player 4/59 Wingman 4/59 + UN_f002_TCAF_DeltaSaber_W slots 63 Player 59/59 Wingman 59/59 + UN_f002_TCAF_DeltaSaber_W_Player slots 63 Player 59/59 Wingman 59/59 + UN_f004_TCAF_DeltaSaber_A_Player slots 63 Player 59/59 Wingman 59/59 + +## THE CHAIN, per craft variant: item -> slot -> WeaponID -> Weapon.ID + UN_f001_TCAF_DeltaSaber_T 59/ 59 land on a Weapon.ID, 10 distinct + slot WeaponIDs: 59 non-NULL, 10 distinct, e.g. ['Weapon_TCAF_DeltaSaber_ASMissile', 'Weapon_TCAF_DeltaSaber_Beam'] + UN_f001_TCAF_DeltaSaber_T_EX5 59/ 59 land on a Weapon.ID, 10 distinct + slot WeaponIDs: 59 non-NULL, 10 distinct, e.g. ['Weapon_TCAF_DeltaSaber_ASMissile', 'Weapon_TCAF_DeltaSaber_Beam'] + UN_f001_TCAF_DeltaSaber_T_EX5_el 59/ 59 land on a Weapon.ID, 10 distinct + slot WeaponIDs: 59 non-NULL, 10 distinct, e.g. ['Weapon_TCAF_DeltaSaber_ASMissile', 'Weapon_TCAF_DeltaSaber_Beam'] + UN_f001_TCAF_DeltaSaber_T_Player 59/ 59 land on a Weapon.ID, 59 distinct + slot WeaponIDs: 59 non-NULL, 59 distinct, e.g. ['Weapon_DSaber_P_wep_01_Beam', 'Weapon_DSaber_P_wep_02_Missile'] + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 3/ 59 land on a Weapon.ID, 3 distinct + slot WeaponIDs: 3 non-NULL, 3 distinct, e.g. ['Weapon_DSaber_P_wep_01_Beam', 'Weapon_DSaber_P_wep_02_Missile'] + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 1/ 59 land on a Weapon.ID, 1 distinct + slot WeaponIDs: 1 non-NULL, 1 distinct, e.g. ['Weapon_DSaber_P_wep_01_Beam'] + UN_f001_TCAF_DeltaSaber_T_Ttrl 4/ 59 land on a Weapon.ID, 4 distinct + slot WeaponIDs: 4 non-NULL, 4 distinct, e.g. ['Weapon_TCAF_DeltaSaber_ASMissile', 'Weapon_TCAF_DeltaSaber_Beam'] + UN_f002_TCAF_DeltaSaber_W 59/ 59 land on a Weapon.ID, 10 distinct + slot WeaponIDs: 59 non-NULL, 10 distinct, e.g. ['Weapon_TCAF_DeltaSaber_ASMissile', 'Weapon_TCAF_DeltaSaber_Beam'] + UN_f002_TCAF_DeltaSaber_W_Player 59/ 59 land on a Weapon.ID, 59 distinct + slot WeaponIDs: 59 non-NULL, 59 distinct, e.g. ['Weapon_DSaber_P_wep_01_Beam', 'Weapon_DSaber_P_wep_02_Missile'] + UN_f004_TCAF_DeltaSaber_A_Player 59/ 59 land on a Weapon.ID, 59 distinct + slot WeaponIDs: 59 non-NULL, 59 distinct, e.g. ['Weapon_DSaber_P_wep_01_Beam', 'Weapon_DSaber_P_wep_02_Missile'] + + player craft slots not referenced by any item: ['Turret_000', 'Turret_097', 'Turret_098', 'Turret_099'] + +## a full item record -- Arbalest_155KG + Dependency + MissionObjective + Model rou_f001_wep_50_hangar + Package rou_f001_wep_50_hangar.xpr + PlayerLOD Equip_EnumLODSet_wep_50.tbl + PlayerMotion Equip_EnumGameModel_wep_50.tbl + PlayerPackage rou_f001_wep_50.xpr + PlayerWeapon Turret_050 + Points 25000 + Power B + Range B + SilhouetteModel rou_f001_wep_50_NA + Size L + Speed - + Stage 6 + Type g04b + Weight 1.5 + WingmanLOD Equip_EnumLODSet_wep_50.tbl + WingmanMotion Equip_EnumGameModel_wep_50.tbl + WingmanPackage rou_f001_wep_50.xpr + WingmanWeapon Turret_050 diff --git a/docs/re/structures/arsenal-item-weapon-chain.md b/docs/re/structures/arsenal-item-weapon-chain.md new file mode 100644 index 00000000..be0d3f5b --- /dev/null +++ b/docs/re/structures/arsenal-item-weapon-chain.md @@ -0,0 +1,92 @@ +# ✅✅ The 59-of-131 arsenal question — an item names a HARDPOINT, not a weapon + +Open since the weapon datasheet was decoded: the Hangar's arsenal lists **59** +weapons, the disc's `Weapon` table has **131**, and +[the previous pass](unit-substructure-records.md) showed the two name sets +overlap in **0** values. This closes it. + +**An Arsenal item does not reference a `Weapon` record. It references a +`Turret_NNN` HARDPOINT SLOT on the player craft's own unit table, and the slot +is what carries the `WeaponID`.** Three hops: + +``` +GP_HANGAR_ARSENAL.pak Arbalest_155KG + .PlayerWeapon ───▶ Turret_050 (a slot on UN_f001_TCAF_DeltaSaber_T_Player) + .WeaponID ───▶ Weapon_DSaber_P_wep_50_Cannon (a Weapon record) +``` + +Artefact `../data/arsenal-chain.txt`, regenerator +`tools/re-capture/arsenal_chain.py`. + +## 🧪 The controls + +| | | +|---|---:| +| `WEAPONS` roster values | 59 | +| arsenal ITEM records (field set `Model`/`Package`/`PlayerWeapon`/`Points`/`Power`/`Range`/…) | **59** | +| item names that are roster values | 55 | +| roster values with no item record | 4 — `No_Equipment`, `NullWeapon_Arm1/2/3` | +| **distinct `PlayerWeapon` values that are a `Weapon.ID`** | **0 / 59** | +| **distinct `PlayerWeapon` values that are a `Turret_NNN` slot id** | **59 / 59** | +| the full chain lands on a `Weapon.ID` | **59 / 59** | + +`WingmanWeapon` resolves identically, 59/59. The 4 roster entries with no item +record are the empty-slot sentinels, so the roster is 55 real weapons + 4 nulls +against 59 item records — the item table is the superset. + +## 🔑 Wingmen fly a cheaper gun + +Following the same 59 slots on different craft variants splits them cleanly: + +| craft variant | slots | distinct `Weapon.ID` reached | +|---|---:|---:| +| `…DeltaSaber_T_Player`, `_W_Player`, `_A_Player` | 63 | **59** (`Weapon_DSaber_P_wep_NN_*`) | +| `…DeltaSaber_T`, `_W`, `_T_EX5`, `_T_EX5_el` | 63 | **10** (`Weapon_TCAF_DeltaSaber_Beam`, `…_Gun`, …) | + +Same slot ids, same 59 arsenal items, but the **`_Player` tables give each item +its own weapon record while the AI tables collapse all 59 onto 10 generic +behaviour classes**. A wingman carrying an `Arbalest_155KG` does not fire the +player's `wep_50` ballistics. + +That also explains the 131: 59 player weapons + 10 AI-Saber classes + the ship +guns and ADAN weapons the turrets use. + +Four of the player craft's 63 slots are referenced by no item: `Turret_000` +(the `Weapon_NULL` slot) and `Turret_097` / `_098` / `_099`. + +## ✅ Upgrades a 🟡 from the previous pass + +[unit-substructure-records](unit-substructure-records.md) read the player +craft's 63 `Turret_NNN` records as "a hardpoint catalogue, `TurretCount 4` = +how many mount at once", marked 🟡 because nothing showed the selection. The +arsenal table **is** the selection, from an independent file, 59/59. Adopted. + +## The item record + +``` +Model / Package rou_f001_wep_50_hangar (+ .xpr) the Hangar display mesh +PlayerPackage / _LOD / _Motion rou_f001_wep_50.xpr, Equip_EnumLODSet_wep_50.tbl, +WingmanPackage / _LOD / _Motion Equip_EnumGameModel_wep_50.tbl +SilhouetteModel rou_f001_wep_50_NA +PlayerWeapon / WingmanWeapon Turret_050 +Points 25000 Stage 6 develop cost, and the stage that unlocks it +Power B Range B Size L Speed - Weight 1.5 the Hangar's stat bars +Type g04b the slot-compatibility class +Dependency MissionObjective (empty on this item) +``` + +`Model` ties an arsenal entry to the meshes the corpus already decodes; `Points` +and `Stage` are the develop economy that +[arsenal-develop-economy](../arsenal-develop-economy.md) recovered from the +screen side. + +## 🟡 Not settled + +* `Type` (`g04b`) is a slot-compatibility class. `STANDARD_ARM1/2/3`, + `STANDARD_NOSE`, `PlayerSET_*`, `ExSET_*`, `S01SET_*`, `S02SET_*` are records + whose only field is `Type` — the per-slot allow-lists — and the loadout + records (`Bird1-Sandra`, `Rhino1-Katana`, … with `Arm1`/`Arm2`/`Arm3`/`Nose`/ + `UnitID`) are the **wingman loadouts**. Named here, not opened. +* `Dependency` and `MissionObjective` are empty on the sampled item; what fills + them elsewhere is unread. +* Which `Weapon` records the 26 never-on-a-turret weapons are. diff --git a/docs/re/structures/unit-substructure-records.md b/docs/re/structures/unit-substructure-records.md index e16584ac..8be537dd 100644 --- a/docs/re/structures/unit-substructure-records.md +++ b/docs/re/structures/unit-substructure-records.md @@ -94,11 +94,16 @@ declared=1 records=2 x1 UN_e001_ADAN_Elan_EX4 declared=8 records=9 x1 UN_e107_ADAN_AAFrigate_EX4 ``` -🟡 The reading — **not adopted**: on the player craft the `Turret_NNN` list is -a *hardpoint catalogue* and `TurretCount 4` is how many mount at once. It fits -(63 slots, all `Frame=GN_mnt`, all `IsAuto=No`, 10 distinct `WeaponID`s across -them, and the tutorial variants carry a cut-down 5) but nothing here shows the -selection. The two `+1` warship/craft cases are unexplained. +✅ **ADOPTED the next day** — on the player craft the `Turret_NNN` list is a +*hardpoint catalogue* and `TurretCount 4` is how many mount at once. The +Arsenal table proves it from an independent file: **59/59 of its items name a +`Turret_NNN` slot in `PlayerWeapon`**, see +[arsenal-item-weapon-chain](arsenal-item-weapon-chain.md). The two `+1` +warship/craft cases are still unexplained. + +⚠️ The "10 distinct `WeaponID`s" measured here is the **AI** variant +`UN_f001_TCAF_DeltaSaber_T`. The `_Player` variants give the same 59 slots +**59 distinct** weapon records — wingmen fly a collapsed 10-class set. ## 🧪 Control 2 — turret weapons resolve into the weapon datasheet diff --git a/tools/re-capture/arsenal_chain.py b/tools/re-capture/arsenal_chain.py new file mode 100644 index 00000000..767e6eca --- /dev/null +++ b/tools/re-capture/arsenal_chain.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Resolve the arsenal item -> hardpoint slot -> weapon record chain. + +An Arsenal item in GP_HANGAR_ARSENAL.pak names a HARDPOINT SLOT on the player +craft (`PlayerWeapon = Turret_050`), not a weapon. The slot's `WeaponID` is +what points into the 131-record `Weapon` datasheet. Regenerates +docs/re/data/arsenal-chain.txt. +""" +import sys, os, glob, re, collections + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import unitgroup as U +from unit_substructures import pak_entries + +ITEMF = {'Dependency', 'MissionObjective', 'Model', 'Package', + 'PlayerWeapon', 'Points', 'Power', 'Range'} + +def main(): + ars = glob.glob('/work/sylph_extract/**/GP_HANGAR_ARSENAL.pak', recursive=True)[0] + mg = glob.glob('/work/sylph_extract/**/GP_MAIN_GAME_E.pak', recursive=True)[0] + + slots = collections.defaultdict(dict); wids = set() + for h, b in pak_entries(mg): + if b[:4] != b'IDXD': continue + try: recs = U.parse(b) + except Exception: continue + for r in recs: + if r['squadron'] == 'Weapon': + i = U.named(r).get('ID') + if i: wids.add(i) + g = [r for r in recs if r['squadron'] == 'Generic'] + if not (g and 'HP' in U.named(g[0])): continue + uid = U.named(g[0]).get('ID') + for r in recs: + if re.fullmatch(r'Turret_\d{3}', r['squadron']): + slots[uid][r['squadron']] = U.named(r).get('WeaponID') + + items = {}; roster = None + for h, b in pak_entries(ars): + if b[:4] != b'IDXD': continue + try: recs = U.parse(b) + except Exception: continue + for r in recs: + if r['squadron'] == 'WEAPONS' and roster is None: roster = r + if ITEMF <= set(U.named(r)): items.setdefault(r['squadron'], r) + + print("# The Arsenal item -> hardpoint slot -> Weapon record chain") + print("# Regenerate: python3 tools/re-capture/arsenal_chain.py") + print("# See docs/re/structures/arsenal-item-weapon-chain.md") + rset = {v for _, _, v in roster['fields']} + print("\n## the roster and the item records") + print(" WEAPONS roster values %3d" % len(rset)) + print(" arsenal ITEM records %3d" % len(items)) + print(" item names that are roster values %3d" % len(set(items) & rset)) + print(" roster values with no item record: %s" % sorted(rset - set(items))) + print(" Weapon.ID values in the datasheet %3d" % len(wids)) + + print("\n## CONTROL item.PlayerWeapon is NOT a Weapon.ID") + pw = {U.named(r).get('PlayerWeapon', '').strip() for r in items.values()} + print(" distinct PlayerWeapon values %3d ; how many are a Weapon.ID: %d" % (len(pw), len(pw & wids))) + + print("\n## CONTROL item.PlayerWeapon IS a Turret_NNN slot id") + for craft in sorted(slots): + if 'Saber' not in craft: continue + s = slots[craft] + hit = sum(1 for r in items.values() if U.named(r).get('PlayerWeapon', '').strip() in s) + wing = sum(1 for r in items.values() if U.named(r).get('WingmanWeapon', '').strip() in s) + print(" %-40s slots %2d Player %2d/%d Wingman %2d/%d" % + (craft, len(s), hit, len(items), wing, len(items))) + + print("\n## THE CHAIN, per craft variant: item -> slot -> WeaponID -> Weapon.ID") + for craft in sorted(slots): + if 'Saber' not in craft: continue + s = slots[craft] + reach = [s.get(U.named(r).get('PlayerWeapon', '').strip()) for r in items.values()] + land = [w for w in reach if w in wids] + print(" %-40s %3d/%3d land on a Weapon.ID, %2d distinct" % + (craft, len(land), len(items), len(set(land)))) + vs = [v for v in s.values() if v and v != 'Weapon_NULL'] + print(" slot WeaponIDs: %d non-NULL, %d distinct, e.g. %s" % + (len(vs), len(set(vs)), sorted(set(vs))[:2])) + + s = slots['UN_f001_TCAF_DeltaSaber_T_Player'] + used = {U.named(r).get('PlayerWeapon', '').strip() for r in items.values()} + print("\n player craft slots not referenced by any item: %s" % sorted(set(s) - used)) + + k = sorted(items)[0] + print("\n## a full item record -- %s" % k) + for kk, vv in sorted(U.named(items[k]).items()): + print(" %-22s %s" % (kk, vv)) + +main() diff --git a/tools/re-capture/unit_substructures.py b/tools/re-capture/unit_substructures.py index 9a27661c..ba30c1b6 100644 --- a/tools/re-capture/unit_substructures.py +++ b/tools/re-capture/unit_substructures.py @@ -118,4 +118,5 @@ def main(): print(" resolve: %d do NOT resolve: %d" % (hit, sum(wids.values()) - hit)) print(" weapons never mounted on a turret: %d" % len(wnames - set(wids))) -main() +if __name__ == "__main__": + main()