sots-re/verify/design-rules/SHIP_DESIGN_RULES.md

21 KiB

SOTS1 ship-design rules (data-derived)

What constrains a valid ShipDesignDef, derived from the shipped data (Species/<Race>/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 "<file>" 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_<name> entry is satisfied by any researched tech with group <name> (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 <Race>: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<Race>, XNC_Inc<Race>, XNC_Trns<Race>2/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
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?