sots-re/verify/design-rules/SHIP_DESIGN_RULES.md
alex a4aba6a9fb lane D2: the ship-design catalogue -- how designs persist, hull size, and the 0x400 flag
Blocker #4 on lane Y's path to a byte-matching turn.

HOW DESIGNS PERSIST, correcting a published finding. Game::ShipDesign::Write is
0x008325e0 and makes four stream calls. The recorded address 0x008747a0 is in
NO vftable at all (lane V2's inversion), so "ShipDesign::Write makes no stream
call" was a misattribution, not a fact about the class. Game::ShipDesign
derives from Game::ShipDesignDef and inherits IStreamable second, so its writer
is reached through an adjustor thunk -- which is what the slot-indexed
serializer sweep found instead. A design persists as two serializers, base and
derived: ShipDesignDef::Write 0x00827390 emits FAIDes/DHide/DWep/DName then
exactly three DSec frames (+0x4c command, +0x24 mission, +0x74 engine),
ShipDesign::Write appends Dtc, the Dwgv flag and a conditional Dwg frame.

THREE sections, not five. The campaign's "slots 3-4 reserved and always empty"
was save_reader.py's trailing Rest("sections") sweeping Dtc and Dwgv into the
section list, and stock_designs.py decoding them as two empty sections -- rule 8
in its exact form, reader and consumer agreeing with each other and both wrong.
Two independent enumerations say three: the writer's straight-line body, and the
ctor's eh_vector_constructor_iterator(this+0x24, 0x28, 3) closing at
0x9c = sizeof(ShipDesignDef).

DWep and Dwgv are BOOLs, not ints -- byte-indistinguishable from ints at a
four-character tag, the same class of defect as ObservedTech.odet.

THE 0x400 FLAG IS `defence_platform`, read off the .shipsection parser's own bit
setter at 0x005749b7. NOT lane B5's 0x400: that one is a fleet flag, on the wire
as FtFlg. The full role-flag table is in the finding. HULL SIZE is section_class
through a three-name stricmp table (Destroyer/Cruiser/Dreadnought -> 0/1/2),
absent or unrecognised meaning 0 with a log line rather than an error. Both
words are recomputed from the data files by ShipDesign::UpdateDerivedStats
0x0087e7c0 and neither is on the wire. Corroborated by the default hull-health
table the same bit picks: 500/3000/15000 without it, 100/500/1000 with.

MEASURED: the census rebuilt from each save's own state matches the record the
game archived, 480 leaves / 0 mismatched over 11 saves and 503 designs, computed
independently in Python and in C++. COVERAGE REPORTED AS LOUDLY: only 32 of the
480 leaves are nonzero, and three of the six census leaves (both cruiser rows,
dreadnought platforms) are unexercised by every save in the corpus.

Closed 0 / regressed 0 against the standalone's divergence list, reported
separately: the census leaves live in src/app's turn record, which lane A2 holds
this cycle, so this lane evaluated and reported rather than writing.

Oracles fixed openly (rule 12): save_reader.py's Des shape, 49/49 with three
corrected tests and one added that pins "exactly three DSec" against real saves;
stock_designs.json regenerated, whose diff is only raw_slots 5->3 and dWep
int->bool across all 127 designs with every other field identical;
test_design_rules.py still 32/32 with the same ground truth.

19 addresses in ghidra/addresses.d/lane-d2.json, no collision; the generated
header was validated to a scratch path, never written in place.
2026-09-08 12:45:36 -04:00

335 lines
24 KiB
Markdown

# SOTS1 ship-design rules (data-derived)
> **Tag-name note (2026-09-07):** where this document quotes save fields by R1's spellings (`faiDes`, `dHide`, `dWep`, `dName`), the on-disk tags are `FAIDes`, `DHide`, `DWep`, `DName` (byte-confirmed); the Python reader keeps the R1 keys as typed-dict keys for compatibility. See `verify/save-reader/SAVE_FORMAT.md` §10.
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 bool, dName string,
sections[3] : 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 } },
dtc int, dwgv bool, [ Dwg {...} only when dwgv ] }
```
> **CORRECTION (lane D2, 2026-09-08).** This section used to read `sections[5]` and
> "five section slots, three used; slots 3-4 are absent in every design
> (reserved)". **There are three sections and no reserved slots.** The two
> extra entries were `Dtc` and `Dwgv` — the derived writer's own tail — swept
> into the section list by `save_reader.py`'s trailing `Rest("sections")` and
> then decoded by `stock_designs.py` as two empty sections. Both writers are
> now recovered: `Game::ShipDesignDef::Write 0x00827390` emits the four scalars
> and exactly three `DSec` frames, `Game::ShipDesign::Write 0x008325e0` appends
> `Dtc`, `Dwgv` and the conditional `Dwg`. The constructor confirms the count
> independently (`eh_vector_constructor_iterator(this+0x24, stride 0x28,
> count 3)`, and `0x24 + 3*0x28 = 0x9c = sizeof(ShipDesignDef)`). `dWep` is a
> **bool** on the wire, not an int — byte-indistinguishable, writer-decidable.
> See `findings/objects/ship-design-catalogue.md`.
* **Three section slots**: disk order 0 = command, 1 = mission, 2 = engine. An
empty slot is `(species 0, section 0)`; ids in `_shipsections.txt` start at 1.
(In memory the array is ordered mission, command, engine — offset order is not
write order here, and the memory order is what decides hull size.)
* `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 | {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`.
## 7A. Hull size and the class flag (lane D2, code-settled)
Two derived words the design caches, **neither on the wire**, both rebuilt from
this catalog by `ShipDesign::UpdateDerivedStats 0x0087e7c0` whenever a design
changes or is loaded. They are what the turn record's per-hull-class ship
census counts by. Full derivation in
`findings/objects/ship-design-catalogue.md`.
| word | rule |
|---|---|
| **hull size** (`ShipDesign+0x12c`) | the `section_class` of the **last resolved section in memory slot order** (engine, else command, else mission), mapped `Destroyer 0 / Cruiser 1 / Dreadnought 2` case-insensitively. Absent or unrecognised -> **0**, with a log line, not an error — settles the 16 shipped sections with no `section_class`. Assignment, not max: A6 makes first-wins and last-wins agree on all 503 design records in the 11 saves |
| **role flags** (`ShipDesign+0xb8`/`+0xbc`, 64-bit) | the **OR** of every resolved section's own flag word. One bit per boolean role key; `defence_platform` is `0x400` of the low dword. The OR is read from the instruction and is untestable on shipped data — no design mixes a flagged with an unflagged section |
The same `0x400` bit also picks the design's default hull health when no section
overrides it: without it `500 / 3000 / 15000` by hull size, with it
`100 / 500 / 1000`. So the flag separates *platforms* from *ships*, which is
exactly how the census uses it (`shpt` vs `satt`).
This also answers §8 question 1 in part: **A6 is not enforced by this path.**
The aggregator assigns the class from whichever section it visits last and never
compares the three, so a mixed-class design would be silently classified by its
engine section rather than rejected here. Whether the *designer UI* rejects it
is still open.
## 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?