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.txtstart at 1. species_idxindexes the save's species tableHuman, Hiver, Tarkas, Liir, _NPC, Zuul, Morrigi(SPECIES_INDEX);section_idis the race's_shipsections.txtid. 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 (theIndependent Colonybuilds a TarkasDEDefencePlatform).- One weapon per bank (
DW2perbank{}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, exactlyapplied_techs(): hull-class tech, thenrequires, then the chosen option of each option group (section 5). Verified equal on all 197 instances.dHidemarks engine-generated hidden designs (Default Assault Shuttle,Default Herald Defender);dWep 1accompanies them (default-weapon fill);faiDesis 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
Armorputsmis(medium / missile) inDEArmor's third bank, which ismedium standard(Human and Tarkas);DEDefencePlatformtoo. The missile row(medium, medium, missile) turret_m1missile.xsupplies 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.txtfillsstrafe small/medium/largewithlas_red(standard); the NPC_HeraldDefenderstrafe 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_disruptorwhipareturretclass grapple, no section has a grapple bank,_turrets.txtand_defaultweapons.txthave 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;
planetmissilebanks take the four planet missiles (mis_defplatis 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.savpass A*/B*/D* (structure, bank count, weapon fit, options); 197/197 section instances reproduceDOpts. - 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 Shuttleof the two Tarkas players (_AssaultShuttlerequiresDRN_AdvFrm, unresearched on turn 1; the Human version has norequires). Code rule not visible in data: default rider designs are created for every race at game start without tech gating. The validator reports gating onhiddendesigns aswarn. - 87 scenario template triples x every race carrying them: all structurally
valid (they are class-homogeneous, command/mission/engine typed, sockets
present);
DERamScoop/CRRamScoopappear as command sections withDEFusion/CRFusionengines, consistent with theirexcludelists. - Stock starting designs per race (Human and Tarkas observed):
Armor(DECommand + DEArmor + DEFission, gauss everywhere,misin the medium bank),Tanker(DETanker),Colonizer(DEColonizer),Extended Range(DEExtendedRange),Light Defense Platform(DEDefencePlatform alone, 4x gauss +mis), hiddenDefault Assault Shuttle. TheIndependent ColonyNPC has a TarkasDefence Platformwithlas_redx4 +mis.
8. Open questions for the Ghidra side
- Class-equality (A6) -- enforced in
ShipDesignDefvalidation, or only by the designer never offering a mix? Look for a compare ofsection_classacross the three slots. - 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 whethertinyreally is small-only. - Hull-class tech (C4) -- confirm
IND_CruisCon/IND_DreadConare injected fromsection_class, thedesign_class stationexemption, and whether the check happens at design time or build time. - Hidden default designs -- where the engine builds
Default Assault Shuttle/Default Herald Defenderand whyrequiresis skipped (probably: designs are only validated when the player saves one; hidden designs are constructed directly). - Option semantics -- default pick per group, auto-upgrade when a better
option is researched, and what
option_costmultiplies (section cost? whole design? also build time?). - Cost/mass/speed formulas -- weapon cost per bank or per mount; whether
weapons/turrets add mass; how
netforcelimits+ mass become tactical speed; howftlspeed/nodespeed+TECHBEN_STRATSPDbecome strategic speed for each drive family. - Missiles and
WEP_Nukes--mis.weaponhas norequires, yet every race starts withWEP_Nukes; is missile availability gated in code? faiDes,dWep-- meaning of the two design flags (faiDestrue on the player-madeHonor Lance;dWep 1on hidden defaults).- Empty banks (B2) -- can a design be saved with an unfilled bank, or does
the designer always fill from
_defaultweapons.txt? - Duplicate-key resolution -- first or last value for repeated
mass/crew/turretsizekeys (affects two engine sections' mass). - Mixed-species designs -- the record allows a different species per slot; does anything forbid it for players?