From 15c9e13c12875ce374c19fc0d67ec7876afeccad Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 15:35:09 +0000 Subject: [PATCH] re: CoverArea is a 6-bit MOUNT mask; the three other turret leftovers closed Six bits, never more. Over all 835 turrets: 41 distinct values, max 0x3f, bits 6/7 set on none; per-bit 709/117/274/274/260/237. 0x00 (34) = the empty Weapon_NULL hardpoints with YawLimit 0; 0x01 alone (439) = craft hardpoints with YawLimit 2.5/1.0/0; 2-6 bits (362) = warship mounts with YawLimit 45-180. It tracks the MOUNT, not the weapon. UN_e107_ADAN_AAFrigate has eight identical AAFrigate_AAGun turrets: GN_GunXS_01..04 are 0x1d and 05..08 are 0x2d -- same gun, same YawLimit 120, different mask. The Battleship spreads five masks over GN_TGunL_01..05 / GN_TGunM_02..03, all firing the same CAF_Ship_ASGun. Which sector each bit denotes is NOT determined; six bits and the name invite +-X/+-Y/+-Z or six hull faces, but nothing static fixes the convention. Not adopted. Refuted on the way: bits 2 and 3 are not a mutually-exclusive pair -- 188 turrets set both. The 26 weapons no turret mounts are a coherent set: 11 _P player variants, the nose/twin mounts, two _Child sub-munitions, the three S16Boss_*, three Weapon_Test_*, and ADAN_Attacker_S_GunTurret. Versatile_NNN ships nowhere -- 0 populated records across all 41 archives, only the ??? template row. The two units one turret short: both extras are missile mounts with no Frame. Elan_EX4 = NoseGun + Missile, both mask 0x01, both frameless; AAFrigate_EX4 = eight framed guns plus one Ship_AAMissile at 0x0c with no Frame. n=2, stated as the observed pattern, not a rule. New artefact and regenerator; the other nine regenerate byte-identical. --- docs/re/BACKLOG.md | 35 ++++++ docs/re/data/turret-coverarea.txt | 114 ++++++++++++++++++ .../structures/unit-substructure-records.md | 60 ++++++++- tools/re-capture/turret_coverarea.py | 109 +++++++++++++++++ 4 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 docs/re/data/turret-coverarea.txt create mode 100644 tools/re-capture/turret_coverarea.py diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index fe18d83..43559cc 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -827,6 +827,41 @@ 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) `CoverArea` IS A 6-BIT MOUNT MASK β€” plus the three other + turret leftovers, all closed.** + [structures/unit-substructure-records](structures/unit-substructure-records.md), + artefact `data/turret-coverarea.txt`, regenerator + `tools/re-capture/turret_coverarea.py`. + **Six bits, never more.** Over all **835** turrets: 41 distinct values, max + `0x3f`, **bits 6/7 set on none**; per-bit 709/117/274/274/260/237. `0x00` (34) + = the empty `Weapon_NULL` hardpoints, `YawLimit 0` on all 34; `0x01` alone + (439) = craft hardpoints, `YawLimit` 2.5/1.0/0; 2-6 bits (362) = warship + mounts with `YawLimit` 45-180. + πŸ”‘ **It tracks the MOUNT, not the weapon.** `UN_e107_ADAN_AAFrigate` has eight + identical `AAFrigate_AAGun` turrets: `GN_GunXS_01..04` are **`0x1d`** and + `05..08` are **`0x2d`** β€” same gun, same `YawLimit 120`, different mask. + `UN_f104_TCAF_Battleship` spreads five masks over `GN_TGunL_01..05` / + `GN_TGunM_02..03`, all firing the same `CAF_Ship_ASGun`. + 🟑 **Which sector each bit denotes is NOT determined** β€” six bits and the name + *CoverArea* invite +-X/+-Y/+-Z or six hull faces, and the 01-04 / 05-08 split + looks like a side, but nothing static fixes the convention. **Not adopted.** + ❌ Refuted on the way: bits 2 and 3 are NOT a mutually-exclusive pair (188 + turrets set both). + βœ… **The 26 weapons no turret mounts** are a coherent set, not strays: 11 `_P` + player variants, the nose/twin mounts (`_NoseGun`, `_NoseGun_P`, `_None`, + `_Ttrl`, `_TwinGun`, `_TwinGun_P`), two `_Child` sub-munitions (`wep_13`, + `wep_29`), the three `S16Boss_*`, three `Weapon_Test_*`, and + `ADAN_Attacker_S_GunTurret`. + βœ… **`Versatile_NNN` ships NOWHERE** β€” 0 populated records across all 41 + archives, only the `???` template row. A sixth part kind with zero instances. + βœ… **The two units one turret short: both extras are MISSILE mounts with NO + `Frame`.** `Elan_EX4` (`TurretCount 1`, 2 records) = `Turret_NoseGun` + + `Turret_Missile`, both mask `0x01`, both frameless. `AAFrigate_EX4` + (`TurretCount 8`, 9 records) = eight `GN_GunXS_0N_ContH` guns plus one + `Ship_AAMissile` at mask `0x0c` with no `Frame`. So `TurretCount` counts + frame-mounted guns and a frameless launcher rides in the same record family + uncounted β€” **n=2, stated as the observed pattern, not a rule**. + * πŸ”΄ **(2026-08-27) BLOCKED β€” the 2D / `GP_READY_ROOM` TOC names: three routes, all closed, each with a control.** [structures/archive-naming](structures/archive-naming.md), artefact diff --git a/docs/re/data/turret-coverarea.txt b/docs/re/data/turret-coverarea.txt new file mode 100644 index 0000000..09119f1 --- /dev/null +++ b/docs/re/data/turret-coverarea.txt @@ -0,0 +1,114 @@ +# Turret CoverArea, and the turret leftovers +# Regenerate: python3 tools/re-capture/turret_coverarea.py +# See docs/re/structures/unit-substructure-records.md + +## CoverArea is a 6-BIT mask + turrets 835 ; distinct values 41 ; max 0x3f ; any bit >= 6 set: False + per-bit set count: + bit 0 709 + bit 1 117 + bit 2 274 + bit 3 274 + bit 4 260 + bit 5 237 + bit 6 0 + bit 7 0 + popcount: {0: 34, 1: 439, 2: 40, 3: 62, 4: 147, 5: 100, 6: 13} + +## it tracks the MOUNT, not the weapon + mask n weapon classes + 0x01 439 {'Gun': 95, 'Beam': 67, 'Cannon': 56, 'Laser': 41} + 0x3d 82 {'Missile': 82} + 0x00 34 {'NULL': 34} + 0x2d 28 {'ASGun': 14, 'AAGun': 12, 'Beam': 2} + 0x1d 25 {'ASGun': 14, 'AAGun': 8, 'Beam': 2, 'Laser': 1} + 0x17 13 {'AAGun': 13} + 0x1b 13 {'AAGun': 13} + 0x3f 13 {'Gun': 13} + 0x39 12 {'AAGun': 8, 'ASGun': 4} + 0x35 12 {'AAGun': 8, 'ASGun': 4} + 0x27 12 {'AAGun': 10, 'ASGun': 2} + 0x2b 12 {'AAGun': 10, 'ASGun': 2} + 0x19 10 {'AAGun': 5, 'ASGun': 4, 'Gun': 1} + 0x15 10 {'AAGun': 5, 'ASGun': 4, 'Gun': 1} + 0x14 10 {'AAGun': 6, 'ASGun': 4} + 0x18 10 {'AAGun': 6, 'ASGun': 4} + worked example -- one hull, one weapon, masks differ by mount: + UN_e107_ADAN_AAFrigate + mask 0x1d Frame=GN_GunXS_01_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame=GN_GunXS_02_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame=GN_GunXS_03_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame=GN_GunXS_04_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame=GN_GunXS_05_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame=GN_GunXS_06_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame=GN_GunXS_07_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame=GN_GunXS_08_ContH Yaw=120.0 Weapon_ADAN_AAFrigate_AAGun + mask 0x0c Frame= Yaw=20.0 Weapon_ADAN_Ship_AAMissile + UN_f104_TCAF_Battleship + mask 0x01 Frame= Yaw=3.0 Weapon_TCAF_Battleship_BowChaser + mask 0x1d Frame=GN_TGunL_01_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x2b Frame=GN_TGunM_02_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x1d Frame=GN_TGunL_02_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x27 Frame=GN_TGunM_03_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x39 Frame=GN_TGunL_03_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x17 Frame=GN_TAAGunL_01 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x39 Frame=GN_TGunL_04_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x1b Frame=GN_TAAGunL_02 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x35 Frame=GN_TGunL_05_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x17 Frame=GN_TAAGunL_03 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x35 Frame=GN_TGunL_06_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x1b Frame=GN_TAAGunL_04 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x2d Frame=GN_TGunL_07_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x27 Frame=GN_TAAGunL_05 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x2d Frame=GN_TGunL_08_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x2b Frame=GN_TAAGunL_06 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x1f Frame=GN_TGunM_01_ContH Yaw=120.0 Weapon_TCAF_Ship_ASGun + mask 0x27 Frame=GN_TAAGunL_07 Yaw=45.0 Weapon_TCAF_Ship_AAGun + mask 0x2b Frame=GN_TAAGunL_08 Yaw=45.0 Weapon_TCAF_Ship_AAGun + +## the weapons no turret mounts + Weapon.ID 131 ; mounted 105 ; never 26 + Weapon_ADAN_Attacker_S_GunTurret + Weapon_DSaber_P_wep_13_Child + Weapon_DSaber_P_wep_29_Child + Weapon_S16Boss_AAGun + Weapon_S16Boss_HBeam + Weapon_S16Boss_Laser + Weapon_TCAF_DeltaSaber_ASMissile_P + Weapon_TCAF_DeltaSaber_Beam_P + Weapon_TCAF_DeltaSaber_Bomb_P + Weapon_TCAF_DeltaSaber_Cannon_P + Weapon_TCAF_DeltaSaber_Gun_P + Weapon_TCAF_DeltaSaber_Laser_P + Weapon_TCAF_DeltaSaber_Laser_Ttrl + Weapon_TCAF_DeltaSaber_Missile_P + Weapon_TCAF_DeltaSaber_NoseGun + Weapon_TCAF_DeltaSaber_NoseGun_None + Weapon_TCAF_DeltaSaber_NoseGun_P + Weapon_TCAF_DeltaSaber_NoseGun_Ttrl + Weapon_TCAF_DeltaSaber_Rocket_P + Weapon_TCAF_DeltaSaber_Shotgun_P + Weapon_TCAF_DeltaSaber_Special_P + Weapon_TCAF_DeltaSaber_TwinGun + Weapon_TCAF_DeltaSaber_TwinGun_P + Weapon_Test_ASMissile + Weapon_Test_Gun_Player + Weapon_Test_Missile_Player + +## Versatile_NNN across all 41 archives + populated records: NONE β€” only the `???` template row + +## the two units whose turret count is one short + UN_e001_ADAN_Elan_EX4 TurretCount=1 records=2 + mask 0x01 Frame='' Weapon_ADAN_Turret_NoseGun + mask 0x01 Frame='' Weapon_ADAN_Turret_Missile + UN_e107_ADAN_AAFrigate_EX4 TurretCount=8 records=9 + mask 0x1d Frame='GN_GunXS_01_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame='GN_GunXS_02_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame='GN_GunXS_03_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x1d Frame='GN_GunXS_04_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame='GN_GunXS_05_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame='GN_GunXS_06_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame='GN_GunXS_07_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x2d Frame='GN_GunXS_08_ContH' Weapon_ADAN_AAFrigate_AAGun + mask 0x0c Frame='' Weapon_ADAN_Ship_AAMissile diff --git a/docs/re/structures/unit-substructure-records.md b/docs/re/structures/unit-substructure-records.md index 8be537d..730b95a 100644 --- a/docs/re/structures/unit-substructure-records.md +++ b/docs/re/structures/unit-substructure-records.md @@ -124,13 +124,69 @@ and wrong: the two sets **overlap in 0 values**, and the 59 slots carry only `Weapon.ID` (`Weapon_TCAF_DeltaSaber_Gun`). Two namespaces, one coincidence of count. +## βœ… `CoverArea` is a 6-bit mount mask β€” and the other three leftovers + +Artefact `../data/turret-coverarea.txt`, regenerator +`tools/re-capture/turret_coverarea.py`. + +**Six bits, never more.** Across all **835** turrets: 41 distinct values, max +`0x3f`, **bits 6 and 7 set on none**. Per-bit counts 709 / 117 / 274 / 274 / 260 +/ 237; popcounts 0…6. + +| mask | n | what | +|---|---:|---| +| `0x00` | 34 | the empty `Weapon_NULL` hardpoints β€” `YawLimit 0` on all 34 | +| `0x01` (one bit) | 439 | craft hardpoints β€” `YawLimit` 2.5 / 1.0 / 0 | +| 2–6 bits | 362 | warship mounts β€” `YawLimit` 45…180, real pitch limits | + +πŸ”‘ **It tracks the MOUNT, not the weapon.** `UN_e107_ADAN_AAFrigate` carries +eight identical `..._AAFrigate_AAGun` turrets: `GN_GunXS_01…04` are **`0x1d`** +and `GN_GunXS_05…08` are **`0x2d`** β€” same gun, same `YawLimit 120`, different +mask. `UN_f104_TCAF_Battleship` spreads five masks (`0x1d`, `0x39`, `0x35`, +`0x27`, `0x2b`) across `GN_TGunL_01…05` / `GN_TGunM_02…03`, all firing the same +`CAF_Ship_ASGun`. + +🟑 **Which physical sector each bit denotes is NOT determined.** Six bits and a +field called *CoverArea* invite Β±X/Β±Y/Β±Z or six hull faces, and the 01–04 / +05–08 split above looks like a side β€” but nothing static fixes the axis +convention. **A reading, not adopted.** ❌ Also refuted on the way: bits 2 and 3 +are not a mutually-exclusive pair β€” 188 turrets set both. + +### The 26 weapons no turret mounts + +`Weapon.ID` 131, mounted 105, never **26** β€” and they are a coherent set, not +strays: **11 `_P` player variants** (`…_Beam_P`, `_Gun_P`, `_Bomb_P`, …), +the **nose and twin mounts** (`_NoseGun`, `_NoseGun_P`, `_NoseGun_None`, +`_NoseGun_Ttrl`, `_TwinGun`, `_TwinGun_P`), **two `_Child` sub-munitions** +(`wep_13`, `wep_29`), the **three `S16Boss_*`** weapons, three `Weapon_Test_*`, +and `ADAN_Attacker_S_GunTurret`. + +### `Versatile_NNN` ships nowhere + +**0 populated records across all 41 archives** β€” only the `???` template row. +The schema declares a sixth destructible-part kind with zero instances. + +### The two units one turret short β€” both extras are MISSILE mounts with no frame + +* `UN_e001_ADAN_Elan_EX4`: `TurretCount 1`, two records β€” `Turret_000` + `…Turret_NoseGun` and `Turret_001` `…Turret_Missile`, both mask `0x01`, both + with an empty `Frame`. +* `UN_e107_ADAN_AAFrigate_EX4`: `TurretCount 8`, nine records β€” eight + `GN_GunXS_0N_ContH` gun turrets, plus one `…Ship_AAMissile` at mask `0x0c` + **with no `Frame` at all**. + +πŸ”‘ In both, the uncounted extra is a **missile launcher with no mesh node** β€” so +`TurretCount` counts frame-mounted guns, and a frameless launcher rides in the +same record family without being counted. Consistent across both, n=2, so +**stated as the observed pattern, not a rule**. + ## 🟑 Not settled * What links a roster entry to a `Weapon.ID` β€” the 59-of-131 question is still open, and this pass shows the answer is **not** the hardpoint list. * `Versatile_NNN` has 0 populated records in this pak; only the `???` template row exists, so its schema is known and its instances are not. -* `CoverArea` reads as a hex word (`0x00000000` / `0x00000001`) β€” a bit mask, - bits unknown. +* ~~`CoverArea` reads as a hex word β€” a bit mask, bits unknown.~~ **Opened + above**: a 6-bit mount mask. Which sector each bit means is still open. * The 6 `no field` comparisons are one unit whose `StructureCount` omits every count. diff --git a/tools/re-capture/turret_coverarea.py b/tools/re-capture/turret_coverarea.py new file mode 100644 index 0000000..42514f5 --- /dev/null +++ b/tools/re-capture/turret_coverarea.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""`Turret_NNN.CoverArea`, and the three other turret leftovers. + +Regenerates docs/re/data/turret-coverarea.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 + +TUR = re.compile(r'Turret_\d{3}') +VER = re.compile(r'Versatile_\d{3}') + +def units_of(pak): + out = [] + for h, b in pak_entries(pak): + if b[:4] != b'IDXD': continue + try: recs = U.parse(b) + except Exception: continue + g = [r for r in recs if r['squadron'] == 'Generic'] + if g and 'HP' in U.named(g[0]): + out.append((recs, U.named(g[0]))) + return out + +def wclass(w): + for k in ('AAGun', 'ASGun', 'Missile', 'Beam', 'Laser', 'Cannon', 'Gun', 'NULL'): + if k.lower() in w.lower(): return k + return w or '' + +def main(): + mg = glob.glob('/work/sylph_extract/**/GP_MAIN_GAME_E.pak', recursive=True)[0] + units = units_of(mg) + rows = [(gn.get('ID'), gn.get('Type'), U.named(r)) + for recs, gn in units for r in recs if TUR.fullmatch(r['squadron'])] + m = lambda n: int(n.get('CoverArea', '0x0'), 16) + + print("# Turret CoverArea, and the turret leftovers") + print("# Regenerate: python3 tools/re-capture/turret_coverarea.py") + print("# See docs/re/structures/unit-substructure-records.md") + + print("\n## CoverArea is a 6-BIT mask") + print(" turrets %d ; distinct values %d ; max 0x%02x ; any bit >= 6 set: %s" + % (len(rows), len({m(n) for _, _, n in rows}), max(m(n) for _, _, n in rows), + any(m(n) >> 6 for _, _, n in rows))) + print(" per-bit set count:") + for b in range(8): + print(" bit %d %4d" % (b, sum(1 for _, _, n in rows if m(n) >> b & 1))) + pc = collections.Counter(bin(m(n)).count('1') for _, _, n in rows) + print(" popcount: %s" % dict(sorted(pc.items()))) + + print("\n## it tracks the MOUNT, not the weapon") + agg = collections.defaultdict(collections.Counter) + for _, _, n in rows: + agg[m(n)][wclass(n.get('WeaponID', ''))] += 1 + print(" mask n weapon classes") + for k in sorted(agg, key=lambda k: -sum(agg[k].values())): + t = sum(agg[k].values()) + if t < 10: continue + print(" 0x%02x %4d %s" % (k, t, dict(agg[k].most_common(4)))) + print(" worked example -- one hull, one weapon, masks differ by mount:") + for uid in ('UN_e107_ADAN_AAFrigate', 'UN_f104_TCAF_Battleship'): + for recs, gn in units: + if gn.get('ID') != uid: continue + print(" %s" % uid) + for r in recs: + if not TUR.fullmatch(r['squadron']): continue + n = U.named(r) + print(" mask 0x%02x Frame=%-20s Yaw=%-7s %s" + % (m(n), n.get('Frame'), n.get('YawLimit'), n.get('WeaponID', ''))) + + wid, mounted, vers = set(), set(), collections.Counter() + for pk in sorted(glob.glob('/work/sylph_extract/**/*.pak', recursive=True)): + for h, b in pak_entries(pk): + 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: wid.add(i) + if VER.fullmatch(r['squadron']): vers[os.path.basename(pk)] += 1 + g = [r for r in recs if r['squadron'] == 'Generic'] + if not (g and 'HP' in U.named(g[0])): continue + for r in recs: + if TUR.fullmatch(r['squadron']): + w = U.named(r).get('WeaponID') + if w: mounted.add(w) + print("\n## the weapons no turret mounts") + print(" Weapon.ID %d ; mounted %d ; never %d" % (len(wid), len(wid & mounted), len(wid - mounted))) + for w in sorted(wid - mounted): + print(" %s" % w) + + print("\n## Versatile_NNN across all 41 archives") + print(" populated records: %s" % (dict(vers) or 'NONE β€” only the `???` template row')) + + print("\n## the two units whose turret count is one short") + for recs, gn in units: + uid = gn.get('ID') + if uid not in ('UN_e001_ADAN_Elan_EX4', 'UN_e107_ADAN_AAFrigate_EX4'): continue + sc = U.named([r for r in recs if r['squadron'] == 'StructureCount'][0]) + ts = [U.named(r) for r in recs if TUR.fullmatch(r['squadron'])] + print(" %s TurretCount=%s records=%d" % (uid, sc.get('TurretCount'), len(ts))) + for n in ts: + print(" mask 0x%02x Frame=%-20r %s" % (m(n), n.get('Frame'), n.get('WeaponID'))) + +if __name__ == "__main__": + main()