sots-re/findings/subsystems/data-parsers.md

326 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SOTS1 data-format parsers — report
Round-two deliverable: stdlib-only Python 3 readers for every textual data
format shipped in `sots.gob` / `sots_local_en.gob`, proven against all 1,665
extracted files, with cross-link checks and normalized JSON artifacts.
Location: `spicy:/bulk-storage/re-lab/handoff/parsers/` (this file is its sibling).
Input: `spicy:/bulk-storage/re-lab/gob-extract/`. Python: 3.x, no third-party deps.
```
python3 parsers/verify.py <gob-extract-dir> <out-dir> # re-runs everything, prints the appendix below
```
## Headline
- **1,595 / 1,595 data files parse (100%)**; the 70 skipped files are HLSL
shaders (`.fx/.fxh`) and prose (`Desc<Race>.txt`, `ChatTrans.txt`) — not data.
- 12 shipsections are **syntactically broken** in the shipped archive (11 never
close their outer `{`, one has an extra `}`). They load in-game, so the engine's
parser is lenient in exactly two ways: EOF closes all open blocks, and a stray
top-level `}` is ignored. `mars_data` mirrors that by default and records a
warning; `strict=True` rejects them.
- Cross-links are clean where it matters: **0 dangling** weapon→tech,
section→tech, tech→section, tech→weapon-file, tech→tech, option→tech,
bank→weapon-file, allows→tech, AI/scenario CSV→catalog references.
- Real inconsistencies found (data bugs / engine-behaviour evidence): keys and
identifiers are matched **case-insensitively** everywhere (25 refs only
resolve that way), 10 `_shipsections.txt` ids point at files that do not exist,
3 NPC weapon name tokens are missing from `Strings.csv`, 4 `Strings.csv` keys
are duplicated with a trailing space, and the three `aitech*.csv` AI tables are
empty templates.
## The readers
### `mars_data.py` — the Mars brace-block format
`parse(text, typed=True, keep_case=False, strict=False, warnings=None)` /
`parse_file(path)`. Tokenizer-based (not line-based), so it handles every
layout the files use. Grammar as observed:
```
body := (NAME '{' body '}' | NAME value | "bare quoted item")*
value := "quoted" | bareword comment := // to end of line
```
Returns plain dicts. Repeated keys become lists (`get_list(d, k)` normalizes);
bare quoted strings inside a block go under `"_items"`; the file's top level is
itself a body. Barewords that look like C numbers (`.5`, `-.8`, `7e+8`) or
`true/false` are typed; quoted strings never are (`numplayers "8"` stays a str).
Used for: `.weapon` (207), `.shipsection` (875), `.tech` (1), `.combat` (3),
`.def` (2), `.script` (4), and the 24 block-form `.txt` (scenarios, tutorial,
credits, systemnames, skydefs, ctechvars, shipai).
Quirks handled (all present in shipped files):
| quirk | where | handling |
|---|---|---|
| keys case-insensitive: `Requires`/`requires`, `badge`/`Badge`, `Planet_Impact_Effect` | tech, defs, weapons | keys lower-cased by default |
| identifier values case-insensitive: `WEP_HCLAS` vs tech `WEP_HCLas`, `Ind_AstMon`, `SECTIONNAME_DEHammerHead` vs file `DEHammerhead`, manifest `DEWar.SHIPSECTION` | 18 weapon + 7 section `requires`, string keys, manifests | all cross-links compare `.lower()` |
| block opens on the same line as a pair: `turretsize small mount {` | `_NPC/_Wreckage_E.shipsection` | tokenizer, not line-based |
| block name and brace on one line: `weapon {` | MasterTechList.tech (67×) | same |
| outer block never closed (EOF at depth 1) | 11 shipsections (Hiver 6, Liir 4, Morrigi 1) | lenient close + warning |
| one `}` too many | `Human/CrPropaganda.shipsection` | ignored + warning |
| `//` inside quoted strings; backslash Windows paths; `""` empty values | globals.txt, GUI scripts | no escape processing, comment scan is quote-aware |
| bare quoted list items with no key | `Data/Strategy/systemnames.txt` | `_items` list |
| scalar and block under the same key: `option DRV_PlsmFoc` beside `option { option A ... }` | 153 shipsections (229 occurrences) | both merge into one list; consumers accept str-or-dict members |
| repeated `turretsize`/`turretclass` inside one `bank{}` | 19 banks (Zuul DN*, …) | kept as list; verify takes last |
| mixed CRLF/LF, cp1252 bytes | everywhere | decode cp1252 |
No `/* */` comments, no `=`, no escapes, no BOMs were found.
### `flat_kv.py` — `KEY value` tuning tables and positional rows
`parse_kv(text, typed=True, on_dup='last')` → `{KEY: value}`; a value of
several unquoted tokens becomes a list; `duplicates(text)` lists repeated keys
(none in shipped files). `color("r g b")` → tuple. `parse_rows(text)` → list of
token rows for the positional tables (`_turrets.txt`, `_defaultweapons.txt`,
`damfx*.txt`, `playercolors.txt`, `BadgeTable.txt`, `AvatarTable.txt`,
`WeaponIconPlacements.txt`). Comment stripping is quote-aware
(`ENDGAME_FILL_COLOR "0 0 0" // 48 29 2"` is a real line).
### `manifest.py` — id manifests and CSVs
`parse_manifest(text)` → `Manifest(entries=[(id, filename)], deleted=[ids],
problems=[…])`; validates id uniqueness, honours `// DELETED - n`, and
`by_name()` folds case. `parse_csv(text)` drops `#`/`//` comment rows and
blank rows, strips cells, honours RFC-4180 quoting (`Strings.csv` has one
multi-line cell and quoted commas; `stock_diplomacy_messages.csv` quotes every
cell including its `"# species"` header). `parse_csv_with_header()` recovers a
`# <a>,<b>` schema row when present.
### `effect_txt.py` — `Effects/*.effect`
**Not brace-block**, contrary to the round-one write-up. A `TXT` magic line,
then `KEY value` lines and `KEY` + indented `BEGIN … END` groups. Order is
semantic (`PARTICLEDATATYPE n` precedes the `CREATION/VARIATION/OVERLIFE`
curves that belong to it; `MODIFIER` repeats once per type), so each level is
returned as an ordered `[key, value]` list; `to_dict()` gives the unordered view.
415/415 parse; BEGIN/END always balanced.
### `verify.py`
Parses every file with the reader its type calls for, computes key-frequency
schema stats per block path, runs the cross-link suite, writes the artifacts,
and prints the appendix below. Exit code 0 = no parse failures.
## Cross-link results (summary; full detail in appendix and `crosslink.json`)
| link | refs | dangling | notes |
|---|---|---|---|
| weapon `requires` → tech | 189 (177 weapons; 12 list 2+ techs = AND) | 0 | 18 case-only mismatches; 13 player weapons have no `requires` (missiles, mirv warheads, boarding shuttles, spyship, wraith) |
| shipsection `requires` → tech | 970 | 0 | 7 case-only (`Ind_AstMon`, `DRN_BTLRdrs`) |
| shipsection `option`/`optiondef` → tech | 3,842 | 0 | |
| tech `ship{section}` → shipsection | 149 | 0 | section names are race-agnostic; matched against the union of race catalogs |
| tech `weapon{filename}` → file | 67 | 0 | |
| tech `requires` → tech or `GRP_x` | 154 | 0 | 9 groups (`group TORPS` ↔ `requires GRP_TORPS`): PRJCTR 4, PD 3, HVYBEAM 3, SPINAL 2, TORPS 12, MINES 7, BIOMISSILE 5, JAMMING 3, SHIELDS 6 |
| tech `allows` edges | 354 | 0 | every edge parses to `child RP:n [Race:%…]`; 12 roots (`*_ROOT`) are never a target |
| `_weapons.txt` ↔ `Weapons/*.weapon` | 123 ids, deleted 36/58/59 | 0 either way | `Species/_NPC/weapons/` (84) has no manifest — referenced by path from NPC `bank{weapon}` (404 refs, 0 dangling) |
| `_shipsections.txt` ↔ files | 885 ids | **10 ids with no file** | Tarkas ids 3,4,16,32,43,108,109 (`CRAbsorber, CRAIC, CRDeflector, DEAbsorber, DEDeflector, DEDisruptor, CRDisruptor`); `DEWar` in Human 98, Liir 86, Morrigi 39. These are reserved wire ids, consistent with the "never reuse ids" header. |
| `TECHNAME_/TECHDESC_` | 293 | 0 / 0 | |
| `SECTIONNAME_/SECTIONDESC_` (267 stems) | | 0 / 67 | 62 of the missing DESC are `_NPC`-only, 5 are hidden player stems (`_assaultshuttle`, `_biomissile`, `_boardingpod`, `_nodemissile`, `_spy`) |
| every `@TOKEN` in brace files | 378 | **3** | `@WEAPON_NPC_REFUGEEDRONE`, `@WEAPON_SILQUEEN_CANNON`, `@WEAPON_VNSYSK_CUTTINGBEAM` (NPC weapons; would render as raw token) |
| `_turrets.txt` vs weapon/bank (size,class) | 42 rows | 0 / 0 | only when compared case-insensitively (`Large`, `Missile`, `Standard`, `COL`, `PlanetMissile` appear) |
| `_defaultweapons.txt` → weapon file | 31 | 0 | |
| AI `affinity_section/raider_sections` → section, `weapon_replacements` → weapon, `affinity_weapon` → weaponfamily | 200 / – / 6 / 9 | 0 | |
| `aitechpri/aitechgrp/aitechmode.csv` | **0 rows** | – | comment-only templates: AI tech priorities are code-owned |
| Scenario `*Techs.csv` → tech, `*FleetTemplates.csv` → section | 127 / 87 | 0 | |
## Schema facts worth knowing (from `schema_stats.json`)
- `weapon`: 207 instances, 60 distinct scalar keys; behaviour sub-block is one
of `bolt`(66, always with `rangetable`), `beam`(52), `torpedo`(19, with
`rangetable`), `rider`(17), `missile`(13), `chainlightning`(10), `col`(9),
`mine`(7), `disintegrator`(3), `grapple`, `projectedshield`, `mirv`(2 each),
`nodecannon`, `siege`, `mesonprojector`, `spyship`, `wraith`(1 each). Always
present: `name weaponclass turretsize turretclass burst_volleys recharge_time`.
- `shipsection`: 875 instances, 87 distinct keys; always `model health mass`;
`bank`(3,721) → `mount`(7,375); `thruster`(1,105); `netforcelimits`(875);
`option`(1,226 block instances); `anim`(247). `section_type` values are
case-mixed (`engine`/`Engine`, `command`/`Command`) and 41 sections have none
(riders, NPC hulls); `section_class` has 16 with none.
- `tech`: 293 nodes; keys `name`(293) `allows`(354) `threat`(169) `family`(155)
`requires`(154) `group`(45) `type`(39, always `P`) `option_cost`(18)
`unlock_explicitly`(6); sub-blocks `ship`(71) `weapon`(67) `strategy`(61).
`family` is written on only 155 nodes — `tech_tree.json` adds `family_inferred`
from the name prefix.
- `Strings.csv`: 5,200 data rows → 5,196 keys (4 duplicated via trailing
space, identical text). Key prefixes: AIDIP 755, EVENTMSG 415, TECHNAME/TECHDESC
293 each, SECTIONNAME 283, WEAPON 139, TECHBEN 25 …
## Artifacts (`parsers/out/`)
| file | size | content |
|---|---|---|
| `tech_tree.json` | 217 KB | 293 nodes (name, display name/desc, family, family_inferred, type, threat, group, option_cost, requires, TECHBEN inc/dec, sections, weapons, allows) + 354 edges `{from, to, rp, pct{Race:%}}` + groups |
| `weapons.json` | 345 KB | 207 weapons: full parsed body + `stem, file, scope(player/NPC), id, display_name` |
| `shipsections.json` | 2.7 MB | 875 sections: full body + `race, stem, id, display_name, description, unlocked_by[]` |
| `strings.json` | 396 KB | 5,196 token → text |
| `schema_stats.json` | 33 KB | per block path: instance count, key frequencies, sub-block frequencies |
| `crosslink.json` | 27 KB | machine-readable form of the cross-link findings |
| `tech_tree.dot` | 54 KB | Graphviz digraph, nodes coloured by family, edges labelled RP + race % (no graphviz on this box; `dot -Tsvg` elsewhere) |
| `report.md` | | verbatim `verify.py` output (appended below) |
Caveat on `edges[].pct`: a race absent from an `allows` string has no override
in the data. The default (believed 100%) is engine code, not asserted by the
artifact.
---
# Appendix — `verify.py` output
## Parse results
| reader | file kind | files | parsed | failed |
|---|---|---|---|---|
| mars_data | brace:combat | 3 | 3 | 0 |
| mars_data | brace:def | 2 | 2 | 0 |
| mars_data | brace:script | 4 | 4 | 0 |
| mars_data | brace:shipsection | 875 | 875 | 0 |
| mars_data | brace:tech | 1 | 1 | 0 |
| mars_data | brace:txt | 24 | 24 | 0 |
| mars_data | brace:weapon | 207 | 207 | 0 |
| manifest.parse_csv | csv | 28 | 28 | 0 |
| effect_txt | effect | 415 | 415 | 0 |
| flat_kv.parse_kv | kv | 20 | 20 | 0 |
| manifest.parse_manifest | manifest | 8 | 8 | 0 |
| flat_kv.parse_rows | rows | 8 | 8 | 0 |
Total: 1595 parsed, 0 failed (skipped: 70 HLSL/prose files that are not data).
Lenient recoveries (engine-compatible; strict=True would reject these):
- `Species/Morrigi/sections/DECommand.shipsection`: end of file inside block (depth 1)
- `Species/Liir/sections/CRAbsorber.shipsection`: end of file inside block (depth 1)
- `Species/Liir/sections/CRCommand.shipsection`: end of file inside block (depth 1)
- `Species/Liir/sections/CRFireControl.shipsection`: end of file inside block (depth 1)
- `Species/Liir/sections/CRShield.shipsection`: end of file inside block (depth 1)
- `Species/Human/sections/CrPropaganda.shipsection`: line 105: stray '}' at top level
- `Species/Hiver/sections/DEAntiMatter.shipsection`: end of file inside block (depth 1)
- `Species/Hiver/sections/DEDeflector.shipsection`: end of file inside block (depth 1)
- `Species/Hiver/sections/DEDisruptor.shipsection`: end of file inside block (depth 1)
- `Species/Hiver/sections/DEJammer.shipsection`: end of file inside block (depth 1)
- `Species/Hiver/sections/DEShield.shipsection`: end of file inside block (depth 1)
- `Species/Hiver/sections/DESquadronCnc.shipsection`: end of file inside block (depth 1)
## Schema stats (key frequency per block type)
Full table in `schema_stats.json`. Block paths with instance counts and the
keys seen in them (count = number of block instances carrying the key):
### `weapon.weapon` (207 instances)
keys: name:207, weaponclass:207, turretsize:207, turretclass:207, burst_volleys:207, recharge_time:207, cost:204, icon_file:204, icon_rect:204, muzzle_sound:203, muzzle_sound_minrange:200, muzzle_effect:190, requires:189, dam_est:179, volley_period:176, rating_frate:176, rating_dam:176, rating_acc:176, rating_range:176, fc_requires_los:173, fc_requires_inrange:173, fc_requires_enemycolony:173, fc_manual_target:173, fc_manual_toggle:173, fc_manual_launch:173, fc_controllable:173, fc_holdsfire:173, range:167, range_planet:167, hpbonus:152, model1:143, muzzle_speed:140, weaponfamily:130, volley_duration:107, solution_tolerance:89, turretmodel1:67, trackspeed_mod:55, model2:43, pinpoint:41, buildup_delay:40, blindfire:37, weapondamagetype:21, munitionsize:15, model3:14, fc_explicit_target:14, fc_exclusive_launch:14, fc_targets_expire:14, turretmodel2:14, target_queue_size:13, compatible_section:10, hidden:9, lock_period:4, buildup_sound:3, buildup_sound_minrange:3, exclusive_species:2, secondary_pd:2, turretmodel3:2, buildup_effect:1, centered_muzzle_effect:1, anim:1
sub-blocks: bolt:66, beam:52, torpedo:19, rider:17, missile:13, chainlightning:10, col:9, mine:7, disintegrator:3, grapple:2, projectedshield:2, mirv:2, nodecannon:1, siege:1, mesonprojector:1, spyship:1, wraith:1
### `shipsection.shipsection` (875 instances)
keys: requires:970, mass:877, model:875, health:875, section_class:859, section_type:834, cost:831, cpoints:824, crew:644, preview_ofs:549, socket_aft:543, socket_fore:487, option:229, entity_class:189, range:182, engine_sound:178, autonomous:178, ftlspeed:168, dam_model:154, engine_idle_sound:133, design_class:126, engine_sound_minrange:125, engine_techera:115, command_cost:111, maintenance_cost:98, nodespeed:75, node_cannon_fling:68, dam_socket_aft:68, dam_socket_fore:67, section_sound:65, section_sound_repeat:65, nodesign:63, section_lod_type:60, view_dist:60, explicit_section:60, defence_platform:49, death_effect:39, tacticalsensorrange:38, scanrange:35, command_quota:34, station:33, death_area_impact_effect:30, death_dam:30, death_damradius:30, explicit_command_section:30, explicit_engine_section:30, refueling_capacity:21, split_traffic_volume:20, repair_capacity:18, aicontrol:17, rebelai_scanrange:17, rebelai_tacticalsensorrange:17, colonizer_pop:12, colonizer_infra:12, colonizer_terra:12, refinery:12, freighter:10, monitor:8, shield_model_offset:7, construction_capacity:7, huge:6, mining_capacity:6, mining_rate:6, spytender:6, propaganda:6, ewar:6, science:6, spy:6, exclude:6, freighterq:5, police:5, tradingpost:5, prisoner_capacity:4, node_bore:3, section_sound_minrange:3, gravboat_bonus:3, gateship:2, ramscoop:2, construction_devourer:1, construction_refresh:1, construction_salvagerate:1, mining_trap:1, colony_trap:1, imperial_population:1, civilian_population:1, protectorate:1, node_missile:1
sub-blocks: bank:3721, option:1226, thruster:1105, netforcelimits:875, anim:247, optiondef:12
### `tech.tech` (293 instances)
keys: allows:354, name:293, threat:169, family:155, requires:154, group:45, type:39, option_cost:18, unlock_explicitly:6
sub-blocks: ship:71, weapon:67, strategy:61
All block paths: `combat`(3), `combat.combatenv`(3), `combat.endcondition`(3), `combat.orbit`(17), `combat.planet`(1), `combat.summary`(3), `def`(2), `def.badge`(27), `def.light`(196), `script`(4), `script.distanceline`(1), `script.fleetline`(1), `script.gateline`(1), `script.hiver`(1), `script.hiver.dead`(1), `script.hiver.good`(1), `script.hiver.ideal`(1), `script.hiver.ideal_2`(1), `script.hiver.poor`(1), `script.human`(1), `script.human.dead`(1), `script.human.good`(1), `script.human.ideal`(1), `script.human.ideal_2`(1), `script.human.poor`(1), `script.liir`(1), `script.liir.dead`(1), `script.liir.good`(1), `script.liir.ideal`(1), `script.liir.ideal_2`(1), `script.liir.poor`(1), `script.morrigi`(1), `script.morrigi.dead`(1), `script.morrigi.good`(1), `script.morrigi.ideal`(1), `script.morrigi.ideal_2`(1), `script.morrigi.poor`(1), `script.moveline`(1), `script.nodeline`(1), `script.sprite`(11), `script.tarkas`(1), `script.tarkas.dead`(1), `script.tarkas.good`(1), `script.tarkas.ideal`(1), `script.tarkas.ideal_2`(1), `script.tarkas.poor`(1), `script.techcolors`(1), `script.techtreebackground`(1), `script.techtreemodelinfo`(12), `script.tradeline`(1), `script.zuul`(1), `script.zuul.dead`(1), `script.zuul.good`(1), `script.zuul.ideal`(1), `script.zuul.ideal_2`(1), `script.zuul.poor`(1), `shipsection`(875), `shipsection.shipsection`(875), `shipsection.shipsection.anim`(247), `shipsection.shipsection.bank`(3721), `shipsection.shipsection.bank.mount`(7375), `shipsection.shipsection.netforcelimits`(875), `shipsection.shipsection.option`(1226), `shipsection.shipsection.optiondef`(12), `shipsection.shipsection.thruster`(1105), `tech`(1), `tech.tech`(293), `tech.tech.ship`(71), `tech.tech.strategy`(61), `tech.tech.weapon`(67), `weapon`(207), `weapon.weapon`(207), `weapon.weapon.beam`(52), `weapon.weapon.bolt`(66), `weapon.weapon.bolt.rangetable`(66), `weapon.weapon.chainlightning`(10), `weapon.weapon.col`(9), `weapon.weapon.disintegrator`(3), `weapon.weapon.grapple`(2), `weapon.weapon.mesonprojector`(1), `weapon.weapon.mine`(7), `weapon.weapon.mirv`(2), `weapon.weapon.missile`(13), `weapon.weapon.nodecannon`(1), `weapon.weapon.projectedshield`(2), `weapon.weapon.rider`(17), `weapon.weapon.siege`(1), `weapon.weapon.spyship`(1), `weapon.weapon.torpedo`(19), `weapon.weapon.torpedo.rangetable`(19), `weapon.weapon.wraith`(1)
## Cross-link results
- weapon `requires` -> tech: 189 refs in 177 weapons (12 weapons list 2+ techs), 0 dangling, 18 case-mismatched; 30 weapons have no `requires` (NPC: 17, player: ['_mis_mirv_warhead.weapon', '_mis_planet_mirv_warhead.weapon', 'bal_grapple.weapon', 'brd_prisonershuttle.weapon', 'brd_shuttle.weapon', 'brd_tarkahunter.weapon', 'mis.weapon', 'mis_defplat.weapon', 'mis_planet.weapon', 'mis_planet_hvy.weapon', 'mis_planet_mirv.weapon', 'spyship.weapon', 'wraith.weapon']).
- case: `Weapons/bem_beamer_uv.weapon` requires `WEP_UVBmr` (tech is `WEP_UvBmr`)
- case: `Weapons/bem_part.weapon` requires `WEP_prtBm` (tech is `WEP_PrtBm`)
- case: `Weapons/bem_part_spinal.weapon` requires `WEP_prtBm` (tech is `WEP_PrtBm`)
- case: `Weapons/can_plasma.weapon` requires `WEP_plsmcan` (tech is `WEP_PlsmCan`)
- case: `Weapons/hvy_bem_hclas.weapon` requires `WEP_HCLAS` (tech is `WEP_HCLas`)
- case: `Weapons/hvy_bem_hclas_free.weapon` requires `WEP_HCLAS` (tech is `WEP_HCLas`)
- case: `Weapons/hvy_bem_lancer.weapon` requires `WEP_lancer` (tech is `WEP_Lancer`)
- case: `Weapons/hvy_bem_lancer_free.weapon` requires `WEP_lancer` (tech is `WEP_Lancer`)
- case: `Weapons/las_green.weapon` requires `WEP_grnlas` (tech is `WEP_GrnLas`)
- case: `Weapons/las_uv.weapon` requires `WEP_UVlas` (tech is `WEP_UvLas`)
- case: `Weapons/las_xray.weapon` requires `WEP_xrylas` (tech is `WEP_XryLas`)
- case: `Weapons/trp_plasma.weapon` requires `WEP_plsmtrp` (tech is `WEP_PlsmTrp`)
- case: `Species/_NPC/weapons/Herald_bem_beamer_uv.weapon` requires `WEP_UVBmr` (tech is `WEP_UvBmr`)
- case: `Species/_NPC/weapons/Refugee_sml.weapon` requires `WEP_UVBmr` (tech is `WEP_UvBmr`)
- case: `Species/_NPC/weapons/SiliciodQueenCannon.weapon` requires `WEP_plsmcan` (tech is `WEP_PlsmCan`)
- case: `Species/_NPC/weapons/SwarmerCannon.weapon` requires `WEP_plsmcan` (tech is `WEP_PlsmCan`)
- case: `Species/_NPC/weapons/Wreckage_bem_part.weapon` requires `WEP_prtBm` (tech is `WEP_PrtBm`)
- case: `Species/_NPC/weapons/Wreckage_las_xray.weapon` requires `WEP_xrylas` (tech is `WEP_XryLas`)
- shipsection `requires` -> tech: 970 refs, 0 dangling, 7 case-mismatched.
- case: `Species/Zuul/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- case: `Species/Tarkas/sections/DNCarrier.shipsection` requires `DRN_BTLRdrs`
- case: `Species/Tarkas/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- case: `Species/Morrigi/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- case: `Species/Liir/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- case: `Species/Human/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- case: `Species/Hiver/sections/_AsteroidMonitor.shipsection` requires `Ind_AstMon`
- shipsection `option{option T}`/`optiondef` -> tech: 3842 refs, 0 dangling. Two forms coexist: `option { option A option B }` (a mutually-exclusive choice group) and a bare section-level `option T` (229 occurrences in 153 files, e.g. `option DRV_PlsmFoc` on engine sections) -- both merge under the key `option`, so consumers must accept str-or-dict list members.
- tech `ship{section}` -> shipsection: 149 refs, 0 dangling (matched against the union of all race catalogs, case-insensitive).
- tech `weapon{filename}` -> file: 67 refs, 0 dangling.
- tech `requires` -> tech/GRP_: 154 refs, 0 dangling. Groups: {'PRJCTR': 4, 'PD': 3, 'HVYBEAM': 3, 'SPINAL': 2, 'TORPS': 12, 'MINES': 7, 'BIOMISSILE': 5, 'JAMMING': 3, 'SHIELDS': 6}.
- tech `allows` edges: 354, 0 point at unknown techs, 0 unparsable.
- techs never allowed by anything (roots/orphans): 12: IND_ROOT, EWP_ROOT, SLD_Root, NRG_Root, TRP_ROOT, WHD_ROOT, BAL_ROOT, DRV_ROOT, BIO_ROOT, CCC_ROOT, DRN_ROOT, XNC_ROOT
- duplicate tech names: none
- id manifests <-> files:
- `Weapons`: 123 ids (deleted [36, 58, 59]); listed-but-no-file 0; file-but-unlisted 0
- `Human`: 145 ids (deleted none); listed-but-no-file 1; file-but-unlisted 0
- MISSING FILE for id 98: `dewar.shipsection`
- `Zuul`: 122 ids (deleted none); listed-but-no-file 0; file-but-unlisted 0
- `Hiver`: 137 ids (deleted none); listed-but-no-file 0; file-but-unlisted 0
- `Tarkas`: 139 ids (deleted none); listed-but-no-file 7; file-but-unlisted 0
- MISSING FILE for id 3: `crabsorber.shipsection`
- MISSING FILE for id 4: `craic.shipsection`
- MISSING FILE for id 16: `crdeflector.shipsection`
- MISSING FILE for id 109: `crdisruptor.shipsection`
- MISSING FILE for id 32: `deabsorber.shipsection`
- MISSING FILE for id 43: `dedeflector.shipsection`
- MISSING FILE for id 108: `dedisruptor.shipsection`
- `Liir`: 136 ids (deleted none); listed-but-no-file 1; file-but-unlisted 0
- MISSING FILE for id 86: `dewar.shipsection`
- `Morrigi`: 142 ids (deleted none); listed-but-no-file 1; file-but-unlisted 0
- MISSING FILE for id 39: `dewar.shipsection`
- `_NPC`: 64 ids (deleted none); listed-but-no-file 0; file-but-unlisted 0
- `Species/_NPC/weapons/*.weapon` (84 files) have no manifest at all; they are referenced by filename from `_NPC` shipsection `bank{weapon}` lines.
- localization:
- `Strings.csv`: 5200 data rows -> 5196 keys. 4 keys occur twice because one copy carries a trailing space (parse_csv strips cells; the later row wins):
- `SECTIONNAME_CRGravboat`: 'Gravboat' then 'Gravboat'
- `SECTIONNAME_CRFusion_Carver`: 'Fusion Void Carver' then 'Fusion Void Carver'
- `SECTIONNAME_CRAntimatter_Carver`: 'AM Void Carver' then 'AM Void Carver'
- `SECTIONNAME_CRAntimatter_Mastery`: 'AM Void Mastery' then 'AM Void Mastery'
- TECHNAME_/TECHDESC_ for 293 techs: 0 / 0 missing. [] []
- SECTIONNAME_/SECTIONDESC_ for 267 distinct section stems: 0 / 67 missing.
- missing SECTIONNAME_: 0 are `_NPC`-only stems (never shown in the design UI); player-race stems: 0 []
- missing SECTIONDESC_: 62 are `_NPC`-only stems (never shown in the design UI); player-race stems: 5 ['_assaultshuttle', '_biomissile', '_boardingpod', '_nodemissile', '_spy']
- weapon `name @TOKEN`: 3 unresolved of 207; 0 weapons carry no `name`.
- UNRESOLVED `Species/_NPC/weapons/Drone_shuttle.weapon` name `@WEAPON_NPC_REFUGEEDRONE`
- UNRESOLVED `Species/_NPC/weapons/SiliciodQueenCannon.weapon` name `@WEAPON_SILQUEEN_CANNON`
- UNRESOLVED `Species/_NPC/weapons/VNSYSK_Beam.weapon` name `@WEAPON_VNSYSK_CUTTINGBEAM`
- all `@TOKEN` refs in brace-block files: 378 refs, 3 unresolved.
- UNRESOLVED `Species/_NPC/weapons/Drone_shuttle.weapon` `@WEAPON_NPC_REFUGEEDRONE`
- UNRESOLVED `Species/_NPC/weapons/SiliciodQueenCannon.weapon` `@WEAPON_SILQUEEN_CANNON`
- UNRESOLVED `Species/_NPC/weapons/VNSYSK_Beam.weapon` `@WEAPON_VNSYSK_CUTTINGBEAM`
- `_turrets.txt` (42 rows; size/class values compared case-insensitively -- the data mixes `Large`/`large`, `Missile`/`missile`, `Standard`/`standard`):
- weapon (turretsize,turretclass) pairs with no turret row: none
- section bank (turretsize,turretclass) pairs with no turret row: none
- banks with no turretsize at all (NPC fixed-weapon banks): 2; banks that repeat turretsize/turretclass inside one bank{} (last value taken): 19
- shipsection `bank{weapon <file>}` -> file: 404 refs, 0 dangling.
- `_defaultweapons.txt`: 31 rows, 0 name a missing weapon file.
- `Data/Strategy/AI/aitechpri.csv`: 0 data rows -- the shipped file is a comment-only template (schema documented in its header, no entries); the AI's tech priorities must therefore come from code.
- `Data/Strategy/AI/aitechgrp.csv`: 0 data rows -- the shipped file is a comment-only template (schema documented in its header, no entries); the AI's tech priorities must therefore come from code.
- `Data/Strategy/AI/aitechmode.csv`: 0 data rows -- the shipped file is a comment-only template (schema documented in its header, no entries); the AI's tech priorities must therefore come from code.
- `AI/affinity_section.csv`: 200 rows; unknown sections: none
- `AI/raider_sections.csv`: unknown sections: none
- `AI/weapon_replacements.csv`: 6 rows; unknown weapon stems: none
- `AI/affinity_weapon.csv`: families ['conventionalbeam', 'conventionalmine', 'emitter', 'energycannon', 'gauss', 'heavybeam', 'laser', 'missile', 'torpedo']; not a weaponfamily in any .weapon: none. weaponfamily values in data: {'energycannon': 19, 'heavybeam': 7, 'missile': 14, 'gauss': 27, 'laser': 12, 'conventionalbeam': 21, 'emitter': 9, 'conventionalmine': 6, 'torpedo': 15}
- `Scenarios/HiverInvasion_FleetTemplates.csv`: 81 rows; unknown sections: none
- `Scenarios/HiverInvasion_Techs.csv`: 67 rows; unknown techs: none
- `Scenarios/UpstartApes_EmpireTechs.csv`: 60 rows; unknown techs: none
- `Scenarios/UpstartApes_GiftFleetTemplates.csv`: 6 rows; unknown sections: none
## Artifacts
- `tech_tree.json` (225 KB)
- `weapons.json` (345 KB)
- `shipsections.json` (2752 KB)
- `strings.json` (396 KB)
- `schema_stats.json` (33 KB)
- `crosslink.json` (24 KB)
- `tech_tree.dot` (54 KB)
- tech_tree.json: 293 nodes, 354 edges; weapons.json: 207; shipsections.json: 875; strings.json: 5196 keys