# SOTS1 ship-design rules (data-derived) What constrains a valid `ShipDesignDef`, derived from the shipped data (`Species//sections/*.shipsection` 875, `Weapons/*.weapon` 207, `Weapons/_turrets.txt` 42 rows, `Weapons/_defaultweapons.txt` 31 rows, `TechTree/MasterTechList.tech` 293 nodes) and checked against every design in three real saves (127 designs, 197 section instances, 24 player records) plus the scenario fleet templates (87 section triples). `design_rules.py` is the executable form; `test_design_rules.py --report` prints the ground-truth run. Rule ids below match `Violation.rule`. "Evidence" is what in the data or saves forces the rule; "Only code can decide" marks the residual ambiguity. ``` verify/design-rules/ design_rules.py Catalog / validate / derive_stats / applied_techs / design_from_save stock_designs.py .sav -> stock_designs.json (designs + researched techs per player) stock_designs.json 127 designs from turn1/2/3-state.sav in Design form test_design_rules.py 32 tests (unit rules, catalog self-consistency, saves, scenarios) data/_turrets.txt, data/_defaultweapons.txt, data/*_FleetTemplates.csv (copied game data) ``` ## 1. The design record (what a design *is*) From the save (`Player.designs[]` / `Player.legacyDesigns[]`, fully decoded in `stock_designs.py`): ``` Des { faiDes bool, dHide bool, dWep int, dName string, sections[5] : DSec { DSec { int species_idx, int section_id }, DGbnk2 { int n_banks, n x ( DW2 { bool bID, wid int | wfn string, int did } ) }, DOpts { int n, n x tech-name string } } } ``` * **Five section slots, three used**: slot 0 = command, 1 = mission, 2 = engine; slots 3-4 are absent in every design (reserved). An empty slot is `(species 0, section 0)`; ids in `_shipsections.txt` start at 1. * `species_idx` indexes the save's species table `Human, Hiver, Tarkas, Liir, _NPC, Zuul, Morrigi` (`SPECIES_INDEX`); `section_id` is the race's `_shipsections.txt` id. Every design in the saves uses one species for all its sections, but the record allows otherwise and NPC players do use other races' catalogs (the `Independent Colony` builds a *Tarkas* `DEDefencePlatform`). * **One weapon per bank** (`DW2` per `bank{}` block, in file order; all mounts of a bank share it). `bID true` + `wid` = player weapon id from `_weapons.txt`; `bID false` + `wfn` = weapon file path (NPC fixed banks). `did` = design id of the rider design carried by a rider weapon (0 everywhere in these saves). * `DOpts` = the techs the section instance is built with, exactly `applied_techs()`: hull-class tech, then `requires`, then the chosen option of each option group (section 5). Verified equal on all 197 instances. * `dHide` marks engine-generated hidden designs (`Default Assault Shuttle`, `Default Herald Defender`); `dWep 1` accompanies them (default-weapon fill); `faiDes` is set on the one design the player made in the designer (`Honor Lance`, turn 2) -- meaning still open. ## 2. Structure rules (A*) | id | rule | evidence | |---|---|---| | A2 | the mission slot is always filled | every design in saves and templates has a mission section; standalone hulls sit in the mission slot with command/engine empty | | A3 | mission section **with** `socket_fore`/`socket_aft` = hull ship: command **and** engine required. Mission section **without** sockets = standalone (platforms, stations' monitor, freighters, police, spy, gravboat, tricksters, node bore, wraith, devourer, construction, propaganda, terrorize): command and engine must be empty | 57-61 hull vs 16-21 standalone mission sections per race; `Light Defense Platform` = `[0,0] DEDefencePlatform [0,0]`; every 3-section design's mission section has both sockets | | A4 | section_type must match the slot (`command`/`mission`/`engine`, case-insensitive). Typeless sections (`_AssaultShuttle`, `_Drone`, `_Biomissile`, `_BoardingPod`, `_NodeMissile`, ...) only appear in the mission slot of hidden rider designs | 164+30 command, 502 mission, 108+30 engine; hidden `Default Assault Shuttle` = `[0,0] _AssaultShuttle [0,0]` | | A5 | every section comes from one species catalog (the design's race) | per-race `sections/` dirs; `DSec.species_idx`; the same stem differs per race (Human `DEFission` requires `DRV_Node`, Tarkas `DRV_Hyper`, Zuul nothing) | | A6 | all sections share `section_class` (destroyer/cruiser/dreadnought) | all 127 designs and 87 templates are class-homogeneous; DE/CR/DN name prefixes. **Only code can decide** whether the engine checks this or merely never offers a mix (socket node names are shared across classes, so geometry alone would not stop DE+CR) | | A7 | `exclude` list: a section may not be combined with a listed stem | Hiver `DERamScoop exclude {DEFission DEPulseFission DELongRangeFission}`, `CRRamscoop` likewise (the only two) -- ramscoop is a *command* section that forbids fission engines | | A8 | `explicit_command_section`/`explicit_engine_section` on a mission section pin its partners; sections flagged `explicit_section true` are usable only that way | the six `DNStation{Command,Habitat,Repair,Science,Sensor,Trade}` triples (`_Fore`/`_Aft`, `design_class station`) | | A9 | `nodesign 1` = not offered by the designer; **not** an engine rule | `DNGuard` (nodesign, "special flagship used by hungry children scenario") is a scenario fleet mission section; NPC designs use nodesign sections throughout -> reported as `warn` | **Sockets are geometry, not a compatibility key.** `socket_aft` on a command section and `socket_fore` on a mission section are mesh node names used to snap the models; the names do *not* have to match. Stock `Colonizer` = `DECommand (socket_aft CommandNode)` + `DEColonizer (socket_fore MissionNode01, socket_aft MissionNode)` + `DEFission (socket_fore EngineNode)`. Node names vary in case (`COmmandNode`, `Commandnode`, `fore`/`Fore`) and per class/race (`Fore/Aft`, `Command/Engine`, `MissionNode01/02`, `EngineNode01`). The only assembly information sockets carry is *presence* (rule A3). ## 3. Banks and weapon fit (B*) Hardpoints: `bank { turretclass C turretsize S mount { node ... azimuth/inclination } ... }`. 3,721 banks / 7,375 mounts; player-race bank population: | bank size/class | banks | sections carrying it | |---|---|---| | small standard | 1533 | almost everything | | medium standard | 975 | | | large standard | 333 | | | large beam / freebeam | 94 / 14 | *BattleBridge, *Blazer, *Barrage, *Assault, DNFlagship, DNGuard, DNWar | | large torpedo | 67 | DETorpedo, CRAssault, CRBarrage, DNAssault, DNBarrage, *TorpedoPlatform | | large dronerider | 59 | *Drone, *DronePlatform, CRMining/Refinery/RepairandSalvage, CRStrikeForceCNC, DNArmadaCNC, DNFlagship, DNSupport | | large missile / medium missile | 27 / 10 | CRBarrage, DNBarrage, *DefencePlatform, CRDronePlatform, CRTorpedoPlatform, DNBioWar, DNImpactor | | large col | 25 | CRCOL, DNCOL | | large assaultshuttlerider | 23 | DEAssaultShuttle, CRAssaultShuttle, CRAssault, DNAssault | | large projector | 23 | CRProjector, DNProjector | | large planetmissile | 20 | DNDefencePlatform, DNTorpedoPlatform, _AsteroidMonitor | | large boardingpod | 18 | CRBoarding, CRMining, CRRefinery, CRRepairandSalvage, CRStrikeForceCNC, CRTerrorize, DNArmadaCNC, DNDevourer, DNFlagship | | large missilerider | 15 | *Biowar | | small/medium strafe | 7 / 14 | DEStrafe, CRStrafe, _Drone, _DroneHeavy | | large railcannon 13, mine 12, spinal 6, siege 6, spyship 6, nodecannon 4, prisonershuttlerider 5, nodemissilerider 1, tarkahunter 1, wraith 1 | | | | id | rule | evidence | |---|---|---| | B1 | the design carries exactly one weapon entry per `bank{}` block, in file order | `DGbnk2.n == len(bank{})` for all 197 instances (Human `DEArmor` 3, `DEDefencePlatform` 5, Tarkas `DEFission` 2, `_AsteroidMonitor` 26 ...) | | B2 | no bank is left empty | every bank in every save design has a `DW2`; `_defaultweapons.txt` exists to fill banks "in the absence of tech and player information" | | B3 | a bank with `weapon ""` in data (404 NPC banks) is bound to that weapon | NPC designs store the same file in `wfn` | | B4 | **fit** = class compatibility **and** a `_turrets.txt` row for `(bank size, weapon size, class)` | see below | | B5 | weapon must exist; NPC-scope weapons (`Species/_NPC/weapons`, no manifest id) only on NPC hulls | player weapons are referenced by id, NPC ones by path | | B6 | `exclusive_species zuul` (bal_grapple, bal_disruptorwhip) | data field | | B7 | rider weapons (`compatible_section`) need that rider section in the race catalog: brd_shuttle -> `_AssaultShuttle`, brd_shuttleadv -> `_AssaultShuttle_Advanced`, brd_prisonershuttle -> `_ZuulAssaultShuttle` (Zuul only), brd_drone/col_flock -> `_Drone`/`_DroneHeavy`, brd_tarkahunter -> `_HunterShip_*` (Tarkas only); `did` names the rider design | data field; DNCarrier's `tarkahunter` bank exists only in the Tarkas catalog | | B8 | `hidden 1/true` weapons (mis_planet*, `_mis_*_warhead`, Silicoid NPC guns) are engine-only | data field | ### B4 in detail -- `_turrets.txt` `size weapon-size class health track-speed az% inc% "model"` (42 rows): ``` standard/strafe/grapple: small<-{tiny,small} medium<-{small,medium} large<-{small,medium,large} missile: medium<-medium large<-{medium,large} planetmissile: large<-medium beam freebeam spinal torpedo mine missilerider assaultshuttlerider prisonershuttlerider nodemissilerider siege projector boardingpod wraith nodecannon dronerider col spyship railcannon tarkahunter: large<-large only ``` Weapon side: every weapon has `turretsize` (tiny 4 / small 16 / medium 24 / large 82 player weapons) and `turretclass` (23 distinct). Both tables match only when compared case-insensitively (`Large`, `Missile`, `COL`). Class compatibility (`BANK_CLASS_ACCEPTS`) -- the part the turret table alone does not settle: * **standard bank accepts missile-class weapons.** Stock `Armor` puts `mis` (medium / *missile*) in `DEArmor`'s third bank, which is `medium standard` (Human and Tarkas); `DEDefencePlatform` too. The missile row `(medium, medium, missile) turret_m1missile.x` supplies the turret model, i.e. the row lookup is keyed by the *weapon's* class first. * **strafe bank accepts standard-class weapons.** No weapon is `turretclass strafe`; the strafe rows mirror the standard rows model-for-model; `_defaultweapons.txt` fills `strafe small/medium/large` with `las_red` (standard); the NPC `_HeraldDefender` strafe bank carries a standard-class beamer. Row lookup falls back to the *bank's* class. * **grapple weapons go in standard banks** (assumption). `bal_grapple` / `bal_disruptorwhip` are `turretclass grapple`, no section has a grapple bank, `_turrets.txt` and `_defaultweapons.txt` have grapple rows for medium/large. Only code can decide; the alternative (grapple banks exist in unshipped data) leaves the two Zuul weapons unmountable. * Everything else: exact class match. Every other class has its own banks and weapons, and the stock/scenario/NPC data never crosses them. Consequences the catalog self-tests confirm: every bank of every player-race section accepts at least one player weapon, and every player weapon fits some bank. PD (`tiny`) weapons fit **only** small standard/strafe banks; a large standard bank takes 54 player weapons (small, medium, large standard + missiles + grapples); missile banks take missiles only; `planetmissile` banks take the four planet missiles (`mis_defplat` is the only non-hidden one). Repeated `turretsize`/`turretclass` inside one `bank{}` (19 banks: Zuul/Human/ Morrigi DN*, DEFusion, CRDeepScan, CRBlazer...) -- the validator takes the last value; the engine's choice (first/last) is code. ## 4. Tech gating (C*) | id | rule | evidence | |---|---|---| | C1 | every `requires` of every section must be researched; a `GRP_` entry is satisfied by any researched tech with `group ` (case-insensitive: `GRP_Torps`, `GRP_HvyBeam`, `GRP_Shields` appear) | 970 refs; Human `DEFission requires DRV_Fissn DRV_Node`, both `St==4` on turn 1; groups PRJCTR/PD/HVYBEAM/SPINAL/TORPS/MINES/BIOMISSILE/JAMMING/SHIELDS | | C2 | every weapon's `requires` (AND of 1-2 techs) must be researched | 189 refs; `bal_gauss requires WEP_GsDrvr` (researched), `mis` has none | | C3 | chosen options must be researched | the Human/Tarkas turn-1 designs choose no options: none of IND_PlyAlloy... is researched; NPC players (all 293 techs) choose one per group | | C4 | **hull-class tech**: cruiser sections need `IND_CruisCon`, dreadnought sections `IND_DreadCon`; `design_class station` sections are exempt | `DOpts` carries `IND_CruisCon`/`IND_DreadCon` first for every CR/DN section (`_SwarmHive`, `_Wreckage_*`, `_VNMMoonbase`, `_CRRipper*`, `_SilicoidMother`) and omits it for `_AsteroidMonitor`/`_AsteroidMonitor2` (station). Both techs grant `TECHBEN_HULLSIZE`. Destroyers need nothing | | C5 | race availability: a tech whose every incoming `allows` edge writes `:0` can never be researched by that race, so sections/weapons requiring it are unbuildable for it | `DRV_Node` Human-only, `DRV_Hyper` Tarkas-only, `DRV_StrWrp` Liir-only; the per-race catalogs already encode this (Tarkas `DEFission requires DRV_Hyper`) | Not a gate: the tech tree's `ship { section X }` lists (149 refs, 71 techs) are the "new section unlocked" UI notice, **not** the buildability rule -- for 139 of the ~150 stems they differ from the sections' own `requires` (e.g. `IND_CruisCon` lists no sections; `DRV_Fusn` lists `DEfusion` while the section requires `DRV_Fusn` **and** a race drive tech). Starting techs (save `TechTree`, `St == 4` on turn 1): all 12 `*_ROOT`, `DRV_Fissn`, the race drive (`DRV_Node`/`DRV_Hyper`), `WEP_GsDrvr`, `WEP_RedLas`, `WEP_Nukes`, `CCC_FTLCom`, `CCC_Trns`, `XNC_Inc`, `XNC_Trns2/3` -- 22 techs. Every stock starting design validates against exactly this set. (`St` values seen: 0 locked, 1 unavailable-to-race, 2 researchable, 3 in progress, 4 researched.) Scenario fleet templates are **granted**, not designed: `HiverInvasion` gives DN designs while `HiverInvasion_Techs.csv` lacks `IND_DreadCon`. They pass the structural rules (A*, B1/B4) for every race that carries the sections. ## 5. Options (D*) `option { option A option B ... }` = a mutually exclusive group (1,226 blocks); a bare `option T` (229) is a one-member group; `optiondef { option SLD_MkOne ... SLD_GrvShld }` (12 shield sections) is the shield-level group. Signatures: | count | groups | |---|---| | 323 | armor {PlyAlloy MagLat QrkRes AdmAly StlthArm} + coating {RefCoat ImpRfCt} | | 170 | none | | 98 | {PlyAlloy} + {RefCoat} | | 47 | armor without StlthArm + coating | | 32 | {DRV_PlsmFoc} + armor + coating (fusion engines) | | 28 | armor + coating + {DRV_RecFiss} (fission engines) | | 12 | {PlyAlloy} {RefCoat} {BIO_SpndAni} (colonizers) ; 12 | {PlyAlloy} {RefCoat} {SLD_Intang} (cloaking) | | id | rule | evidence | |---|---|---| | D1 | a chosen option must be a member of one of the section's groups; at most one per group | no tech appears in two groups of one section; NPC `DOpts` hold exactly one per group | | D2 | `applied_techs = [hull-class tech] + requires + chosen options (group order)` is what the save stores | 197/197 instances match | NPC auto-designs pick the **first** member of each group (`IND_PlyAlloy`, `IND_RefCoat`; `_CRRipperCommand` has groups `{IND_MagLat}` `{IND_RefCoat}` and picks those). What the player designer picks by default, and whether a better-known option is auto-upgraded, is code. Option techs carry `option_cost` (RefCoat 1.2, ImpRfCt 1.5, PlyAlloy 1.2, MagLat 1.4, QrkRes 1.6, AdmAly 1.8, StlthArm 1.5, RecFiss 1.2, PlsmFoc 1.4, SpndAni 1.4, MkTwo 1.4, MkThree 1.8, MkFour 2.2, ErgAb/MesShld/GrvShld 2.5, Intang 2.0, HrdElec 1.5) -- read as a section-cost multiplier (assumption, section 6). ## 6. Derived stats (`derive_stats`) | stat | derivation | status | |---|---|---| | mass | sum of section `mass` | data; whether weapons add mass is code (weapons have no mass field) | | cost | sum of section `cost` x product of chosen options' `option_cost` + sum of weapon `cost` | option multiplier and per-bank vs per-mount weapon cost are **assumptions**; both weapon sums are reported | | health | per section (`ShipHealth` in the save is 3 floats) | sum reported only as a total | | crew | sum of `crew` (destroyers have none) | data | | cpoints | sum (`cpoints`, build-point/time figure) | meaning is code | | FTL | engine `ftlspeed`, `nodespeed`, `engine_techera` (fission/fusion/antimatter) | per-race semantics: Human node drive `ftlspeed 0.2 nodespeed 4`, Tarkas hyperdrive `ftlspeed 2.0 nodespeed 0`, Zuul `0.2/3` -- the strategic-speed formula is code, tuned by `TECHBEN_STRATSPD/STRATRNGE` | | tactical speed/turn | engine section `netforcelimits {force_*, torque_*, speed, rotspeed}` reported; every section has one | integrator is code | | command | command section `command_cost` (0/1), mission `command_quota` (20-76 on CnC/station sections), `maintenance_cost` | data | | capacities | `refueling_capacity repair_capacity construction_capacity mining_* prisoner_capacity colonizer_pop/infra/terra scanrange tacticalsensorrange range` and boolean roles (`refinery freighter police tradingpost monitor spy science propaganda ewar gateship ramscoop node_bore gravboat_bonus protectorate huge defence_platform`) | data, copied through | | turrets | one per mount; `_turrets.txt` row gives the turret model, health, track speed, azimuth/inclination constraint scale | data | Duplicate scalar keys in shipped sections (`mass [1800, 1000]` on Liir `DEAntimatterImpSW` and Tarkas `DEShapedHyperfieldAntimatter`; `crew` on four Zuul sections) -- last value taken; engine behaviour unknown. ## 7. Ground-truth results `python3 test_design_rules.py --report`: * 127/127 designs from `turn1/2/3-state.sav` pass A*/B*/D* (structure, bank count, weapon fit, options); 197/197 section instances reproduce `DOpts`. * Tech gating against each player's researched techs: 121/127 pass. The 6 failures are the same design three times over two players: the hidden `Default Assault Shuttle` of the two **Tarkas** players (`_AssaultShuttle` requires `DRN_AdvFrm`, unresearched on turn 1; the Human version has no `requires`). **Code rule not visible in data**: default rider designs are created for every race at game start without tech gating. The validator reports gating on `hidden` designs as `warn`. * 87 scenario template triples x every race carrying them: all structurally valid (they are class-homogeneous, command/mission/engine typed, sockets present); `DERamScoop`/`CRRamScoop` appear as command sections with `DEFusion`/`CRFusion` engines, consistent with their `exclude` lists. * Stock starting designs per race (Human and Tarkas observed): `Armor` (DECommand + DEArmor + DEFission, gauss everywhere, `mis` in the medium bank), `Tanker` (DETanker), `Colonizer` (DEColonizer), `Extended Range` (DEExtendedRange), `Light Defense Platform` (DEDefencePlatform alone, 4x gauss + `mis`), hidden `Default Assault Shuttle`. The `Independent Colony` NPC has a Tarkas `Defence Platform` with `las_red` x4 + `mis`. ## 8. Open questions for the Ghidra side 1. **Class-equality (A6)** -- enforced in `ShipDesignDef` validation, or only by the designer never offering a mix? Look for a compare of `section_class` across the three slots. 2. **Turret-class compatibility (B4)** -- confirm the accept table `standard <- {standard, missile, grapple}`, `strafe <- {standard}` and the row-lookup order (weapon class first, bank class second). The grapple case is the weakest. Also whether `tiny` really is small-only. 3. **Hull-class tech (C4)** -- confirm `IND_CruisCon`/`IND_DreadCon` are injected from `section_class`, the `design_class station` exemption, and whether the check happens at design time or build time. 4. **Hidden default designs** -- where the engine builds `Default Assault Shuttle`/`Default Herald Defender` and why `requires` is skipped (probably: designs are only validated when the player saves one; hidden designs are constructed directly). 5. **Option semantics** -- default pick per group, auto-upgrade when a better option is researched, and what `option_cost` multiplies (section cost? whole design? also build time?). 6. **Cost/mass/speed formulas** -- weapon cost per bank or per mount; whether weapons/turrets add mass; how `netforcelimits` + mass become tactical speed; how `ftlspeed`/`nodespeed` + `TECHBEN_STRATSPD` become strategic speed for each drive family. 7. **Missiles and `WEP_Nukes`** -- `mis.weapon` has no `requires`, yet every race starts with `WEP_Nukes`; is missile availability gated in code? 8. **`faiDes`, `dWep`** -- meaning of the two design flags (`faiDes` true on the player-made `Honor Lance`; `dWep 1` on hidden defaults). 9. **Empty banks (B2)** -- can a design be saved with an unfilled bank, or does the designer always fill from `_defaultweapons.txt`? 10. **Duplicate-key resolution** -- first or last value for repeated `mass`/`crew`/`turretsize` keys (affects two engine sections' mass). 11. **Mixed-species designs** -- the record allows a different species per slot; does anything forbid it for players?