verify: mars data parsers proven 100% on 1595 files; catalogs; tech-tree graph

This commit is contained in:
alex 2026-09-07 15:22:55 -04:00
parent 9378cec2fa
commit 7139dee219
20 changed files with 154269 additions and 2 deletions

View file

@ -23,7 +23,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| class hierarchy + key vftables | meta | mapped | high | 100% | 2026-09-07 | findings/objects/ghidra-recon.md - RTTI Base_Class_Array read directly; vftables for 8 core classes |
| data-model (.gob data files) | subsystem | mapped | high | 100% | 2026-09-07 | findings/subsystems/data-model.md - tech/weapons/sections/races/AI all data-driven |
| string / config intel | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/strings-and-config.md |
| Mars brace-block parser | subsystem | in-progress | — | 0% | 2026-09-07 | R2: python parsers + 100% parse proof + cross-links -> verify/parsers/ |
| Mars brace-block parser | subsystem | verified | high | 100% | 2026-09-07 | verify/parsers/ (mars_data, flat_kv, manifest, effect_txt, verify.py): 1595/1595 files parse, 0 dangling cross-links; catalogs in verify/results/data-catalogs/ |
| networking (SNM/FNM + GameSpy) | subsystem | backlog | — | 0% | 2026-09-07 | ~60 SNM strategy msgs, FNM file-xfer/host-migration; lockstep sim |
| battle-load: thread contention | subsystem | backlog | — | 0% | 2026-09-07 | root cause hunt: CreateThread sites FUN_00902350, FUN_00736e30, FUN_008a0e50; streaming-sound thread; lockstep sync waits |
| UI screen & flow map | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/ui-screen-map.md - 36 screens; screens are C++ on Mars controls (NOT data); turn state machine recovered |

View file

@ -16,3 +16,5 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
- **Save-struct contradictions (R1 vs R2)** — field widths (`Abdn`/`Dstyd`/`ltis` Int16 vs Int32; `Bats2`), R2's `OID = PID*16` owner-handle claim, species id 4 (`_NPC` vs 'AI Rebellion'). Resolve against the binary's Streamable read code. (from [[save-editor-structs]])
- **Unlabeled save blocks** — `CdPlayer` (unknown1..35), empty `SimSystemDetailSpy`, opaque ~2500 B RNG blob. Analyst targets once the Streamable readers are located. (from [[save-editor-structs]])
- **Missing HUD scripts** — exe references `GUI/Combat/CombatHUD.script`, `SensorHUD.script`, `NoHUD.script` but none ship in the gobs or loose; likely dev-only overrides via the gobio native-FS fallback. Confirm via `CombatScreen` load path. Also: `.script` files are display configs, not widget layouts (corrects round-one note). (from [[ui-screen-map]])
- **Tech `allows` default per-race %** — `tech_tree.json` edges only carry races written in the `allows` string; the default for an unlisted race (believed 100%) is engine code. Ghidra target in the tech loader. (from [[data-parsers]])
- **Engine parser leniency** — 12 shipped shipsections are syntactically broken (unclosed `{`, extra `}`) yet load; keys and identifiers are case-insensitive. Reimplementation must match this leniency. `.effect` is its own `TXT`/`BEGIN-END` format, not brace-block (corrects round one). (from [[data-parsers]])

View file

@ -0,0 +1,326 @@
# 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

View file

@ -1 +0,0 @@
# placeholder — populated during the campaign

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,98 @@
"""effect_txt.py -- reader for Effects/*.effect (particle-effect definitions).
NOT the brace-block format. Layout:
TXT # magic first line
KEY value # scalar (number, TRUE/FALSE, "quoted")
KEY # group: KEY on its own line, then
BEGIN
...nested KEY value / groups...
END
Order matters: 'PARTICLEDATATYPE n' is followed by the CREATION /
VARIATION / OVERLIFE curves that belong to that datatype, and 'MODIFIER'
repeats once per type. So each level is returned as an ordered list of
[key, value] pairs (value = scalar or nested list). to_dict() gives a
dict view (repeats -> lists) when order is not needed.
Quirks handled: one file has CRLF; 'NAME "New Emitter"' values contain
spaces; indentation is cosmetic (tabs); the format is line-based.
Stdlib only.
"""
from __future__ import annotations
from typing import Any
from flat_kv import split_tokens, strip_comment
from mars_data import coerce
__all__ = ["parse", "parse_file", "to_dict", "EffectSyntaxError"]
Pairs = list # list[[key, value]]
class EffectSyntaxError(ValueError):
pass
def parse(text: str, *, typed: bool = True) -> Pairs:
lines = text.splitlines()
if not lines or lines[0].strip() != "TXT":
raise EffectSyntaxError("missing TXT magic")
stack: list[Pairs] = [[]]
pending_key: str | None = None
for lineno, raw in enumerate(lines[1:], 2):
line = strip_comment(raw).strip()
if not line:
continue
if line == "BEGIN":
if pending_key is None:
raise EffectSyntaxError(f"line {lineno}: BEGIN without a key")
grp: Pairs = []
stack[-1].append([pending_key, grp])
stack.append(grp)
pending_key = None
continue
if line == "END":
if len(stack) == 1:
raise EffectSyntaxError(f"line {lineno}: END without BEGIN")
stack.pop()
continue
if pending_key is not None:
raise EffectSyntaxError(f"line {lineno}: key {pending_key!r} not followed by BEGIN")
toks = split_tokens(line)
key = toks[0][0]
if len(toks) == 1:
pending_key = key
continue
vals = [coerce(t) if (typed and not q) else t for t, q in toks[1:]]
stack[-1].append([key, vals[0] if len(vals) == 1 else vals])
if len(stack) != 1:
raise EffectSyntaxError(f"{len(stack) - 1} unclosed BEGIN group(s)")
if pending_key is not None:
raise EffectSyntaxError(f"trailing key {pending_key!r} without BEGIN")
return stack[0]
def to_dict(pairs: Pairs) -> dict:
d: dict = {}
for key, val in pairs:
if isinstance(val, list) and val and isinstance(val[0], list) and len(val[0]) == 2 and isinstance(val[0][0], str):
val = to_dict(val)
if key in d:
if not isinstance(d[key], list) or not getattr(d[key], "_rep", False):
d[key] = _Rep([d[key]])
d[key].append(val)
else:
d[key] = val
return d
class _Rep(list):
_rep = True
def parse_file(path, **kw) -> Pairs:
with open(path, "rb") as f:
return parse(f.read().decode("cp1252"), **kw)

127
verify/parsers/flat_kv.py Normal file
View file

@ -0,0 +1,127 @@
"""flat_kv.py -- readers for the flat 'KEY value' tuning tables and the
whitespace-positional tables under Data/, Weapons/, Badges/, Avatars/, GUI/.
Two shapes exist:
parse_kv(text) KEY value -> {KEY: value}
one constant per line; value is a bareword or a
"quoted string"; '//' comments; colors are quoted
"r g b" (use color()). Files: Data/globals.txt,
Data/species.txt, Data/Strategy/StrategyVars.txt,
Data/Combat/*.txt (most), Data/encounters.txt, ...
parse_rows(text) tok tok tok ... -> [[tok, ...], ...]
one record per line, whitespace separated, quoted
tokens may contain spaces; '//' comments. Files:
Weapons/_turrets.txt, Weapons/_defaultweapons.txt,
Data/Combat/damfx*.txt, Data/Strategy/playercolors.txt,
Badges/BadgeTable.txt, Avatars/AvatarTable.txt,
GUI/WeaponIconPlacements.txt
Quirks handled: '//' inside a quoted value is not a comment; a quoted
value may be empty (""); keys repeat in a few files (kept as list);
duplicate-key detection is exposed via parse_kv(..., on_dup=).
Stdlib only.
"""
from __future__ import annotations
import re
from typing import Any
from mars_data import coerce
__all__ = ["parse_kv", "parse_rows", "duplicates", "color", "strip_comment",
"split_tokens", "parse_kv_file", "parse_rows_file"]
_TOK_RE = re.compile(r'"([^"]*)"|(\S+)')
def strip_comment(line: str) -> str:
"""Remove a trailing // comment, ignoring // inside double quotes."""
in_q = False
i = 0
n = len(line)
while i < n:
c = line[i]
if c == '"':
in_q = not in_q
elif c == "/" and not in_q and line.startswith("//", i):
return line[:i]
i += 1
return line
def split_tokens(line: str) -> list[tuple[str, bool]]:
"""Split a line into (token, was_quoted) pairs."""
out = []
for m in _TOK_RE.finditer(line):
if m.group(1) is not None:
out.append((m.group(1), True))
else:
out.append((m.group(2), False))
return out
def parse_rows(text: str, *, typed: bool = True) -> list[list[Any]]:
rows = []
for raw in text.splitlines():
line = strip_comment(raw).strip()
if not line:
continue
toks = split_tokens(line)
rows.append([coerce(t) if (typed and not q) else t for t, q in toks])
return rows
def _pairs(text: str, typed: bool):
for lineno, raw in enumerate(text.splitlines(), 1):
line = strip_comment(raw).strip()
if not line:
continue
toks = split_tokens(line)
key = toks[0][0]
vals = [coerce(t) if (typed and not q) else t for t, q in toks[1:]]
val: Any = None if not vals else (vals[0] if len(vals) == 1 else vals)
yield lineno, key, val
def parse_kv(text: str, *, typed: bool = True, on_dup: str = "last") -> dict:
"""KEY value per line -> dict. A value made of several unquoted tokens
is kept as a list. on_dup: 'last' (later line wins), 'first', 'error'.
Use duplicates() to find repeated keys."""
d: dict = {}
for lineno, key, val in _pairs(text, typed):
if key in d:
if on_dup == "error":
raise ValueError(f"line {lineno}: duplicate key {key}")
if on_dup == "first":
continue
d[key] = val
return d
def duplicates(text: str) -> dict[str, list[int]]:
"""key -> line numbers, for keys that appear more than once."""
seen: dict[str, list[int]] = {}
for lineno, key, _ in _pairs(text, False):
seen.setdefault(key, []).append(lineno)
return {k: v for k, v in seen.items() if len(v) > 1}
def color(value: str) -> tuple:
"""'r g b' or 'r g b a' -> tuple of numbers."""
return tuple(coerce(t) for t in value.split())
def _read(path) -> str:
with open(path, "rb") as f:
return f.read().decode("cp1252")
def parse_kv_file(path, **kw) -> dict:
return parse_kv(_read(path), **kw)
def parse_rows_file(path, **kw) -> list:
return parse_rows(_read(path), **kw)

121
verify/parsers/manifest.py Normal file
View file

@ -0,0 +1,121 @@
"""manifest.py -- the numbered id manifests and the '#'-commented CSVs.
parse_manifest(text) -> Manifest
Weapons/_weapons.txt and Species/<Race>/sections/_shipsections.txt:
<int-id> <filename> one per line
// DELETED - <id> retired id (still reserved)
Ids are the persistent network / savegame ids. Filenames are matched
case-insensitively (the shipped manifests have 'DEWar.SHIPSECTION',
'CRAIC.Shipsection' etc. against lower-case files -- Windows FS).
parse_csv(text) -> list[list[str]]
Rows with '#' or '//' as first non-blank char are comments; blank rows
dropped; RFC-4180 quoting honoured (Strings.csv has one multi-line cell
and quoted commas). Header rows that start with '#' (aitechpri.csv,
"# species" in stock_diplomacy_messages.csv) are returned separately
via parse_csv_with_header().
Stdlib only.
"""
from __future__ import annotations
import csv
import io
import re
from dataclasses import dataclass, field
__all__ = ["Manifest", "parse_manifest", "parse_manifest_file",
"parse_csv", "parse_csv_file", "parse_csv_with_header", "read_text"]
_DELETED_RE = re.compile(r"//\s*DELETED\s*-\s*(\d+)", re.I)
_ENTRY_RE = re.compile(r"^\s*(\d+)\s+(\S+)\s*$")
@dataclass
class Manifest:
entries: list[tuple[int, str]] = field(default_factory=list) # (id, filename)
deleted: list[int] = field(default_factory=list)
problems: list[str] = field(default_factory=list)
def by_id(self) -> dict[int, str]:
return dict(self.entries)
def by_name(self) -> dict[str, int]:
"""lower-cased filename -> id"""
return {n.lower(): i for i, n in self.entries}
def parse_manifest(text: str) -> Manifest:
m = Manifest()
seen: dict[int, int] = {}
for lineno, raw in enumerate(text.splitlines(), 1):
line = raw.strip()
if not line:
continue
d = _DELETED_RE.search(line)
if d:
m.deleted.append(int(d.group(1)))
continue
if line.startswith("//"):
continue
e = _ENTRY_RE.match(line)
if not e:
m.problems.append(f"line {lineno}: unrecognised {line!r}")
continue
i, name = int(e.group(1)), e.group(2)
if i in seen:
m.problems.append(f"line {lineno}: duplicate id {i} (first at line {seen[i]})")
seen[i] = lineno
m.entries.append((i, name))
for i in m.deleted:
if i in seen:
m.problems.append(f"id {i} is both DELETED and assigned")
return m
def read_text(path) -> str:
with open(path, "rb") as f:
return f.read().decode("cp1252")
def parse_manifest_file(path) -> Manifest:
return parse_manifest(read_text(path))
def _is_comment(row: list[str]) -> bool:
if not row:
return True
first = row[0].lstrip()
if first.startswith("#") or first.startswith("//"):
return True
return all(c.strip() == "" for c in row)
def parse_csv(text: str, *, strip: bool = True) -> list[list[str]]:
rows = []
for row in csv.reader(io.StringIO(text, newline="")):
if _is_comment(row):
continue
rows.append([c.strip() for c in row] if strip else row)
return rows
def parse_csv_with_header(text: str) -> tuple[list[str] | None, list[list[str]]]:
"""Return (header, rows). Header = the first '#'-prefixed row that
contains a comma (e.g. '# <tech>,<human-pri>,...'), with the '#' and
any '<>' stripped; None when there is no such row."""
header = None
for row in csv.reader(io.StringIO(text, newline="")):
if not row:
continue
first = row[0].lstrip()
if first.startswith("#") and len(row) > 1:
header = [c.strip().lstrip("#").strip().strip("<>") for c in row]
break
if not _is_comment(row):
break
return header, parse_csv(text)
def parse_csv_file(path, **kw):
return parse_csv(read_text(path), **kw)

211
verify/parsers/mars_data.py Normal file
View file

@ -0,0 +1,211 @@
"""mars_data.py -- reader for the Mars engine's brace-block key/value format.
Covers: *.weapon, *.shipsection, *.tech, *.combat, *.def, *.script and the
block-form *.txt files (scenarios, tutorial, credits, systemnames, skydefs,
ctechvars, shipai).
Grammar (as observed in every shipped SOTS1 file):
body := (block | pair | item)*
block := NAME '{' body '}'
pair := NAME value
item := QUOTED # bare quoted string inside a block
value := QUOTED | BAREWORD
comment := '//' .* EOL
A file's top level is itself a body (scenario .txt files mix top-level pairs
and player{} blocks; catalog files hold one or many named blocks).
Result shape: plain dicts. A key seen once maps to its value; a key seen
more than once maps to a list (use get_list() when you want a list always).
Bare quoted items are collected under the key "_items".
Quirks handled (all seen in the real data, see parsers-report.md):
* keys are case-insensitive to the engine ("Requires"/"requires",
"badge"/"Badge") -> keys are lower-cased unless keep_case=True
* a block may open on the same line as a preceding pair
("turretsize small mount {") and a block name may sit on the same line
as its brace ("weapon {")
* backslashes inside quoted strings are literal (Windows paths); there is
no escape syntax
* '//' inside a quoted string is not a comment
* CRLF and LF line endings, cp1252 bytes (decoded losslessly)
* numbers use C float syntax: ".5", "-.8", "7e+8"
Stdlib only.
"""
from __future__ import annotations
import re
from typing import Any, Iterator
__all__ = ["parse", "parse_file", "coerce", "get_list", "MarsSyntaxError"]
class MarsSyntaxError(ValueError):
pass
# --- tokenizer -------------------------------------------------------------
_TOKEN_RE = re.compile(
r"""
(?P<ws>\s+)
| (?P<comment>//[^\n]*)
| (?P<open>\{)
| (?P<close>\})
| (?P<quoted>"[^"]*")
| (?P<bad_quote>")
| (?P<bare>[^\s{}"]+)
""",
re.VERBOSE,
)
def _tokenize(text: str) -> Iterator[tuple[str, str, int]]:
"""Yield (kind, value, line). kind in {open, close, quoted, bare}."""
line = 1
pos = 0
n = len(text)
while pos < n:
m = _TOKEN_RE.match(text, pos)
if m is None: # pragma: no cover - regex is exhaustive
raise MarsSyntaxError(f"line {line}: cannot tokenize {text[pos:pos+20]!r}")
kind = m.lastgroup
tok = m.group()
pos = m.end()
if kind == "ws":
line += tok.count("\n")
continue
if kind == "comment":
continue
if kind == "bad_quote":
raise MarsSyntaxError(f"line {line}: unterminated string")
if kind == "quoted":
yield kind, tok[1:-1], line
line += tok.count("\n")
else:
yield kind, tok, line
# --- parser ----------------------------------------------------------------
_INT_RE = re.compile(r"[+-]?\d+$")
_FLOAT_RE = re.compile(r"[+-]?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?$")
def coerce(tok: str) -> Any:
"""Bareword -> int / float / bool when it looks like one, else str."""
if _INT_RE.match(tok):
return int(tok)
if _FLOAT_RE.match(tok):
return float(tok)
low = tok.lower()
if low == "true":
return True
if low == "false":
return False
return tok
def _add(d: dict, key: str, value: Any) -> None:
if key in d:
cur = d[key]
if isinstance(cur, list):
cur.append(value)
else:
d[key] = [cur, value]
else:
d[key] = value
def get_list(d: dict, key: str) -> list:
"""Always return a list for a key (missing -> [], single -> [x])."""
v = d.get(key)
if v is None:
return []
return v if isinstance(v, list) else [v]
class _Parser:
def __init__(self, text: str, typed: bool, keep_case: bool, strict: bool, warnings: list | None):
self.toks = list(_tokenize(text))
self.i = 0
self.typed = typed
self.keep_case = keep_case
self.strict = strict
self.warnings = warnings if warnings is not None else []
def _warn(self, msg: str) -> None:
if self.strict:
raise MarsSyntaxError(msg)
self.warnings.append(msg)
def _peek(self):
return self.toks[self.i] if self.i < len(self.toks) else None
def _next(self):
t = self.toks[self.i]
self.i += 1
return t
def _key(self, name: str) -> str:
return name if self.keep_case else name.lower()
def body(self, depth: int) -> dict:
d: dict = {}
while True:
t = self._peek()
if t is None:
if depth:
# 11 shipped shipsections never close their outer block;
# the engine treats EOF as closing every open block.
self._warn(f"end of file inside block (depth {depth})")
return d
kind, val, line = t
if kind == "close":
self._next()
if not depth:
# CrPropaganda.shipsection has one '}' too many.
self._warn(f"line {line}: stray '}}' at top level")
continue
return d
if kind == "open":
raise MarsSyntaxError(f"line {line}: '{{' without a block name")
self._next()
if kind == "quoted":
# bare string item (systemnames.txt lists) -- never a key
_add(d, "_items", val)
continue
nxt = self._peek()
if nxt is None or nxt[0] == "close":
# lone bareword at end of block: treat as flag item
_add(d, "_items", val)
continue
if nxt[0] == "open":
self._next()
_add(d, self._key(val), self.body(depth + 1))
continue
nkind, nval, _ = self._next()
if nkind == "bare" and self.typed:
nval = coerce(nval)
_add(d, self._key(val), nval)
def parse(text: str, *, typed: bool = True, keep_case: bool = False,
strict: bool = False, warnings: list | None = None) -> dict:
"""Parse brace-block text into nested dicts.
typed -- convert bareword numbers/bools (quoted strings stay str)
keep_case -- keep key case instead of lower-casing
strict -- raise on unbalanced braces instead of recovering the way
the engine does (EOF closes open blocks, stray top-level
'}' ignored); pass warnings=[] to collect the recoveries
"""
return _Parser(text, typed, keep_case, strict, warnings).body(0)
def parse_file(path, **kw) -> dict:
with open(path, "rb") as f:
raw = f.read()
return parse(raw.decode("cp1252"), **kw)

634
verify/parsers/verify.py Normal file
View file

@ -0,0 +1,634 @@
"""verify.py -- parse every shipped SOTS1 data file, cross-link the catalogs,
and emit normalized JSON artifacts.
usage: python3 verify.py <gob-extract-dir> <out-dir>
Prints a markdown report to stdout; writes to <out-dir>:
tech_tree.json weapons.json shipsections.json strings.json
schema_stats.json crosslink.json tech_tree.dot
"""
from __future__ import annotations
import collections
import json
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import effect_txt
import flat_kv
import manifest
import mars_data
from mars_data import get_list
RACES = ["Human", "Zuul", "Hiver", "Tarkas", "Liir", "Morrigi"]
PLAYABLE = RACES
ALL_RACE_DIRS = RACES + ["_NPC"]
BRACE_TXT = {"Data/tutorial.txt", "Data/credits.txt", "Data/Strategy/systemnames.txt",
"Data/Combat/ctechvars.txt", "Data/Combat/shipai.txt",
"Models/Skysphere/skydefs.txt", "Models/Skysphere/NodeSpace-skydefs.txt"}
ROWS_TXT = {"Weapons/_turrets.txt", "Weapons/_defaultweapons.txt", "Data/Combat/damfx.txt",
"Data/Combat/damfx_levels.txt", "Data/Strategy/playercolors.txt",
"Badges/BadgeTable.txt", "Avatars/AvatarTable.txt", "GUI/WeaponIconPlacements.txt"}
PROSE = {"Locale/EN/ChatTrans.txt"}
def kind_of(rel: str) -> str:
ext = rel.rsplit(".", 1)[-1].lower()
base = os.path.basename(rel)
if ext in ("weapon", "shipsection", "tech", "combat", "def", "script"):
return "brace:" + ext
if ext == "effect":
return "effect"
if ext == "csv":
return "csv"
if ext in ("fx", "fxh"):
return "hlsl"
if ext == "txt":
if base in ("_weapons.txt", "_shipsections.txt"):
return "manifest"
if rel.startswith("Scenarios/") or rel in BRACE_TXT:
return "brace:txt"
if rel in ROWS_TXT:
return "rows"
if rel.startswith("Locale/EN/Desc") or rel in PROSE:
return "prose"
return "kv"
return "other"
def walk(root):
for dp, _, fn in os.walk(root):
for f in sorted(fn):
p = os.path.join(dp, f)
yield p, os.path.relpath(p, root).replace(os.sep, "/")
# --- schema stats ------------------------------------------------------------
def schema_walk(node, path, stats):
"""Count key occurrences per block path, and which keys are blocks."""
st = stats.setdefault(path, {"blocks": 0, "keys": collections.Counter(), "sub": collections.Counter()})
st["blocks"] += 1
for k, v in node.items():
vals = v if isinstance(v, list) else [v]
for x in vals:
if isinstance(x, dict):
st["sub"][k] += 1
schema_walk(x, path + "." + k, stats)
else:
st["keys"][k] += 1
# --- tech tree ---------------------------------------------------------------
_RP_RE = re.compile(r"^RP:(\d+)$", re.I)
_PCT_RE = re.compile(r"^(\w+):(\d+)$")
def parse_allows(s: str):
toks = s.split()
child = toks[0]
rp = None
pct = {}
extra = []
for t in toks[1:]:
m = _RP_RE.match(t)
if m:
rp = int(m.group(1))
continue
m = _PCT_RE.match(t)
if m and m.group(1) in RACES:
pct[m.group(1)] = int(m.group(2))
continue
extra.append(t)
return child, rp, pct, extra
def main(root: str, out: str) -> int:
os.makedirs(out, exist_ok=True)
rep = []
P = rep.append
# ---- 1. parse everything ---------------------------------------------
ok = collections.Counter()
fail = collections.Counter()
fails = []
warns = []
parsed = {} # rel -> object
for p, rel in walk(root):
k = kind_of(rel)
try:
if k.startswith("brace"):
w = []
obj = mars_data.parse_file(p, warnings=w)
# strict re-parse to record the recovery
if w:
warns.append((rel, w))
elif k == "effect":
obj = effect_txt.parse_file(p)
elif k == "csv":
obj = manifest.parse_csv_file(p)
elif k == "manifest":
obj = manifest.parse_manifest_file(p)
if obj.problems:
raise ValueError("; ".join(obj.problems))
elif k == "rows":
obj = flat_kv.parse_rows_file(p)
elif k == "kv":
txt = manifest.read_text(p)
obj = flat_kv.parse_kv(txt)
d = flat_kv.duplicates(txt)
if d:
warns.append((rel, [f"duplicate keys {d}"]))
else:
continue
parsed[rel] = obj
ok[k] += 1
except Exception as e: # noqa: BLE001
fail[k] += 1
fails.append((rel, repr(e)))
P("## Parse results")
P("")
P("| reader | file kind | files | parsed | failed |")
P("|---|---|---|---|---|")
reader_of = {"brace": "mars_data", "effect": "effect_txt", "csv": "manifest.parse_csv",
"manifest": "manifest.parse_manifest", "rows": "flat_kv.parse_rows", "kv": "flat_kv.parse_kv"}
for k in sorted(set(ok) | set(fail)):
P(f"| {reader_of[k.split(':')[0]]} | {k} | {ok[k] + fail[k]} | {ok[k]} | {fail[k]} |")
P("")
P(f"Total: {sum(ok.values())} parsed, {sum(fail.values())} failed "
f"(skipped: {sum(1 for _, r in walk(root) if kind_of(r) in ('hlsl', 'prose', 'other'))} "
f"HLSL/prose files that are not data).")
if fails:
P("")
P("Failures:")
for rel, e in fails:
P(f"- `{rel}`: {e}")
if warns:
P("")
P("Lenient recoveries (engine-compatible; strict=True would reject these):")
for rel, w in warns:
P(f"- `{rel}`: {'; '.join(w)}")
P("")
# ---- 2. schema stats ---------------------------------------------------
stats = {}
for rel, obj in parsed.items():
k = kind_of(rel)
if k in ("brace:weapon", "brace:shipsection", "brace:tech", "brace:combat", "brace:def", "brace:script"):
schema_walk(obj, k.split(":")[1], stats)
schema_json = {path: {"blocks": st["blocks"],
"keys": dict(st["keys"].most_common()),
"subblocks": dict(st["sub"].most_common())}
for path, st in sorted(stats.items())}
json.dump(schema_json, open(os.path.join(out, "schema_stats.json"), "w"), indent=1)
P("## Schema stats (key frequency per block type)")
P("")
P("Full table in `schema_stats.json`. Block paths with instance counts and the")
P("keys seen in them (count = number of block instances carrying the key):")
P("")
for path in ["weapon.weapon", "shipsection.shipsection", "tech.tech"]:
st = stats[path]
P(f"### `{path}` ({st['blocks']} instances)")
P("")
P("keys: " + ", ".join(f"{k}:{n}" for k, n in st["keys"].most_common()))
P("")
P("sub-blocks: " + ", ".join(f"{k}:{n}" for k, n in st["sub"].most_common()))
P("")
P("All block paths: " + ", ".join(f"`{p}`({st['blocks']})" for p, st in sorted(stats.items())))
P("")
# ---- 3. build catalogs -------------------------------------------------
techs = parsed["TechTree/MasterTechList.tech"]["tech"]
tech_by = {t["name"].lower(): t for t in techs}
groups = collections.defaultdict(list)
for t in techs:
if "group" in t:
groups[str(t["group"]).upper()].append(t["name"])
strings_rows = parsed["Locale/EN/Strings.csv"]
strings = {}
string_dups = []
for r in strings_rows:
k, v = r[0], (r[1] if len(r) > 1 else "")
if k in strings:
string_dups.append((k, strings[k], v))
strings[k] = v
strings_lc = {k.lower(): v for k, v in strings.items()}
def s(key):
return strings_lc.get(key.lower())
weapons = {} # stem -> record
for rel, obj in parsed.items():
if kind_of(rel) != "brace:weapon":
continue
stem = os.path.basename(rel)[:-7]
w = dict(obj["weapon"])
weapons[stem.lower()] = {"stem": stem, "file": rel,
"scope": "NPC" if rel.startswith("Species/_NPC") else "player",
"id": None, **w}
wman = parsed["Weapons/_weapons.txt"]
for i, name in wman.entries:
key = name.lower()[:-7]
if key in weapons and weapons[key]["scope"] == "player":
weapons[key]["id"] = i
sections = {} # (race, stem) -> record
for rel, obj in parsed.items():
if kind_of(rel) != "brace:shipsection":
continue
race = rel.split("/")[1]
stem = os.path.basename(rel)[:-12]
sections[(race, stem.lower())] = {"race": race, "stem": stem, "file": rel, "id": None, **obj["shipsection"]}
for race in ALL_RACE_DIRS:
m = parsed[f"Species/{race}/sections/_shipsections.txt"]
for i, name in m.entries:
key = (race, name.lower()[:-12])
if key in sections:
sections[key]["id"] = i
section_stems = collections.defaultdict(list) # stem.lower -> [races]
for (race, st), rec in sections.items():
section_stems[st].append(race)
# ---- 4. cross-links ----------------------------------------------------
X = {}
P("## Cross-link results")
P("")
def tech_exists(name):
n = name.lower()
if n in tech_by:
return True
if n.startswith("grp_") and n[4:].upper() in groups:
return True
return False
# weapon.requires -> tech (repeated `requires` lines = AND of techs)
dang = []
case_mismatch = []
nref = 0
multi = 0
for w in weapons.values():
reqs = get_list(w, "requires")
multi += len(reqs) > 1
for r in reqs:
nref += 1
r = str(r)
if not tech_exists(r):
dang.append((w["file"], r))
elif r.lower() in tech_by and tech_by[r.lower()]["name"] != r:
case_mismatch.append((w["file"], r))
no_req = [w["file"] for w in weapons.values() if "requires" not in w]
X["weapon_requires_dangling"] = dang
X["weapon_requires_case_mismatch"] = case_mismatch
X["weapon_without_requires"] = no_req
P(f"- weapon `requires` -> tech: {nref} refs in {len(weapons) - len(no_req)} weapons ({multi} weapons list 2+ techs), "
f"{len(dang)} dangling, {len(case_mismatch)} case-mismatched; {len(no_req)} weapons have no `requires` "
f"(NPC: {sum(1 for f in no_req if f.startswith('Species/_NPC'))}, player: "
f"{[os.path.basename(f) for f in no_req if not f.startswith('Species/_NPC')]}).")
for f, r in dang:
P(f" - DANGLING `{f}` requires `{r}`")
for f, r in case_mismatch:
P(f" - case: `{f}` requires `{r}` (tech is `{tech_by[r.lower()]['name']}`)")
# shipsection.requires / option -> tech
dang = []
case_mm = []
opt_dang = []
scalar_opts = []
nreq = 0
nopt = 0
for rec in sections.values():
for r in get_list(rec, "requires"):
nreq += 1
if not tech_exists(str(r)):
dang.append((rec["file"], r))
elif str(r).lower() in tech_by and tech_by[str(r).lower()]["name"] != r:
case_mm.append((rec["file"], r))
for blk_key in ("option", "optiondef"):
for blk in get_list(rec, blk_key):
# a few files write a bare `option TECH` at section level
# instead of wrapping it in option { }
opts = get_list(blk, "option") if isinstance(blk, dict) else [blk]
if not isinstance(blk, dict):
scalar_opts.append((rec["file"], blk))
for o in opts:
nopt += 1
if not tech_exists(str(o)):
opt_dang.append((rec["file"], o))
X["shipsection_requires_dangling"] = dang
X["shipsection_requires_case_mismatch"] = case_mm
X["shipsection_option_dangling"] = opt_dang
P(f"- shipsection `requires` -> tech: {nreq} refs, {len(dang)} dangling, {len(case_mm)} case-mismatched.")
for f, r in dang:
P(f" - DANGLING `{f}` requires `{r}`")
for f, r in case_mm:
P(f" - case: `{f}` requires `{r}`")
X["shipsection_scalar_option"] = scalar_opts
P(f"- shipsection `option{{option T}}`/`optiondef` -> tech: {nopt} refs, {len(opt_dang)} dangling. "
f"Two forms coexist: `option {{ option A option B }}` (a mutually-exclusive choice group) and a bare "
f"section-level `option T` ({len(scalar_opts)} occurrences in {len(set(f for f, _ in scalar_opts))} files, "
f"e.g. `option DRV_PlsmFoc` on engine sections) -- both merge under the key `option`, so consumers must "
f"accept str-or-dict list members.")
for f, r in sorted(set(opt_dang)):
P(f" - DANGLING `{f}` option `{r}`")
# tech.ship.section -> shipsection
dang = []
nsec = 0
for t in techs:
for blk in get_list(t, "ship"):
for sname in get_list(blk, "section"):
nsec += 1
if str(sname).lower() not in section_stems:
dang.append((t["name"], sname))
X["tech_ship_section_dangling"] = dang
P(f"- tech `ship{{section}}` -> shipsection: {nsec} refs, {len(dang)} dangling "
f"(matched against the union of all race catalogs, case-insensitive).")
for t, sname in dang:
P(f" - DANGLING tech `{t}` unlocks section `{sname}`")
# tech.weapon.filename -> file
disk = {rel.lower() for _, rel in walk(root)}
dang = [(t["name"], w["filename"]) for t in techs for w in get_list(t, "weapon") if w["filename"].lower() not in disk]
X["tech_weapon_filename_dangling"] = dang
nw = sum(len(get_list(t, "weapon")) for t in techs)
P(f"- tech `weapon{{filename}}` -> file: {nw} refs, {len(dang)} dangling.")
# tech.requires / allows -> tech
dang_req = [(t["name"], r) for t in techs for r in get_list(t, "requires") if not tech_exists(str(r))]
edges = []
dang_allow = []
bad_allow = []
for t in techs:
for a in get_list(t, "allows"):
child, rp, pct, extra = parse_allows(a)
if extra or rp is None:
bad_allow.append((t["name"], a))
if child.lower() not in tech_by:
dang_allow.append((t["name"], child))
edges.append({"from": t["name"], "to": child, "rp": rp, "pct": pct})
X["tech_requires_dangling"] = dang_req
X["tech_allows_dangling"] = dang_allow
X["tech_allows_unparsed"] = bad_allow
P(f"- tech `requires` -> tech/GRP_: {sum(len(get_list(t, 'requires')) for t in techs)} refs, {len(dang_req)} dangling. "
f"Groups: {dict((g, len(v)) for g, v in groups.items())}.")
for t, r in dang_req:
P(f" - DANGLING tech `{t}` requires `{r}`")
P(f"- tech `allows` edges: {len(edges)}, {len(dang_allow)} point at unknown techs, {len(bad_allow)} unparsable.")
for t, c in dang_allow:
P(f" - DANGLING tech `{t}` allows `{c}`")
roots = [t["name"] for t in techs if not any(e["to"].lower() == t["name"].lower() for e in edges)]
P(f"- techs never allowed by anything (roots/orphans): {len(roots)}: {', '.join(roots)}")
dup_names = [n for n, c in collections.Counter(t["name"].lower() for t in techs).items() if c > 1]
P(f"- duplicate tech names: {dup_names or 'none'}")
# manifests <-> files
P("- id manifests <-> files:")
man_rep = {}
wfiles = {os.path.basename(rel).lower() for rel in parsed if rel.startswith("Weapons/") and rel.endswith(".weapon")}
listed = {n.lower() for _, n in wman.entries}
man_rep["Weapons"] = {"ids": len(wman.entries), "deleted": wman.deleted,
"listed_but_no_file": sorted(listed - wfiles), "file_but_unlisted": sorted(wfiles - listed)}
for race in ALL_RACE_DIRS:
m = parsed[f"Species/{race}/sections/_shipsections.txt"]
files = {os.path.basename(rel).lower() for rel in parsed if rel.startswith(f"Species/{race}/sections/") and rel.endswith(".shipsection")}
listed = {n.lower() for _, n in m.entries}
man_rep[race] = {"ids": len(m.entries), "deleted": m.deleted,
"listed_but_no_file": sorted(listed - files), "file_but_unlisted": sorted(files - listed)}
X["manifests"] = man_rep
for k, v in man_rep.items():
P(f" - `{k}`: {v['ids']} ids (deleted {v['deleted'] or 'none'}); "
f"listed-but-no-file {len(v['listed_but_no_file'])}; file-but-unlisted {len(v['file_but_unlisted'])}")
for n in v["listed_but_no_file"]:
P(f" - MISSING FILE for id {[i for i, nn in (wman if k == 'Weapons' else parsed[f'Species/{k}/sections/_shipsections.txt']).entries if nn.lower() == n][0]}: `{n}`")
for n in v["file_but_unlisted"]:
P(f" - UNLISTED file `{n}` (no network/save id)")
npc_weapons_unlisted = sorted(w["file"] for w in weapons.values() if w["scope"] == "NPC")
P(f" - `Species/_NPC/weapons/*.weapon` ({len(npc_weapons_unlisted)} files) have no manifest at all; "
f"they are referenced by filename from `_NPC` shipsection `bank{{weapon}}` lines.")
# strings
P("- localization:")
P(f" - `Strings.csv`: {len(strings_rows)} data rows -> {len(strings)} keys. {len(string_dups)} keys occur twice "
f"because one copy carries a trailing space (parse_csv strips cells; the later row wins):")
for k, a, b in string_dups:
P(f" - `{k}`: {a!r} then {b!r}")
miss_tn = [t["name"] for t in techs if s("TECHNAME_" + t["name"]) is None]
miss_td = [t["name"] for t in techs if s("TECHDESC_" + t["name"]) is None]
P(f" - TECHNAME_/TECHDESC_ for {len(techs)} techs: {len(miss_tn)} / {len(miss_td)} missing. {miss_tn} {miss_td}")
stems = sorted(section_stems)
miss_sn = [st for st in stems if s("SECTIONNAME_" + st) is None]
miss_sd = [st for st in stems if s("SECTIONDESC_" + st) is None]
P(f" - SECTIONNAME_/SECTIONDESC_ for {len(stems)} distinct section stems: {len(miss_sn)} / {len(miss_sd)} missing.")
for label, miss in (("SECTIONNAME_", miss_sn), ("SECTIONDESC_", miss_sd)):
npc_only = [st for st in miss if section_stems[st] == ["_NPC"]]
other = [st for st in miss if st not in npc_only]
P(f" - missing {label}: {len(npc_only)} are `_NPC`-only stems (never shown in the design UI); "
f"player-race stems: {len(other)} {other}")
miss_wn = [(w["file"], w.get("name")) for w in weapons.values()
if isinstance(w.get("name"), str) and w["name"].startswith("@") and s(w["name"][1:]) is None]
unnamed = [w["file"] for w in weapons.values() if "name" not in w]
P(f" - weapon `name @TOKEN`: {len(miss_wn)} unresolved of {sum(1 for w in weapons.values() if 'name' in w)}; "
f"{len(unnamed)} weapons carry no `name`.")
for f, n in miss_wn:
P(f" - UNRESOLVED `{f}` name `{n}`")
# every @token anywhere in brace files
at_missing = collections.Counter()
at_total = 0
for rel, obj in parsed.items():
if not kind_of(rel).startswith("brace"):
continue
for tok in re.findall(r"@([A-Za-z0-9_]+)", manifest.read_text(os.path.join(root, rel))):
at_total += 1
if s(tok) is None:
at_missing[(rel, tok)] += 1
P(f" - all `@TOKEN` refs in brace-block files: {at_total} refs, {len(at_missing)} unresolved.")
for (rel, tok), n in sorted(at_missing.items()):
P(f" - UNRESOLVED `{rel}` `@{tok}`")
X["strings"] = {"missing_techname": miss_tn, "missing_techdesc": miss_td,
"missing_sectionname": miss_sn, "missing_sectiondesc": miss_sd,
"unresolved_weapon_name": miss_wn, "unresolved_at_tokens": sorted(f"{r}:@{t}" for r, t in at_missing)}
# turrets
turrets = parsed["Weapons/_turrets.txt"]
def last_lc(d, key):
v = get_list(d, key)
return str(v[-1]).lower() if v else None
tpairs = {(str(r[1]).lower(), str(r[2]).lower()) for r in turrets} # (weapon-size, class)
tslots = {(str(r[0]).lower(), str(r[2]).lower()) for r in turrets} # (mount size, class)
wpairs = collections.Counter((last_lc(w, "turretsize"), last_lc(w, "turretclass")) for w in weapons.values())
w_unfit = sorted((p, n) for p, n in wpairs.items() if p not in tpairs)
bpairs = collections.Counter()
nobank = 0
dupkeys = 0
for rec in sections.values():
for b in get_list(rec, "bank"):
if "turretsize" not in b:
nobank += 1
continue
if isinstance(b.get("turretsize"), list) or isinstance(b.get("turretclass"), list):
dupkeys += 1
bpairs[(last_lc(b, "turretsize"), last_lc(b, "turretclass"))] += 1
b_unfit = sorted((p, n) for p, n in bpairs.items() if p not in tslots)
X["turrets"] = {"turret_rows": len(turrets), "weapon_size_class_pairs_without_turret": w_unfit,
"bank_size_class_pairs_without_turret": b_unfit,
"banks_without_turretsize": nobank, "banks_with_repeated_size_or_class": dupkeys}
P(f"- `_turrets.txt` ({len(turrets)} rows; size/class values compared case-insensitively -- the data mixes "
f"`Large`/`large`, `Missile`/`missile`, `Standard`/`standard`):")
P(f" - weapon (turretsize,turretclass) pairs with no turret row: {w_unfit or 'none'}")
P(f" - section bank (turretsize,turretclass) pairs with no turret row: {b_unfit or 'none'}")
P(f" - banks with no turretsize at all (NPC fixed-weapon banks): {nobank}; banks that repeat "
f"turretsize/turretclass inside one bank{{}} (last value taken): {dupkeys}")
# NPC bank{weapon} refs
dang = []
n = 0
for rec in sections.values():
for b in get_list(rec, "bank"):
for wf in get_list(b, "weapon"):
n += 1
if str(wf).lower() not in disk:
dang.append((rec["file"], wf))
X["bank_weapon_dangling"] = dang
P(f"- shipsection `bank{{weapon <file>}}` -> file: {n} refs, {len(dang)} dangling.")
for f, w in dang:
P(f" - DANGLING `{f}` -> `{w}`")
# default weapons
dw = parsed["Weapons/_defaultweapons.txt"]
dang = [r for r in dw if ("weapons/" + str(r[2])).lower() not in disk]
P(f"- `_defaultweapons.txt`: {len(dw)} rows, {len(dang)} name a missing weapon file. {dang or ''}")
# AI tables
def csv_col(rel, col):
return [r[col] for r in parsed[rel] if len(r) > col and r[col]]
ai = {}
for rel in ("Data/Strategy/AI/aitechpri.csv", "Data/Strategy/AI/aitechgrp.csv", "Data/Strategy/AI/aitechmode.csv"):
rows = parsed[rel]
bad = [t for t in csv_col(rel, 0) if t.lower() not in tech_by]
ai[rel] = bad
if not rows:
P(f"- `{rel}`: 0 data rows -- the shipped file is a comment-only template (schema documented in its "
f"header, no entries); the AI's tech priorities must therefore come from code.")
else:
P(f"- `{rel}`: {len(rows)} rows; col0 not a tech: {bad or 'none'}")
bad = [x for x in csv_col("Data/Strategy/AI/affinity_section.csv", 0) if x.lower() not in section_stems]
ai["affinity_section_unknown"] = bad
P(f"- `AI/affinity_section.csv`: {len(parsed['Data/Strategy/AI/affinity_section.csv'])} rows; unknown sections: {bad or 'none'}")
bad = [x for x in csv_col("Data/Strategy/AI/raider_sections.csv", 0) if x.lower() not in section_stems]
P(f"- `AI/raider_sections.csv`: unknown sections: {bad or 'none'}")
wr = parsed["Data/Strategy/AI/weapon_replacements.csv"]
bad = [x for r in wr for x in r if x and x.lower() not in weapons]
ai["weapon_replacements_unknown"] = bad
P(f"- `AI/weapon_replacements.csv`: {len(wr)} rows; unknown weapon stems: {bad or 'none'}")
fams = collections.Counter(str(w.get("weaponfamily")) for w in weapons.values() if "weaponfamily" in w)
aw = csv_col("Data/Strategy/AI/affinity_weapon.csv", 0)
bad = [x for x in aw if x not in fams]
P(f"- `AI/affinity_weapon.csv`: families {sorted(set(aw))}; not a weaponfamily in any .weapon: {bad or 'none'}. "
f"weaponfamily values in data: {dict(fams)}")
# scenarios
for rel in sorted(parsed):
if rel.startswith("Scenarios/") and rel.endswith("Templates.csv"):
bad = [(r[0], x) for r in parsed[rel] for x in r[1:4] if x.lower() not in section_stems]
P(f"- `{rel}`: {len(parsed[rel])} rows; unknown sections: {bad or 'none'}")
if rel.startswith("Scenarios/") and rel.endswith("Techs.csv"):
bad = [x for x in csv_col(rel, 0) if x.lower() not in tech_by]
P(f"- `{rel}`: {len(parsed[rel])} rows; unknown techs: {bad or 'none'}")
X["ai"] = ai
json.dump(X, open(os.path.join(out, "crosslink.json"), "w"), indent=1)
P("")
# ---- 5. artifacts ------------------------------------------------------
nodes = []
for t in techs:
strat = get_list(t, "strategy")
inc = [x for b in strat for x in get_list(b, "inc")]
dec = [x for b in strat for x in get_list(b, "dec")]
nodes.append({
"name": t["name"],
"display_name": s("TECHNAME_" + t["name"]),
"description": s("TECHDESC_" + t["name"]),
"family": t.get("family"),
"family_inferred": t["name"].split("_", 1)[0].upper(),
"type": t.get("type"),
"threat": t.get("threat"),
"group": t.get("group"),
"option_cost": t.get("option_cost"),
"requires": [str(r) for r in get_list(t, "requires")],
"benefits_inc": inc,
"benefits_dec": dec,
"sections": [str(x) for b in get_list(t, "ship") for x in get_list(b, "section")],
"weapons": [w["filename"] for w in get_list(t, "weapon")],
"allows": [e["to"] for e in edges if e["from"] == t["name"]],
})
tech_tree = {
"_about": "SOTS1 MasterTechList.tech normalized. family is only written on ~half the nodes; "
"family_inferred is the name prefix (IND/WEP/DRV/...). edges[].pct: per-race availability % as written; "
"a race absent from pct has no override in the file (the engine default -- believed to be 100 -- "
"is code-owned, not asserted here). rp = research-point cost of the edge. "
"requires may name GRP_<group>, satisfied by any tech with group <group>.",
"races": RACES,
"groups": dict(groups),
"nodes": nodes,
"edges": edges,
}
json.dump(tech_tree, open(os.path.join(out, "tech_tree.json"), "w"), indent=1)
def norm_weapon(w):
d = dict(w)
d["display_name"] = s(w["name"][1:]) if isinstance(w.get("name"), str) and w["name"].startswith("@") else w.get("name")
return d
json.dump({"_about": "All *.weapon files (Weapons/ = player catalog with ids from _weapons.txt; Species/_NPC/weapons = NPC, no ids). "
"Keys lower-cased; repeated keys -> lists; bareword numbers typed.",
"weapons": [norm_weapon(w) for w in sorted(weapons.values(), key=lambda w: (w["scope"], w["stem"].lower()))]},
open(os.path.join(out, "weapons.json"), "w"), indent=1)
def norm_section(r):
d = dict(r)
d["display_name"] = s("SECTIONNAME_" + r["stem"])
d["description"] = s("SECTIONDESC_" + r["stem"])
d["unlocked_by"] = [t["name"] for t in techs for b in get_list(t, "ship") if r["stem"].lower() in [str(x).lower() for x in get_list(b, "section")]]
return d
json.dump({"_about": "All Species/<race>/sections/*.shipsection; id from the race's _shipsections.txt (null = unlisted). "
"Keys lower-cased; repeated keys (bank, option, thruster, requires) -> lists.",
"sections": [norm_section(r) for r in sorted(sections.values(), key=lambda r: (r["race"], r["stem"].lower()))]},
open(os.path.join(out, "shipsections.json"), "w"), indent=1)
json.dump(strings, open(os.path.join(out, "strings.json"), "w"), indent=1, ensure_ascii=False)
with open(os.path.join(out, "tech_tree.dot"), "w") as f:
f.write("digraph sots_tech {\n rankdir=LR; node [shape=box, fontsize=9];\n")
fam_color = {"IND": "#f4d03f", "NRG": "#e74c3c", "SLD": "#3498db", "DRV": "#9b59b6", "TRP": "#e67e22",
"WAR": "#c0392b", "BAL": "#7f8c8d", "BIO": "#2ecc71", "CCC": "#1abc9c", "DRN": "#95a5a6", "XNC": "#d35400"}
for n in nodes:
col = fam_color.get(str(n["family"]), "#ffffff")
label = n["display_name"] or n["name"]
f.write(f' "{n["name"]}" [label="{label}\\n{n["name"]}", style=filled, fillcolor="{col}"];\n')
for e in edges:
lab = f"{e['rp']}" if e["rp"] is not None else ""
if e["pct"]:
lab += "\\n" + " ".join(f"{r[:2]}{v}" for r, v in e["pct"].items())
f.write(f' "{e["from"]}" -> "{e["to"]}" [label="{lab}", fontsize=7];\n')
f.write("}\n")
P("## Artifacts")
P("")
for fn in ("tech_tree.json", "weapons.json", "shipsections.json", "strings.json", "schema_stats.json", "crosslink.json", "tech_tree.dot"):
P(f"- `{fn}` ({os.path.getsize(os.path.join(out, fn)) // 1024} KB)")
P(f"- tech_tree.json: {len(nodes)} nodes, {len(edges)} edges; weapons.json: {len(weapons)}; "
f"shipsections.json: {len(sections)}; strings.json: {len(strings)} keys")
print("\n".join(rep))
return 0 if not fails else 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1], sys.argv[2]))

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,650 @@
digraph sots_tech {
rankdir=LR; node [shape=box, fontsize=9];
"IND_ROOT" [label="IND_ROOT\nIND_ROOT", style=filled, fillcolor="#f4d03f"];
"EWP_ROOT" [label="EWP_ROOT\nEWP_ROOT", style=filled, fillcolor="#e74c3c"];
"SLD_Root" [label="SLD_Root\nSLD_Root", style=filled, fillcolor="#3498db"];
"NRG_Root" [label="NRG_Root\nNRG_Root", style=filled, fillcolor="#9b59b6"];
"TRP_ROOT" [label="TRP_ROOT\nTRP_ROOT", style=filled, fillcolor="#e67e22"];
"WHD_ROOT" [label="WHD_ROOT\nWHD_ROOT", style=filled, fillcolor="#c0392b"];
"BAL_ROOT" [label="BAL_ROOT\nBAL_ROOT", style=filled, fillcolor="#7f8c8d"];
"DRV_ROOT" [label="DRV_ROOT\nDRV_ROOT", style=filled, fillcolor="#9b59b6"];
"BIO_ROOT" [label="BIO_ROOT\nBIO_ROOT", style=filled, fillcolor="#2ecc71"];
"CCC_ROOT" [label="CCC_ROOT\nCCC_ROOT", style=filled, fillcolor="#1abc9c"];
"DRN_ROOT" [label="DRN_ROOT\nDRN_ROOT", style=filled, fillcolor="#95a5a6"];
"XNC_ROOT" [label="XNC_ROOT\nXNC_ROOT", style=filled, fillcolor="#d35400"];
"IND_Waldo" [label="Waldo Units\nIND_Waldo", style=filled, fillcolor="#ffffff"];
"IND_TrkStl" [label="Tarkasian Living Steel\nIND_TrkStl", style=filled, fillcolor="#ffffff"];
"IND_RefCoat" [label="Reflective Coating\nIND_RefCoat", style=filled, fillcolor="#ffffff"];
"IND_ImpRfCt" [label="Improved Reflective Coating\nIND_ImpRfCt", style=filled, fillcolor="#ffffff"];
"IND_HrdElec" [label="Hardened Electronics\nIND_HrdElec", style=filled, fillcolor="#ffffff"];
"IND_StlthArm" [label="Stealth Armor\nIND_StlthArm", style=filled, fillcolor="#ffffff"];
"IND_OrbFound" [label="Orbital Foundries\nIND_OrbFound", style=filled, fillcolor="#ffffff"];
"IND_SpnlMnt" [label="Spinal Mounts\nIND_SpnlMnt", style=filled, fillcolor="#ffffff"];
"IND_SlvgTech" [label="Salvage Technology\nIND_SlvgTech", style=filled, fillcolor="#ffffff"];
"IND_AstMine" [label="Asteroid Mining\nIND_AstMine", style=filled, fillcolor="#ffffff"];
"IND_MsMine" [label="Mega-Strip Mining\nIND_MsMine", style=filled, fillcolor="#ffffff"];
"IND_AstMon" [label="Monitor Construction\nIND_AstMon", style=filled, fillcolor="#ffffff"];
"IND_PlyAlloy" [label="Polysilicate Alloys\nIND_PlyAlloy", style=filled, fillcolor="#ffffff"];
"IND_MagLat" [label="MagnoCeramic Lattices\nIND_MagLat", style=filled, fillcolor="#ffffff"];
"IND_QrkRes" [label="Quark Resonators\nIND_QrkRes", style=filled, fillcolor="#ffffff"];
"IND_EleNans" [label="Elemental Nanites\nIND_EleNans", style=filled, fillcolor="#ffffff"];
"IND_AdmAly" [label="Adamantite Alloys\nIND_AdmAly", style=filled, fillcolor="#ffffff"];
"IND_CruisCon" [label="Cruiser Construction\nIND_CruisCon", style=filled, fillcolor="#ffffff"];
"IND_MegFrght" [label="Mega-Freighters\nIND_MegFrght", style=filled, fillcolor="#ffffff"];
"IND_ModCon" [label="Modular Construction\nIND_ModCon", style=filled, fillcolor="#ffffff"];
"IND_BrdPod" [label="Boarding Pods\nIND_BrdPod", style=filled, fillcolor="#ffffff"];
"IND_AtProc" [label="Atmospheric Processors\nIND_AtProc", style=filled, fillcolor="#ffffff"];
"IND_OrbDry" [label="Orbital Drydocks\nIND_OrbDry", style=filled, fillcolor="#ffffff"];
"IND_DSCon" [label="Deep Space Constructors\nIND_DSCon", style=filled, fillcolor="#ffffff"];
"IND_OrbCom" [label="Orbital Complexes\nIND_OrbCom", style=filled, fillcolor="#ffffff"];
"IND_HvyPlat" [label="Heavy Platforms\nIND_HvyPlat", style=filled, fillcolor="#ffffff"];
"IND_TrpSat" [label="Torpedo Defense Platforms\nIND_TrpSat", style=filled, fillcolor="#ffffff"];
"IND_DreadCon" [label="Dreadnought Construction\nIND_DreadCon", style=filled, fillcolor="#ffffff"];
"IND_AdvDreadEng" [label="Advanced Dreadnought Engineering\nIND_AdvDreadEng", style=filled, fillcolor="#ffffff"];
"IND_GravCon" [label="Gravity Control\nIND_GravCon", style=filled, fillcolor="#ffffff"];
"IND_TrctBm" [label="Tractor Beam\nIND_TrctBm", style=filled, fillcolor="#ffffff"];
"IND_ArcCon" [label="Arcology Construction\nIND_ArcCon", style=filled, fillcolor="#ffffff"];
"IND_HrdStrct" [label="Hardened Structures\nIND_HrdStrct", style=filled, fillcolor="#ffffff"];
"IND_Decon" [label="Zero-G Deconstruction\nIND_Decon", style=filled, fillcolor="#ffffff"];
"WEP_RedLas" [label="Red Lasers\nWEP_RedLas", style=filled, fillcolor="#e74c3c"];
"WEP_GrnLas" [label="Green Lasers\nWEP_GrnLas", style=filled, fillcolor="#e74c3c"];
"WEP_UvLas" [label="UltraViolet Lasers\nWEP_UvLas", style=filled, fillcolor="#e74c3c"];
"WEP_XryLas" [label="X-Ray Lasers\nWEP_XryLas", style=filled, fillcolor="#e74c3c"];
"WEP_GrnBmr" [label="Green Beamers\nWEP_GrnBmr", style=filled, fillcolor="#e74c3c"];
"WEP_UvBmr" [label="UV Beamers\nWEP_UvBmr", style=filled, fillcolor="#e74c3c"];
"WEP_LtEmitr" [label="Light Emitter\nWEP_LtEmitr", style=filled, fillcolor="#e74c3c"];
"WEP_Emitr" [label="Emitter\nWEP_Emitr", style=filled, fillcolor="#e74c3c"];
"WEP_HvyEmitr" [label="Heavy Emitter\nWEP_HvyEmitr", style=filled, fillcolor="#e74c3c"];
"WEP_PlsmCan" [label="Plasma Cannon\nWEP_PlsmCan", style=filled, fillcolor="#e74c3c"];
"WEP_FusCan" [label="Fusion Cannon\nWEP_FusCan", style=filled, fillcolor="#e74c3c"];
"WEP_AmCan" [label="Anti-Matter Cannon\nWEP_AmCan", style=filled, fillcolor="#e74c3c"];
"WEP_HvyPCan" [label="Heavy Plasma Cannon\nWEP_HvyPCan", style=filled, fillcolor="#e74c3c"];
"WEP_HvyFCan" [label="Heavy Fusion Cannon\nWEP_HvyFCan", style=filled, fillcolor="#e74c3c"];
"WEP_HvyACan" [label="Heavy Anti-Matter Cannon\nWEP_HvyACan", style=filled, fillcolor="#e74c3c"];
"WEP_PolPlsm" [label="Polarized Plasmatics\nWEP_PolPlsm", style=filled, fillcolor="#e74c3c"];
"WEP_PolFus" [label="Chakkar\nWEP_PolFus", style=filled, fillcolor="#e74c3c"];
"WEP_PolAm" [label="Chakram\nWEP_PolAm", style=filled, fillcolor="#e74c3c"];
"WEP_PlsmProj" [label="Plasma Projector\nWEP_PlsmProj", style=filled, fillcolor="#e74c3c"];
"WEP_FusProj" [label="Fusion Projector\nWEP_FusProj", style=filled, fillcolor="#e74c3c"];
"WEP_AmProj" [label="Anti-Matter Projector\nWEP_AmProj", style=filled, fillcolor="#e74c3c"];
"WEP_MesProj" [label="Meson Projector\nWEP_MesProj", style=filled, fillcolor="#e74c3c"];
"WEP_Phas" [label="Phasers\nWEP_Phas", style=filled, fillcolor="#e74c3c"];
"WEP_PDPhas" [label="Point Defense Phaser\nWEP_PDPhas", style=filled, fillcolor="#e74c3c"];
"WEP_PlsPhas" [label="Pulse Phasers\nWEP_PlsPhas", style=filled, fillcolor="#e74c3c"];
"WEP_HCLas" [label="Heavy Combat Lasers\nWEP_HCLas", style=filled, fillcolor="#e74c3c"];
"WEP_Lancer" [label="Lancers\nWEP_Lancer", style=filled, fillcolor="#e74c3c"];
"WEP_CtngBm" [label="Cutting Beam\nWEP_CtngBm", style=filled, fillcolor="#e74c3c"];
"WEP_PrtBm" [label="Particle Beam\nWEP_PrtBm", style=filled, fillcolor="#e74c3c"];
"WEP_NeutBm" [label="Neutron Beam\nWEP_NeutBm", style=filled, fillcolor="#e74c3c"];
"WEP_PosiBm" [label="Positron Beam\nWEP_PosiBm", style=filled, fillcolor="#e74c3c"];
"WEP_MesBm" [label="Meson Beam\nWEP_MesBm", style=filled, fillcolor="#e74c3c"];
"WEP_GravBm" [label="Graviton Beam\nWEP_GravBm", style=filled, fillcolor="#e74c3c"];
"WEP_PulGrvBm" [label="Pulsed Graviton Beam\nWEP_PulGrvBm", style=filled, fillcolor="#e74c3c"];
"WEP_Dsrptr" [label="Disruptor\nWEP_Dsrptr", style=filled, fillcolor="#e67e22"];
"WEP_DsrptrWhp" [label="Disruptor Whip\nWEP_DsrptrWhp", style=filled, fillcolor="#7f8c8d"];
"WEP_EmPulse" [label="Electro Magnetic Pulsar\nWEP_EmPulse", style=filled, fillcolor="#e67e22"];
"WEP_IntCan" [label="Inertial Cannon\nWEP_IntCan", style=filled, fillcolor="#e74c3c"];
"WEP_HvyIntCan" [label="Heavy Inertial Cannon\nWEP_HvyIntCan", style=filled, fillcolor="#e74c3c"];
"WEP_PlsTrp" [label="Pulsar Torpedo\nWEP_PlsTrp", style=filled, fillcolor="#e67e22"];
"WEP_PhotTrp" [label="Photonic Torpedo\nWEP_PhotTrp", style=filled, fillcolor="#e67e22"];
"WEP_GluTrp" [label="Gluonic Torpedo\nWEP_GluTrp", style=filled, fillcolor="#e67e22"];
"WEP_KelTrp" [label="Kelvinic Torpedos\nWEP_KelTrp", style=filled, fillcolor="#e67e22"];
"WEP_MesTrp" [label="Mesonic Torpedo\nWEP_MesTrp", style=filled, fillcolor="#e67e22"];
"WEP_PlsmTrp" [label="Plasma Torpedo\nWEP_PlsmTrp", style=filled, fillcolor="#e67e22"];
"WEP_FusTrp" [label="Fusion Torpedo\nWEP_FusTrp", style=filled, fillcolor="#e67e22"];
"WEP_DtFsTrp" [label="Detonating Fusion Torpedo\nWEP_DtFsTrp", style=filled, fillcolor="#e67e22"];
"WEP_AmTrp" [label="Anti-Matter Torpedo\nWEP_AmTrp", style=filled, fillcolor="#e67e22"];
"WEP_DtAmTrp" [label="Detonating Anti-Matter Torpedo\nWEP_DtAmTrp", style=filled, fillcolor="#e67e22"];
"WEP_Nukes" [label="Nuclear Warhead\nWEP_Nukes", style=filled, fillcolor="#c0392b"];
"WEP_NukeWhd" [label="Shaped Nuclear Warhead\nWEP_NukeWhd", style=filled, fillcolor="#c0392b"];
"WEP_GmaWhd" [label="Gamma Warhead\nWEP_GmaWhd", style=filled, fillcolor="#c0392b"];
"WEP_HvyPMsl" [label="Heavy Planet Missile\nWEP_HvyPMsl", style=filled, fillcolor="#c0392b"];
"WEP_FusWhd" [label="Fusion Warhead\nWEP_FusWhd", style=filled, fillcolor="#c0392b"];
"WEP_AmWhd" [label="Anti-Matter Warhead\nWEP_AmWhd", style=filled, fillcolor="#c0392b"];
"WEP_NukMine" [label="Nuclear Mine\nWEP_NukMine", style=filled, fillcolor="#c0392b"];
"WEP_FusMine" [label="Fusion Mine\nWEP_FusMine", style=filled, fillcolor="#c0392b"];
"WEP_AmMine" [label="Anti-Matter Mine\nWEP_AmMine", style=filled, fillcolor="#c0392b"];
"WEP_GrvMine" [label="Gravity Mine\nWEP_GrvMine", style=filled, fillcolor="#c0392b"];
"WEP_ImpMine" [label="Implosion Mine\nWEP_ImpMine", style=filled, fillcolor="#c0392b"];
"WEP_LpMine" [label="Leap Mine\nWEP_LpMine", style=filled, fillcolor="#c0392b"];
"WEP_ClkMine" [label="Cloaked Mine\nWEP_ClkMine", style=filled, fillcolor="#c0392b"];
"WEP_NdMsl" [label="Node Missile\nWEP_NdMsl", style=filled, fillcolor="#c0392b"];
"WEP_CorMsl" [label="Corrosive Missile\nWEP_CorMsl", style=filled, fillcolor="#c0392b"];
"WEP_DFMsl" [label="DF Racks\nWEP_DFMsl", style=filled, fillcolor="#7f8c8d"];
"WEP_NanMsl" [label="Nanite Missile\nWEP_NanMsl", style=filled, fillcolor="#c0392b"];
"WEP_MWMsl" [label="MW Missile\nWEP_MWMsl", style=filled, fillcolor="#c0392b"];
"WEP_KKMsl" [label="KK Missile\nWEP_KKMsl", style=filled, fillcolor="#c0392b"];
"WEP_GsDrvr" [label="Gauss Driver\nWEP_GsDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_SnpCanDrvr" [label="Sniper Cannon\nWEP_SnpCanDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_MasDrvr" [label="Mass Drivers\nWEP_MasDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_StrmDrvr" [label="Stormers\nWEP_StrmDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_HvyDrvr" [label="Heavy Drivers\nWEP_HvyDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_AccAmp" [label="Accelerator Ampilification\nWEP_AccAmp", style=filled, fillcolor="#7f8c8d"];
"WEP_HStrmDrvr" [label="Heavy Stormers\nWEP_HStrmDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_ErgDrain" [label="Leach Rounds\nWEP_ErgDrain", style=filled, fillcolor="#7f8c8d"];
"WEP_VRFtech" [label="VRF Technology\nWEP_VRFtech", style=filled, fillcolor="#7f8c8d"];
"WEP_APRtech" [label="Armor Piercing Rounds\nWEP_APRtech", style=filled, fillcolor="#7f8c8d"];
"WEP_PDtech" [label="Point Defense Tracking\nWEP_PDtech", style=filled, fillcolor="#7f8c8d"];
"WEP_BrstrDrvr" [label="Bursters\nWEP_BrstrDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_MasShtDrvr" [label="Mass Shotgun\nWEP_MasShtDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_ShldDrvr" [label="Shield Breaker\nWEP_ShldDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_SgeDrvr" [label="Siege Drivers\nWEP_SgeDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_ThmpDrvr" [label="Thumpers\nWEP_ThmpDrvr", style=filled, fillcolor="#7f8c8d"];
"WEP_NeutRnd" [label="Neutronium Rounds\nWEP_NeutRnd", style=filled, fillcolor="#7f8c8d"];
"DRV_Fissn" [label="Fission Drive\nDRV_Fissn", style=filled, fillcolor="#ffffff"];
"DRV_PlsFiss" [label="Pulsed Fission Drive\nDRV_PlsFiss", style=filled, fillcolor="#ffffff"];
"DRV_OvrThrust" [label="Overthrusting\nDRV_OvrThrust", style=filled, fillcolor="#ffffff"];
"DRV_LRFiss" [label="Long-range Fission Drive\nDRV_LRFiss", style=filled, fillcolor="#ffffff"];
"DRV_RecFiss" [label="Recombinant Fissionables\nDRV_RecFiss", style=filled, fillcolor="#ffffff"];
"DRV_Fusn" [label="Fusion\nDRV_Fusn", style=filled, fillcolor="#ffffff"];
"DRV_McroFus" [label="Micro-Fusion Drives\nDRV_McroFus", style=filled, fillcolor="#ffffff"];
"DRV_Ints" [label="Interceptor Missiles\nDRV_Ints", style=filled, fillcolor="#ffffff"];
"DRV_SmlFus" [label="Small Scale Fusion Drives\nDRV_SmlFus", style=filled, fillcolor="#ffffff"];
"DRV_PlsmFoc" [label="Plasma Focusing\nDRV_PlsmFoc", style=filled, fillcolor="#ffffff"];
"DRV_LRFusn" [label="Long-range Fusion Drive\nDRV_LRFusn", style=filled, fillcolor="#ffffff"];
"DRV_AntiMat" [label="Anti-Matter\nDRV_AntiMat", style=filled, fillcolor="#ffffff"];
"DRV_QntCap" [label="Quantum Capacitors\nDRV_QntCap", style=filled, fillcolor="#ffffff"];
"DRV_Rmscps" [label="Ramscoops\nDRV_Rmscps", style=filled, fillcolor="#ffffff"];
"DRV_Node" [label="Node Drive\nDRV_Node", style=filled, fillcolor="#ffffff"];
"DRV_NodFoc" [label="Node Focusing\nDRV_NodFoc", style=filled, fillcolor="#ffffff"];
"DRV_NodPath" [label="Sub-Space Pathing\nDRV_NodPath", style=filled, fillcolor="#ffffff"];
"DRV_Rip" [label="Rip Drive\nDRV_Rip", style=filled, fillcolor="#ffffff"];
"DRV_Rend" [label="Rend Drive\nDRV_Rend", style=filled, fillcolor="#ffffff"];
"DRV_Rad" [label="Radiant Drive\nDRV_Rad", style=filled, fillcolor="#ffffff"];
"DRV_VdCtr" [label="Void Cutter Drive\nDRV_VdCtr", style=filled, fillcolor="#ffffff"];
"DRV_VdCrv" [label="Void Carver Drive\nDRV_VdCrv", style=filled, fillcolor="#ffffff"];
"DRV_VdMstr" [label="Void Mastery Drive\nDRV_VdMstr", style=filled, fillcolor="#ffffff"];
"DRV_GrvSyn" [label="Grav Synergy\nDRV_GrvSyn", style=filled, fillcolor="#ffffff"];
"DRV_Hyper" [label="Hyperdrive\nDRV_Hyper", style=filled, fillcolor="#ffffff"];
"DRV_HyprFld" [label="Shaped Hyper-Fields\nDRV_HyprFld", style=filled, fillcolor="#ffffff"];
"DRV_Warp" [label="WarpDrive\nDRV_Warp", style=filled, fillcolor="#ffffff"];
"DRV_StrWrp" [label="StutterWarp\nDRV_StrWrp", style=filled, fillcolor="#ffffff"];
"DRV_ImpStWrp" [label="Improved StutterWarp\nDRV_ImpStWrp", style=filled, fillcolor="#ffffff"];
"DRV_Flicker" [label="FlickerDrive\nDRV_Flicker", style=filled, fillcolor="#ffffff"];
"DRV_McrFlkr" [label="Micro-FlickerDrive\nDRV_McrFlkr", style=filled, fillcolor="#ffffff"];
"DRV_TpGate" [label="Teleport Gate\nDRV_TpGate", style=filled, fillcolor="#ffffff"];
"DRV_GatAmp" [label="Gate Amplifiers\nDRV_GatAmp", style=filled, fillcolor="#ffffff"];
"DRV_FarCast" [label="FarCasters\nDRV_FarCast", style=filled, fillcolor="#ffffff"];
"DRV_IncThrst" [label="Ionic Thrusters\nDRV_IncThrst", style=filled, fillcolor="#ffffff"];
"BIO_GnMod" [label="Gene Modification\nBIO_GnMod", style=filled, fillcolor="#ffffff"];
"BIO_SpndAni" [label="Suspended Animation\nBIO_SpndAni", style=filled, fillcolor="#ffffff"];
"BIO_Btrans" [label="Biological Transfer\nBIO_Btrans", style=filled, fillcolor="#ffffff"];
"BIO_AtmoAd" [label="Atmospheric Adaptation\nBIO_AtmoAd", style=filled, fillcolor="#ffffff"];
"BIO_TerBac" [label="Terraforming Bacteria\nBIO_TerBac", style=filled, fillcolor="#ffffff"];
"BIO_EnvTail" [label="Environmental Tailoring\nBIO_EnvTail", style=filled, fillcolor="#ffffff"];
"BIO_GrvAdpt" [label="Gravitational Adaptation\nBIO_GrvAdpt", style=filled, fillcolor="#ffffff"];
"BIO_Plg" [label="Plague\nBIO_Plg", style=filled, fillcolor="#ffffff"];
"BIO_PlgVac" [label="Plague Vaccine\nBIO_PlgVac", style=filled, fillcolor="#ffffff"];
"BIO_RtPlg" [label="Retro Plague\nBIO_RtPlg", style=filled, fillcolor="#ffffff"];
"BIO_RtPlgVac" [label="Retro Plague Vaccine\nBIO_RtPlgVac", style=filled, fillcolor="#ffffff"];
"BIO_Bst" [label="Beast Bomb\nBIO_Bst", style=filled, fillcolor="#ffffff"];
"BIO_BstVac" [label="Beast Bomb Vaccine\nBIO_BstVac", style=filled, fillcolor="#ffffff"];
"BIO_AsPlg" [label="Assimilation Plague\nBIO_AsPlg", style=filled, fillcolor="#ffffff"];
"BIO_AsPlgVac" [label="Assimilation Plague Vaccine\nBIO_AsPlgVac", style=filled, fillcolor="#ffffff"];
"BIO_NanVir" [label="Nano Virus\nBIO_NanVir", style=filled, fillcolor="#ffffff"];
"BIO_ConNan" [label="Counter Nanites\nBIO_ConNan", style=filled, fillcolor="#ffffff"];
"BIO_SmrtNan" [label="Smart Nanites\nBIO_SmrtNan", style=filled, fillcolor="#ffffff"];
"BIO_UniAnti" [label="Universal Antigen\nBIO_UniAnti", style=filled, fillcolor="#ffffff"];
"CCC_FTLCom" [label="FTL Communications\nCCC_FTLCom", style=filled, fillcolor="#ffffff"];
"CCC_HypCom" [label="Hyper-Link Communications\nCCC_HypCom", style=filled, fillcolor="#ffffff"];
"CCC_BtlCmp" [label="Battle Computers\nCCC_BtlCmp", style=filled, fillcolor="#ffffff"];
"CCC_FTLBrdB" [label="FTL Broadband\nCCC_FTLBrdB", style=filled, fillcolor="#ffffff"];
"CCC_FTLEcon" [label="FTL Economics\nCCC_FTLEcon", style=filled, fillcolor="#ffffff"];
"CCC_ComRaid" [label="Commerce Raiding\nCCC_ComRaid", style=filled, fillcolor="#ffffff"];
"CCC_SpyBm" [label="Sub-Space Spy Beam\nCCC_SpyBm", style=filled, fillcolor="#ffffff"];
"CCC_NdTrkHum" [label="Node Tracking: Human\nCCC_NdTrkHum", style=filled, fillcolor="#ffffff"];
"CCC_NdTrkZul" [label="Node Tracking: Zuul\nCCC_NdTrkZul", style=filled, fillcolor="#ffffff"];
"CCC_SpJam" [label="Sub-Space Jammers\nCCC_SpJam", style=filled, fillcolor="#ffffff"];
"CCC_SnsJam" [label="Sensor Jammer\nCCC_SnsJam", style=filled, fillcolor="#ffffff"];
"CCC_QntChaf" [label="Quantum Chaff\nCCC_QntChaf", style=filled, fillcolor="#ffffff"];
"CCC_IntSens" [label="Integrated Sensors\nCCC_IntSens", style=filled, fillcolor="#ffffff"];
"CCC_DatCor" [label="Data Correlation\nCCC_DatCor", style=filled, fillcolor="#ffffff"];
"CCC_AdvSens" [label="Advanced Sensors\nCCC_AdvSens", style=filled, fillcolor="#ffffff"];
"CCC_AdvCnC" [label="Advanced Command and Control\nCCC_AdvCnC", style=filled, fillcolor="#ffffff"];
"CCC_TunSens" [label="Tunneling Sensors\nCCC_TunSens", style=filled, fillcolor="#ffffff"];
"CCC_ScanSats" [label="Scanner Satellites\nCCC_ScanSats", style=filled, fillcolor="#ffffff"];
"CCC_DatSyn" [label="Data Synergy\nCCC_DatSyn", style=filled, fillcolor="#ffffff"];
"CCC_CmbtAlg" [label="Combat Algorithms\nCCC_CmbtAlg", style=filled, fillcolor="#ffffff"];
"CCC_ArmCom" [label="Armada Command Systems\nCCC_ArmCom", style=filled, fillcolor="#ffffff"];
"CCC_FCCom" [label="Flag Central Command\nCCC_FCCom", style=filled, fillcolor="#ffffff"];
"CCC_HoloTac" [label="Holographic Tactics\nCCC_HoloTac", style=filled, fillcolor="#ffffff"];
"SLD_Def" [label="Deflectors\nSLD_Def", style=filled, fillcolor="#ffffff"];
"SLD_Clk" [label="Cloaking\nSLD_Clk", style=filled, fillcolor="#ffffff"];
"SLD_ImpClk" [label="Improved Cloaking\nSLD_ImpClk", style=filled, fillcolor="#ffffff"];
"SLD_MkOne" [label="Shields Mk. 1\nSLD_MkOne", style=filled, fillcolor="#ffffff"];
"SLD_MkTwo" [label="Shields Mk. 2\nSLD_MkTwo", style=filled, fillcolor="#ffffff"];
"SLD_MkThree" [label="Shields Mk. 3\nSLD_MkThree", style=filled, fillcolor="#ffffff"];
"SLD_MkFour" [label="Shields Mk. 4\nSLD_MkFour", style=filled, fillcolor="#ffffff"];
"SLD_Magni" [label="Shield Magnifier\nSLD_Magni", style=filled, fillcolor="#ffffff"];
"SLD_ErgAb" [label="Energy Absorbers\nSLD_ErgAb", style=filled, fillcolor="#ffffff"];
"SLD_MesShld" [label="Meson Shields\nSLD_MesShld", style=filled, fillcolor="#ffffff"];
"SLD_GrvShld" [label="Grav Shields\nSLD_GrvShld", style=filled, fillcolor="#ffffff"];
"SLD_Intang" [label="Intangibility\nSLD_Intang", style=filled, fillcolor="#ffffff"];
"SLD_Proj" [label="Shield Projector\nSLD_Proj", style=filled, fillcolor="#ffffff"];
"SLD_Disr" [label="Disruptor Shield\nSLD_Disr", style=filled, fillcolor="#ffffff"];
"SLD_Focus" [label="Shield Focusing\nSLD_Focus", style=filled, fillcolor="#ffffff"];
"IND_CyberInt" [label="Cybernetic Interface\nIND_CyberInt", style=filled, fillcolor="#95a5a6"];
"IND_ExpSys" [label="Expert Systems\nIND_ExpSys", style=filled, fillcolor="#95a5a6"];
"IND_PredGun" [label="Predictive Gunnery\nIND_PredGun", style=filled, fillcolor="#95a5a6"];
"DRN_AdvRob" [label="Advanced Robotics\nDRN_AdvRob", style=filled, fillcolor="#ffffff"];
"DRN_Cmbt" [label="Combat Drones\nDRN_Cmbt", style=filled, fillcolor="#ffffff"];
"DRN_Auto" [label="Autonomous Drones\nDRN_Auto", style=filled, fillcolor="#ffffff"];
"DRN_Sats" [label="Drone Satellites\nDRN_Sats", style=filled, fillcolor="#ffffff"];
"DRN_Squad" [label="Drone Squadrons\nDRN_Squad", style=filled, fillcolor="#ffffff"];
"DRN_AdvFrm" [label="Advanced Drone Frames\nDRN_AdvFrm", style=filled, fillcolor="#ffffff"];
"DRN_WingMan" [label="Drone Wing Management\nDRN_WingMan", style=filled, fillcolor="#ffffff"];
"DRN_BtlRdrs" [label="Battle Riders\nDRN_BtlRdrs", style=filled, fillcolor="#ffffff"];
"DRN_COL" [label="COL\nDRN_COL", style=filled, fillcolor="#ffffff"];
"DRN_CryCOL" [label="CryBaby COL\nDRN_CryCOL", style=filled, fillcolor="#ffffff"];
"DRN_CrakCOL" [label="Cracker COL\nDRN_CrakCOL", style=filled, fillcolor="#ffffff"];
"DRN_TPCOL" [label="TarPit COL\nDRN_TPCOL", style=filled, fillcolor="#ffffff"];
"CCC_AI" [label="Artificial Intelligence\nCCC_AI", style=filled, fillcolor="#95a5a6"];
"CCC_AIAdmin" [label="AI Administration\nCCC_AIAdmin", style=filled, fillcolor="#95a5a6"];
"CCC_AIFac" [label="AI Factories\nCCC_AIFac", style=filled, fillcolor="#95a5a6"];
"CCC_AIFrCon" [label="AI Fire Control\nCCC_AIFrCon", style=filled, fillcolor="#95a5a6"];
"CCC_AIVrus" [label="AI Virus\nCCC_AIVrus", style=filled, fillcolor="#95a5a6"];
"CCC_AISlv" [label="AI Slaves\nCCC_AISlv", style=filled, fillcolor="#95a5a6"];
"CCC_TrnsHum" [label="Translate English\nCCC_TrnsHum", style=filled, fillcolor="#d35400"];
"XNC_TrnsHum2" [label="Translate Latin\nXNC_TrnsHum2", style=filled, fillcolor="#d35400"];
"XNC_IncHum" [label="Incorporate Human\nXNC_IncHum", style=filled, fillcolor="#d35400"];
"XNC_AdctHum" [label="Addict Human\nXNC_AdctHum", style=filled, fillcolor="#d35400"];
"XNC_TempHum" [label="Human Temperance\nXNC_TempHum", style=filled, fillcolor="#d35400"];
"XNC_TrnsHum3" [label="Translate Hanzi\nXNC_TrnsHum3", style=filled, fillcolor="#d35400"];
"XNC_SubHum" [label="Subjugate Human\nXNC_SubHum", style=filled, fillcolor="#d35400"];
"XNC_AccHum" [label="Accommodate Human\nXNC_AccHum", style=filled, fillcolor="#d35400"];
"XNC_ProfHum" [label="Proliferate Human\nXNC_ProfHum", style=filled, fillcolor="#d35400"];
"CCC_TrnsHvr" [label="Translate Ri’kap-ken\nCCC_TrnsHvr", style=filled, fillcolor="#d35400"];
"XNC_TrnsHvr2" [label="Translate K’en-Ken\nXNC_TrnsHvr2", style=filled, fillcolor="#d35400"];
"XNC_IncHvr" [label="Incorporate Hiver\nXNC_IncHvr", style=filled, fillcolor="#d35400"];
"XNC_AdctHvr" [label="Addict Hiver\nXNC_AdctHvr", style=filled, fillcolor="#d35400"];
"XNC_TempHvr" [label="Hiver Temperance\nXNC_TempHvr", style=filled, fillcolor="#d35400"];
"XNC_TrnsHvr3" [label="Translate Tcho’to-Ken\nXNC_TrnsHvr3", style=filled, fillcolor="#d35400"];
"XNC_SubHvr" [label="Subjugate Hiver\nXNC_SubHvr", style=filled, fillcolor="#d35400"];
"XNC_AccHvr" [label="Accommodate Hiver\nXNC_AccHvr", style=filled, fillcolor="#d35400"];
"XNC_ProfHvr" [label="Proliferate Hiver\nXNC_ProfHvr", style=filled, fillcolor="#d35400"];
"CCC_TrnsTrk" [label="Translate Urdu Kai\nCCC_TrnsTrk", style=filled, fillcolor="#d35400"];
"XNC_TrnsTrk2" [label="Translate Gutter Dialect\nXNC_TrnsTrk2", style=filled, fillcolor="#d35400"];
"XNC_IncTrk" [label="Incorporate Tarka\nXNC_IncTrk", style=filled, fillcolor="#d35400"];
"XNC_AdctTrk" [label="Addict Tarka\nXNC_AdctTrk", style=filled, fillcolor="#d35400"];
"XNC_TempTrk" [label="Tarka Temperance\nXNC_TempTrk", style=filled, fillcolor="#d35400"];
"XNC_TrnsTrk3" [label="Translate Kona Kai\nXNC_TrnsTrk3", style=filled, fillcolor="#d35400"];
"XNC_SubTrk" [label="Subjugate Tarka\nXNC_SubTrk", style=filled, fillcolor="#d35400"];
"XNC_AccTrk" [label="Accommodate Tarka\nXNC_AccTrk", style=filled, fillcolor="#d35400"];
"XNC_ProfTrk" [label="Proliferate Tarka\nXNC_ProfTrk", style=filled, fillcolor="#d35400"];
"CCC_TrnsLir" [label="Translate FleetSong\nCCC_TrnsLir", style=filled, fillcolor="#d35400"];
"XNC_TrnsLir2" [label="Translate SteelSong\nXNC_TrnsLir2", style=filled, fillcolor="#d35400"];
"XNC_IncLir" [label="Incorporate Liir\nXNC_IncLir", style=filled, fillcolor="#d35400"];
"XNC_AdctLir" [label="Addict Liir\nXNC_AdctLir", style=filled, fillcolor="#d35400"];
"XNC_TempLir" [label="Liir Temperance\nXNC_TempLir", style=filled, fillcolor="#d35400"];
"XNC_TrnsLir3" [label="MetaConcert\nXNC_TrnsLir3", style=filled, fillcolor="#d35400"];
"XNC_SubLir" [label="Subjugate Liir\nXNC_SubLir", style=filled, fillcolor="#d35400"];
"XNC_AccLir" [label="Accommodate Liir\nXNC_AccLir", style=filled, fillcolor="#d35400"];
"XNC_ProfLir" [label="Proliferate Liir\nXNC_ProfLir", style=filled, fillcolor="#d35400"];
"CCC_TrnsZul" [label="Translate Zuul\nCCC_TrnsZul", style=filled, fillcolor="#d35400"];
"XNC_TrnsZuul2" [label="Interrogate Zuul\nXNC_TrnsZuul2", style=filled, fillcolor="#d35400"];
"XNC_DomZuul" [label="Dominate Zuul\nXNC_DomZuul", style=filled, fillcolor="#d35400"];
"XNC_SubZuul" [label="Subjugate Zuul\nXNC_SubZuul", style=filled, fillcolor="#d35400"];
"CCC_TrnsMorr" [label="Translate Trade Creole\nCCC_TrnsMorr", style=filled, fillcolor="#d35400"];
"XNC_TrnsMorr2" [label="Translate Female Dialect\nXNC_TrnsMorr2", style=filled, fillcolor="#d35400"];
"XNC_IncMorr" [label="Incorporate Morrigi\nXNC_IncMorr", style=filled, fillcolor="#d35400"];
"XNC_AdctMorr" [label="Addict Morrigi\nXNC_AdctMorr", style=filled, fillcolor="#d35400"];
"XNC_TempMorr" [label="Morrigi Temperance\nXNC_TempMorr", style=filled, fillcolor="#d35400"];
"XNC_TrnsMorr3" [label="Translate Ancient Morrigi\nXNC_TrnsMorr3", style=filled, fillcolor="#d35400"];
"XNC_SubMorr" [label="Subjugate Morrigi\nXNC_SubMorr", style=filled, fillcolor="#d35400"];
"XNC_AccMorr" [label="Accommodate Morrigi\nXNC_AccMorr", style=filled, fillcolor="#d35400"];
"XNC_ProfMorr" [label="Proliferate Morrigi\nXNC_ProfMorr", style=filled, fillcolor="#d35400"];
"IND_ROOT" -> "IND_Waldo" [label="5000", fontsize=7];
"IND_ROOT" -> "IND_StlthArm" [label="0\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"EWP_ROOT" -> "WEP_RedLas" [label="0", fontsize=7];
"SLD_Root" -> "SLD_Def" [label="15000\nHu40 Zu20 Hi20 Ta75 Li90 Mo80", fontsize=7];
"NRG_Root" -> "DRV_Fissn" [label="0", fontsize=7];
"TRP_ROOT" -> "WEP_Dsrptr" [label="10000\nHu20 Zu90 Hi40 Ta80 Li90 Mo80", fontsize=7];
"TRP_ROOT" -> "WEP_PhotTrp" [label="15000\nHu90 Zu20 Hi30 Ta20 Li40 Mo50", fontsize=7];
"WHD_ROOT" -> "WEP_Nukes" [label="0", fontsize=7];
"BAL_ROOT" -> "WEP_GsDrvr" [label="0", fontsize=7];
"DRV_ROOT" -> "DRV_Node" [label="0\nHu100 Zu0 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_ROOT" -> "DRV_Hyper" [label="0\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"DRV_ROOT" -> "DRV_StrWrp" [label="0\nHu0 Zu0 Hi0 Ta0 Li100 Mo0", fontsize=7];
"DRV_ROOT" -> "DRV_TpGate" [label="0\nHu0 Zu0 Hi100 Ta0 Li0 Mo0", fontsize=7];
"DRV_ROOT" -> "DRV_Rip" [label="0\nHu0 Zu100 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_ROOT" -> "DRV_VdCtr" [label="0\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"BIO_ROOT" -> "BIO_GnMod" [label="4000", fontsize=7];
"CCC_ROOT" -> "CCC_FTLCom" [label="0", fontsize=7];
"DRN_ROOT" -> "IND_CyberInt" [label="12000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsHum" [label="2000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsLir" [label="2000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsTrk" [label="2000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsHvr" [label="2000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsZul" [label="2000", fontsize=7];
"XNC_ROOT" -> "CCC_TrnsMorr" [label="2000", fontsize=7];
"IND_Waldo" -> "IND_OrbFound" [label="10000", fontsize=7];
"IND_Waldo" -> "IND_RefCoat" [label="16000\nHu60 Zu20 Hi60 Ta70 Li95 Mo100", fontsize=7];
"IND_Waldo" -> "IND_TrkStl" [label="8000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"IND_TrkStl" -> "IND_PlyAlloy" [label="8000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"IND_RefCoat" -> "IND_ImpRfCt" [label="27000\nHu70 Zu20 Hi50 Ta50 Li90 Mo90", fontsize=7];
"IND_RefCoat" -> "IND_PlyAlloy" [label="16000\nHu70 Zu50 Hi90 Ta90 Li80 Mo75", fontsize=7];
"IND_RefCoat" -> "IND_StlthArm" [label="46000\nHu60 Zu70 Hi40 Ta40 Li80 Mo100", fontsize=7];
"IND_ImpRfCt" -> "IND_StlthArm" [label="40000\nHu80 Zu85 Hi40 Ta50 Li80 Mo100", fontsize=7];
"IND_ImpRfCt" -> "IND_HrdElec" [label="50000\nHu80 Zu65 Hi50 Ta70 Li90 Mo90", fontsize=7];
"IND_OrbFound" -> "IND_SlvgTech" [label="12000", fontsize=7];
"IND_OrbFound" -> "IND_CruisCon" [label="16000", fontsize=7];
"IND_OrbFound" -> "IND_SpnlMnt" [label="8000", fontsize=7];
"IND_OrbFound" -> "IND_PlyAlloy" [label="10000\nHu70 Zu40 Hi90 Ta90 Li80 Mo87", fontsize=7];
"IND_SlvgTech" -> "IND_AstMine" [label="30000", fontsize=7];
"IND_AstMine" -> "IND_MsMine" [label="30000\nHu60 Zu90 Hi90 Ta80 Li30 Mo90", fontsize=7];
"IND_AstMine" -> "IND_PlyAlloy" [label="15000\nHu70 Zu60 Hi80 Ta90 Li80 Mo80", fontsize=7];
"IND_MsMine" -> "IND_AtProc" [label="20000\nHu80 Zu40 Hi80 Ta90 Li50 Mo80", fontsize=7];
"IND_MsMine" -> "IND_MagLat" [label="30000\nHu50 Zu20 Hi60 Ta50 Li40 Mo40", fontsize=7];
"IND_MsMine" -> "IND_AstMon" [label="60000", fontsize=7];
"IND_PlyAlloy" -> "IND_MagLat" [label="60000\nHu70 Zu30 Hi80 Ta90 Li60 Mo65", fontsize=7];
"IND_PlyAlloy" -> "IND_ArcCon" [label="80000\nHu80 Zu10 Hi90 Ta70 Li70 Mo50", fontsize=7];
"IND_MagLat" -> "IND_QrkRes" [label="110000\nHu60 Zu30 Hi70 Ta80 Li50 Mo40", fontsize=7];
"IND_MagLat" -> "IND_EleNans" [label="60000\nHu70 Zu30 Hi60 Ta50 Li90 Mo80", fontsize=7];
"IND_QrkRes" -> "IND_AdmAly" [label="170000\nHu40 Zu20 Hi50 Ta40 Li30 Mo20", fontsize=7];
"IND_EleNans" -> "BIO_NanVir" [label="110000\nHu50 Zu0 Hi70 Ta40 Li80 Mo40", fontsize=7];
"IND_CruisCon" -> "IND_OrbDry" [label="30000", fontsize=7];
"IND_CruisCon" -> "WEP_HCLas" [label="20000\nHu90 Zu90 Hi90 Ta90 Li90 Mo90", fontsize=7];
"IND_CruisCon" -> "IND_AtProc" [label="30000\nHu70 Zu30 Hi70 Ta90 Li20 Mo50", fontsize=7];
"IND_CruisCon" -> "IND_BrdPod" [label="30000\nHu80 Zu100 Hi80 Ta90 Li50 Mo60", fontsize=7];
"IND_CruisCon" -> "IND_MegFrght" [label="50000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"IND_MegFrght" -> "IND_ModCon" [label="10000\nHu100 Zu0 Hi85 Ta95 Li45 Mo100", fontsize=7];
"IND_OrbDry" -> "IND_HvyPlat" [label="60000", fontsize=7];
"IND_OrbDry" -> "IND_Decon" [label="20000\nHu90 Zu20 Hi70 Ta50 Li95 Mo80", fontsize=7];
"IND_OrbDry" -> "IND_DSCon" [label="15000", fontsize=7];
"IND_DSCon" -> "IND_OrbCom" [label="15000", fontsize=7];
"IND_HvyPlat" -> "IND_DreadCon" [label="180000", fontsize=7];
"IND_HvyPlat" -> "IND_TrctBm" [label="50000\nHu80 Zu100 Hi70 Ta70 Li90 Mo100", fontsize=7];
"IND_HvyPlat" -> "IND_TrpSat" [label="30000\nHu80 Zu60 Hi90 Ta85 Li100 Mo70", fontsize=7];
"IND_DreadCon" -> "IND_AdvDreadEng" [label="120000", fontsize=7];
"IND_GravCon" -> "WEP_GrvMine" [label="80000\nHu80 Zu90 Hi80 Ta80 Li70 Mo100", fontsize=7];
"IND_GravCon" -> "WEP_ThmpDrvr" [label="65000\nHu50 Zu90 Hi50 Ta60 Li80 Mo90", fontsize=7];
"IND_TrctBm" -> "IND_GravCon" [label="140000\nHu50 Zu100 Hi60 Ta40 Li80 Mo100", fontsize=7];
"IND_ArcCon" -> "BIO_Btrans" [label="20000\nHu80 Zu30 Hi90 Ta70 Li90 Mo90", fontsize=7];
"IND_ArcCon" -> "IND_HrdStrct" [label="100000\nHu75 Zu20 Hi85 Ta70 Li90 Mo40", fontsize=7];
"WEP_RedLas" -> "WEP_GrnLas" [label="5000", fontsize=7];
"WEP_RedLas" -> "WEP_PlsmCan" [label="10000", fontsize=7];
"WEP_RedLas" -> "WEP_PrtBm" [label="18000", fontsize=7];
"WEP_RedLas" -> "WEP_UvLas" [label="22000\nHu30 Zu10 Hi20 Ta30 Li90 Mo90", fontsize=7];
"WEP_RedLas" -> "WEP_LtEmitr" [label="15000\nHu60 Zu20 Hi30 Ta40 Li90 Mo55", fontsize=7];
"WEP_GrnLas" -> "WEP_UvLas" [label="19000", fontsize=7];
"WEP_GrnLas" -> "WEP_GrnBmr" [label="10000\nHu40 Zu10 Hi20 Ta30 Li85 Mo100", fontsize=7];
"WEP_UvLas" -> "WEP_HCLas" [label="35000", fontsize=7];
"WEP_UvLas" -> "WEP_XryLas" [label="42000\nHu60 Zu30 Hi60 Ta50 Li90 Mo80", fontsize=7];
"WEP_XryLas" -> "WEP_HCLas" [label="20000\nHu80 Zu80 Hi70 Ta70 Li90 Mo80", fontsize=7];
"WEP_XryLas" -> "WEP_Phas" [label="55000\nHu60 Zu20 Hi40 Ta40 Li80 Mo95", fontsize=7];
"WEP_GrnBmr" -> "WEP_UvBmr" [label="25000\nHu60 Zu30 Hi40 Ta50 Li80 Mo90", fontsize=7];
"WEP_UvBmr" -> "WEP_Phas" [label="40000\nHu70 Zu40 Hi50 Ta50 Li90 Mo95", fontsize=7];
"WEP_LtEmitr" -> "WEP_Emitr" [label="38000\nHu60 Zu20 Hi30 Ta40 Li90 Mo80", fontsize=7];
"WEP_Emitr" -> "WEP_HvyEmitr" [label="85000\nHu40 Zu20 Hi20 Ta30 Li80 Mo70", fontsize=7];
"WEP_PlsmCan" -> "WEP_FusCan" [label="40000\nHu80 Zu50 Hi70 Ta80 Li90 Mo80", fontsize=7];
"WEP_PlsmCan" -> "WEP_PlsmTrp" [label="40000\nHu40 Zu40 Hi50 Ta60 Li80 Mo70", fontsize=7];
"WEP_PlsmCan" -> "WEP_PlsmProj" [label="50000\nHu60 Zu20 Hi30 Ta50 Li80 Mo70", fontsize=7];
"WEP_PlsmCan" -> "WEP_HvyPCan" [label="40000\nHu70 Zu30 Hi40 Ta70 Li90 Mo80", fontsize=7];
"WEP_PlsmCan" -> "WEP_PolPlsm" [label="35000\nHu75 Zu30 Hi40 Ta50 Li95 Mo90", fontsize=7];
"WEP_FusCan" -> "WEP_AmCan" [label="130000\nHu70 Zu40 Hi60 Ta60 Li90 Mo70", fontsize=7];
"WEP_FusCan" -> "WEP_FusTrp" [label="70000\nHu40 Zu30 Hi50 Ta70 Li60 Mo60", fontsize=7];
"WEP_FusCan" -> "WEP_FusProj" [label="130000\nHu45 Zu20 Hi25 Ta40 Li80 Mo50", fontsize=7];
"WEP_FusCan" -> "WEP_HvyFCan" [label="90000\nHu50 Zu30 Hi40 Ta60 Li90 Mo70", fontsize=7];
"WEP_AmCan" -> "WEP_AMProj" [label="190000\nHu25 Zu10 Hi15 Ta20 Li80 Mo50", fontsize=7];
"WEP_AmCan" -> "WEP_HvyACan" [label="120000\nHu50 Zu20 Hi40 Ta60 Li90 Mo60", fontsize=7];
"WEP_HvyPCan" -> "WEP_HvyFCan" [label="60000\nHu60 Zu20 Hi30 Ta60 Li90 Mo80", fontsize=7];
"WEP_HvyFCan" -> "WEP_HvyACan" [label="110000\nHu30 Zu10 Hi30 Ta40 Li70 Mo60", fontsize=7];
"WEP_PolPlsm" -> "WEP_PolFus" [label="55000\nHu70 Zu20 Hi30 Ta50 Li95 Mo90", fontsize=7];
"WEP_PolFus" -> "WEP_PolAm" [label="95000\nHu60 Zu20 Hi30 Ta40 Li85 Mo75", fontsize=7];
"WEP_PlsmProj" -> "WEP_FusProj" [label="100000\nHu30 Zu20 Hi40 Ta30 Li80 Mo50", fontsize=7];
"WEP_FusProj" -> "WEP_AMProj" [label="160000\nHu25 Zu10 Hi15 Ta20 Li80 Mo70", fontsize=7];
"WEP_FusProj" -> "SLD_Proj" [label="100000\nHu60 Zu30 Hi30 Ta50 Li80 Mo90", fontsize=7];
"WEP_AmProj" -> "WEP_MesProj" [label="200000\nHu25 Zu10 Hi20 Ta30 Li60 Mo50", fontsize=7];
"WEP_Phas" -> "WEP_Lancer" [label="95000\nHu70 Zu30 Hi70 Ta70 Li80 Mo70", fontsize=7];
"WEP_Phas" -> "WEP_PlsPhas" [label="110000\nHu50 Zu20 Hi30 Ta35 Li80 Mo90", fontsize=7];
"WEP_Phas" -> "WEP_PDPhas" [label="95000\nHu60 Zu10 Hi40 Ta50 Li90 Mo95", fontsize=7];
"WEP_PDPhas" -> "WEP_PlsPhas" [label="90000\nHu50 Zu20 Hi35 Ta40 Li90 Mo95", fontsize=7];
"WEP_HCLas" -> "WEP_Lancer" [label="120000\nHu75 Zu50 Hi70 Ta80 Li90 Mo80", fontsize=7];
"WEP_Lancer" -> "WEP_CtngBm" [label="280000\nHu75 Zu30 Hi60 Ta50 Li90 Mo80", fontsize=7];
"WEP_PrtBm" -> "WEP_Phas" [label="95000\nHu60 Zu20 Hi50 Ta40 Li80 Mo95", fontsize=7];
"WEP_PrtBm" -> "WEP_NeutBm" [label="65000\nHu80 Zu40 Hi70 Ta70 Li90 Mo90", fontsize=7];
"WEP_NeutBm" -> "WEP_PosiBm" [label="120000\nHu70 Zu30 Hi60 Ta50 Li90 Mo90", fontsize=7];
"WEP_PosiBm" -> "WEP_MesBm" [label="180000\nHu60 Zu10 Hi50 Ta40 Li80 Mo70", fontsize=7];
"WEP_MesBm" -> "WEP_GravBm" [label="165000\nHu50 Zu70 Hi30 Ta30 Li70 Mo100", fontsize=7];
"WEP_MesBm" -> "WEP_CtngBm" [label="240000\nHu40 Zu20 Hi30 Ta20 Li60 Mo55", fontsize=7];
"WEP_MesBm" -> "WEP_MesProj" [label="190000\nHu55 Zu10 Hi20 Ta35 Li85 Mo75", fontsize=7];
"WEP_GravBm" -> "WEP_PulGrvBm" [label="165000\nHu50 Zu70 Hi30 Ta30 Li70 Mo100", fontsize=7];
"WEP_Dsrptr" -> "WEP_EmPulse" [label="50000\nHu50 Zu90 Hi50 Ta70 Li90 Mo80", fontsize=7];
"WEP_Dsrptr" -> "WEP_PhotTrp" [label="25000\nHu80 Zu40 Hi30 Ta20 Li70 Mo80", fontsize=7];
"WEP_Dsrptr" -> "WEP_DsrptrWhp" [label="15000\nHu0 Zu90 Hi0 Ta0 Li0 Mo0", fontsize=7];
"WEP_EmPulse" -> "WEP_PlsTrp" [label="30000\nHu50 Zu90 Hi40 Ta70 Li90 Mo85", fontsize=7];
"WEP_EmPulse" -> "WEP_Emitr" [label="25000\nHu70 Zu40 Hi60 Ta50 Li90 Mo75", fontsize=7];
"WEP_EmPulse" -> "WEP_IntCan" [label="45000\nHu45 Zu10 Hi30 Ta35 Li75 Mo85", fontsize=7];
"WEP_IntCan" -> "WEP_HvyIntCan" [label="50000\nHu50 Zu20 Hi40 Ta50 Li90 Mo95", fontsize=7];
"WEP_PhotTrp" -> "WEP_PlsmTrp" [label="30000\nHu20 Zu30 Hi20 Ta40 Li30 Mo50", fontsize=7];
"WEP_PhotTrp" -> "WEP_GluTrp" [label="80000\nHu85 Zu25 Hi15 Ta30 Li50 Mo90", fontsize=7];
"WEP_GluTrp" -> "WEP_MesTrp" [label="190000\nHu75 Zu10 Hi20 Ta25 Li30 Mo80", fontsize=7];
"WEP_GluTrp" -> "WEP_KelTrp" [label="150000\nHu65 Zu10 Hi50 Ta35 Li90 Mo70", fontsize=7];
"WEP_PlsmTrp" -> "WEP_FusTrp" [label="65000\nHu50 Zu40 Hi40 Ta70 Li80 Mo75", fontsize=7];
"WEP_FusTrp" -> "WEP_DtFsTrp" [label="45000\nHu50 Zu40 Hi40 Ta80 Li60 Mo75", fontsize=7];
"WEP_DtFsTrp" -> "WEP_AmTrp" [label="150000\nHu50 Zu30 Hi40 Ta80 Li60 Mo70", fontsize=7];
"WEP_AmTrp" -> "WEP_DtAmTrp" [label="90000\nHu60 Zu20 Hi50 Ta80 Li70 Mo70", fontsize=7];
"WEP_AmTrp" -> "WEP_AmCan" [label="90000\nHu60 Zu30 Hi60 Ta70 Li80 Mo70", fontsize=7];
"WEP_Nukes" -> "WEP_NukeWhd" [label="7000", fontsize=7];
"WEP_Nukes" -> "WEP_NukMine" [label="9000", fontsize=7];
"WEP_Nukes" -> "WEP_CorMsl" [label="35000", fontsize=7];
"WEP_Nukes" -> "WEP_DFMsl" [label="11000\nHu80 Zu90 Hi80 Ta90 Li60 Mo70", fontsize=7];
"WEP_NukeWhd" -> "WEP_GmaWhd" [label="10000\nHu90 Zu50 Hi90 Ta90 Li20 Mo70", fontsize=7];
"WEP_NukeWhd" -> "WEP_HvyPMsl" [label="10000\nHu95 Zu80 Hi90 Ta90 Li75 Mo70", fontsize=7];
"WEP_GmaWhd" -> "WEP_FusWhd" [label="25000\nHu80 Zu30 Hi80 Ta90 Li50 Mo70", fontsize=7];
"WEP_GmaWhd" -> "WEP_HvyPMsl" [label="10000\nHu90 Zu50 Hi90 Ta90 Li20 Mo70", fontsize=7];
"WEP_FusWhd" -> "WEP_FusMine" [label="20000", fontsize=7];
"WEP_FusWhd" -> "WEP_AmWhd" [label="90000\nHu60 Zu10 Hi70 Ta80 Li50 Mo50", fontsize=7];
"WEP_NukMine" -> "WEP_FusMine" [label="25000\nHu80 Zu40 Hi80 Ta90 Li40 Mo80", fontsize=7];
"WEP_FusMine" -> "WEP_LpMine" [label="30000\nHu90 Zu60 Hi80 Ta90 Li10 Mo90", fontsize=7];
"WEP_FusMine" -> "WEP_AmMine" [label="85000\nHu80 Zu60 Hi90 Ta70 Li20 Mo80", fontsize=7];
"WEP_AmMine" -> "WEP_GrvMine" [label="50000\nHu60 Zu90 Hi60 Ta50 Li60 Mo100", fontsize=7];
"WEP_GrvMine" -> "WEP_ImpMine" [label="120000\nHu45 Zu75 Hi45 Ta40 Li30 Mo90", fontsize=7];
"WEP_CorMsl" -> "WEP_NanMsl" [label="70000\nHu75 Zu80 Hi60 Ta60 Li20 Mo50", fontsize=7];
"WEP_GsDrvr" -> "WEP_VRFtech" [label="7000", fontsize=7];
"WEP_GsDrvr" -> "WEP_MasDrvr" [label="10000", fontsize=7];
"WEP_GsDrvr" -> "WEP_SnpCanDrvr" [label="15000\nHu70 Zu60 Hi90 Ta85 Li50 Mo40", fontsize=7];
"WEP_MasDrvr" -> "WEP_HvyDrvr" [label="45000", fontsize=7];
"WEP_MasDrvr" -> "WEP_BrstrDrvr" [label="60000\nHu70 Zu80 Hi90 Ta85 Li50 Mo40", fontsize=7];
"WEP_MasDrvr" -> "WEP_APRtech" [label="30000\nHu80 Zu90 Hi90 Ta85 Li50 Mo40", fontsize=7];
"WEP_MasDrvr" -> "WEP_StrmDrvr" [label="35000\nHu80 Zu90 Hi90 Ta85 Li50 Mo40", fontsize=7];
"WEP_StrmDrvr" -> "WEP_HStrmDrvr" [label="55000\nHu80 Zu75 Hi90 Ta90 Li50 Mo40", fontsize=7];
"WEP_HvyDrvr" -> "WEP_NeutRnd" [label="125000\nHu60 Zu30 Hi80 Ta75 Li20 Mo30", fontsize=7];
"WEP_HvyDrvr" -> "WEP_SgeDrvr" [label="160000\nHu60 Zu30 Hi90 Ta85 Li40 Mo30", fontsize=7];
"WEP_HvyDrvr" -> "WEP_ShldDrvr" [label="80000\nHu60 Zu80 Hi50 Ta50 Li90 Mo95", fontsize=7];
"WEP_HvyDrvr" -> "WEP_ErgDrain" [label="40000\nHu70 Zu90 Hi50 Ta50 Li90 Mo95", fontsize=7];
"WEP_HvyDrvr" -> "WEP_AccAmp" [label="120000\nHu90 Zu80 Hi100 Ta100 Li75 Mo80", fontsize=7];
"WEP_VRFtech" -> "WEP_PDtech" [label="15000\nHu90 Zu75 Hi90 Ta90 Li95 Mo100", fontsize=7];
"WEP_VRFtech" -> "WEP_StrmDrvr" [label="25000\nHu80 Zu90 Hi90 Ta85 Li50 Mo40", fontsize=7];
"WEP_BrstrDrvr" -> "WEP_MasShtDrvr" [label="50000\nHu60 Zu30 Hi90 Ta85 Li40 Mo30", fontsize=7];
"WEP_SgeDrvr" -> "WEP_ThmpDrvr" [label="100000\nHu50 Zu95 Hi50 Ta40 Li90 Mo95", fontsize=7];
"WEP_NeutRnd" -> "WEP_KKMsl" [label="100000\nHu75 Zu50 Hi90 Ta80 Li40 Mo80", fontsize=7];
"DRV_Fissn" -> "DRV_Fusn" [label="85000", fontsize=7];
"DRV_Fissn" -> "DRV_PlsFiss" [label="5000\nHu100 Zu100 Hi100 Ta100 Li0 Mo0", fontsize=7];
"DRV_Fissn" -> "DRV_RecFiss" [label="5000\nHu50 Zu60 Hi70 Ta90 Li95 Mo85", fontsize=7];
"DRV_Fissn" -> "DRV_OvrThrust" [label="25000\nHu0 Zu0 Hi0 Ta0 Li95 Mo95", fontsize=7];
"DRV_PlsFiss" -> "DRV_LRFiss" [label="10000\nHu0 Zu0 Hi100 Ta0 Li0 Mo0", fontsize=7];
"DRV_PlsFiss" -> "DRV_OvrThrust" [label="15000\nHu100 Zu100 Hi100 Ta100 Li0 Mo0", fontsize=7];
"DRV_LRFiss" -> "DRV_RecFiss" [label="3000\nHu0 Zu0 Hi90 Ta0 Li0 Mo0", fontsize=7];
"DRV_Fusn" -> "DRV_AntiMat" [label="325000", fontsize=7];
"DRV_Fusn" -> "DRV_McroFus" [label="20000", fontsize=7];
"DRV_Fusn" -> "WEP_FusWhd" [label="15000", fontsize=7];
"DRV_Fusn" -> "SLD_MkOne" [label="15000\nHu40 Zu20 Hi30 Ta40 Li90 Mo100", fontsize=7];
"DRV_Fusn" -> "SLD_Clk" [label="120000\nHu40 Zu80 Hi30 Ta50 Li70 Mo90", fontsize=7];
"DRV_Fusn" -> "DRV_SmlFus" [label="20000\nHu80 Zu90 Hi90 Ta90 Li70 Mo90", fontsize=7];
"DRV_Fusn" -> "DRV_PlsmFoc" [label="10000\nHu70 Zu60 Hi90 Ta90 Li95 Mo90", fontsize=7];
"DRV_Fusn" -> "DRV_LRFusn" [label="10000\nHu0 Zu0 Hi90 Ta0 Li0 Mo0", fontsize=7];
"DRV_Fusn" -> "WEP_PlsmTrp" [label="10000\nHu40 Zu30 Hi40 Ta60 Li65 Mo50", fontsize=7];
"DRV_Fusn" -> "WEP_FusTrp" [label="30000\nHu30 Zu20 Hi30 Ta55 Li90 Mo80", fontsize=7];
"DRV_McroFus" -> "WEP_LpMine" [label="10000\nHu80 Zu90 Hi80 Ta60 Li10 Mo90", fontsize=7];
"DRV_McroFus" -> "WEP_MWMsl" [label="80000\nHu80 Zu40 Hi60 Ta50 Li30 Mo90", fontsize=7];
"DRV_McroFus" -> "DRV_Ints" [label="70000\nHu85 Zu50 Hi80 Ta80 Li55 Mo70", fontsize=7];
"DRV_SmlFus" -> "DRV_IncThrst" [label="15000\nHu80 Zu40 Hi80 Ta60 Li50 Mo90", fontsize=7];
"DRV_LRFusn" -> "DRV_Rmscps" [label="10000\nHu0 Zu0 Hi90 Ta0 Li0 Mo0", fontsize=7];
"DRV_AntiMat" -> "SLD_MkThree" [label="75000\nHu40 Zu20 Hi25 Ta30 Li90 Mo80", fontsize=7];
"DRV_AntiMat" -> "SLD_ErgAb" [label="225000\nHu40 Zu10 Hi25 Ta30 Li85 Mo90", fontsize=7];
"DRV_AntiMat" -> "IND_TrctBm" [label="50000\nHu80 Zu90 Hi80 Ta60 Li90 Mo100", fontsize=7];
"DRV_AntiMat" -> "WEP_AmTrp" [label="90000\nHu40 Zu20 Hi40 Ta60 Li85 Mo80", fontsize=7];
"DRV_AntiMat" -> "DRV_QntCap" [label="140000\nHu70 Zu20 Hi40 Ta55 Li95 Mo95", fontsize=7];
"DRV_QntCap" -> "SLD_Magni" [label="150000\nHu60 Zu20 Hi40 Ta50 Li85 Mo90", fontsize=7];
"DRV_Node" -> "DRV_NodFoc" [label="25000\nHu100 Zu0 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_NodFoc" -> "DRV_NodPath" [label="150000\nHu90 Zu0 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_NodFoc" -> "WEP_NdMsl" [label="180000\nHu70 Zu0 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_Rip" -> "DRV_Rend" [label="35000\nHu0 Zu100 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_Rend" -> "DRV_Rad" [label="130000\nHu0 Zu100 Hi0 Ta0 Li0 Mo0", fontsize=7];
"DRV_VdCtr" -> "DRV_VdCrv" [label="35000\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"DRV_VdCrv" -> "DRV_VdMstr" [label="120000\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"DRV_VdMstr" -> "DRV_GrvSyn" [label="100000\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"DRV_Hyper" -> "DRV_HyprFld" [label="30000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"DRV_HyprFld" -> "DRV_Warp" [label="130000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"DRV_StrWrp" -> "DRV_ImpStWrp" [label="30000\nHu0 Zu0 Hi0 Ta0 Li100 Mo0", fontsize=7];
"DRV_ImpStWrp" -> "DRV_Flicker" [label="90000\nHu0 Zu0 Hi0 Ta0 Li100 Mo0", fontsize=7];
"DRV_Flicker" -> "DRV_McrFlkr" [label="95000\nHu0 Zu0 Hi0 Ta0 Li75 Mo0", fontsize=7];
"DRV_Flicker" -> "SLD_Intang" [label="130000\nHu0 Zu0 Hi0 Ta0 Li70 Mo0", fontsize=7];
"DRV_TpGate" -> "DRV_GatAmp" [label="50000\nHu0 Zu0 Hi100 Ta0 Li0 Mo0", fontsize=7];
"DRV_GatAmp" -> "DRV_FarCast" [label="180000\nHu0 Zu0 Hi100 Ta0 Li0 Mo0", fontsize=7];
"DRV_FarCast" -> "SLD_Intang" [label="150000\nHu0 Zu0 Hi50 Ta0 Li0 Mo0", fontsize=7];
"BIO_GnMod" -> "BIO_Plg" [label="35000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"BIO_GnMod" -> "BIO_AtmoAd" [label="13000", fontsize=7];
"BIO_GnMod" -> "BIO_SpndAni" [label="4000\nHu90 Zu50 Hi100 Ta90 Li100 Mo100", fontsize=7];
"BIO_SpndAni" -> "BIO_Btrans" [label="20000\nHu80 Zu20 Hi90 Ta70 Li100 Mo90", fontsize=7];
"BIO_AtmoAd" -> "BIO_EnvTail" [label="30000\nHu70 Zu40 Hi80 Ta60 Li100 Mo90", fontsize=7];
"BIO_AtmoAd" -> "BIO_TerBac" [label="15000\nHu80 Zu20 Hi75 Ta60 Li90 Mo80", fontsize=7];
"BIO_TerBac" -> "IND_EleNans" [label="65000\nHu70 Zu20 Hi75 Ta70 Li90 Mo55", fontsize=7];
"BIO_EnvTail" -> "BIO_GrvAdpt" [label="75000\nHu50 Zu30 Hi60 Ta50 Li90 Mo100", fontsize=7];
"BIO_Plg" -> "BIO_PlgVac" [label="10000\nHu80 Zu0 Hi80 Ta80 Li95 Mo85", fontsize=7];
"BIO_Plg" -> "BIO_RtPlg" [label="45000\nHu70 Zu0 Hi70 Ta60 Li90 Mo70", fontsize=7];
"BIO_Plg" -> "BIO_Bst" [label="60000\nHu30 Zu0 Hi30 Ta25 Li80 Mo30", fontsize=7];
"BIO_Plg" -> "BIO_TerBac" [label="20000\nHu50 Zu0 Hi45 Ta40 Li90 Mo50", fontsize=7];
"BIO_PlgVac" -> "BIO_UniAnti" [label="120000\nHu5 Zu0 Hi5 Ta5 Li10 Mo5", fontsize=7];
"BIO_RtPlg" -> "BIO_RtPlgVac" [label="20000\nHu70 Zu0 Hi70 Ta70 Li90 Mo75", fontsize=7];
"BIO_RtPlg" -> "BIO_Bst" [label="40000\nHu50 Zu0 Hi60 Ta50 Li70 Mo60", fontsize=7];
"BIO_RtPlg" -> "BIO_AsPlg" [label="90000\nHu30 Zu0 Hi30 Ta45 Li80 Mo40", fontsize=7];
"BIO_RtPlgVac" -> "BIO_UniAnti" [label="110000\nHu10 Zu0 Hi10 Ta10 Li25 Mo15", fontsize=7];
"BIO_Bst" -> "BIO_BstVac" [label="30000\nHu70 Zu0 Hi70 Ta70 Li90 Mo70", fontsize=7];
"BIO_Bst" -> "BIO_AsPlg" [label="80000\nHu30 Zu0 Hi30 Ta25 Li60 Mo30", fontsize=7];
"BIO_BstVac" -> "BIO_UniAnti" [label="90000\nHu15 Zu0 Hi15 Ta15 Li40 Mo30", fontsize=7];
"BIO_AsPlg" -> "BIO_AsPlgVac" [label="50000\nHu35 Zu0 Hi35 Ta45 Li70 Mo50", fontsize=7];
"BIO_AsPlgVac" -> "BIO_UniAnti" [label="70000\nHu25 Zu0 Hi25 Ta35 Li70 Mo40", fontsize=7];
"BIO_NanVir" -> "BIO_ConNan" [label="120000\nHu50 Zu0 Hi40 Ta40 Li60 Mo80", fontsize=7];
"BIO_NanVir" -> "BIO_SmrtNan" [label="90000\nHu50 Zu30 Hi40 Ta40 Li60 Mo80", fontsize=7];
"BIO_ConNan" -> "BIO_SmrtNan" [label="70000\nHu70 Zu10 Hi50 Ta60 Li70 Mo90", fontsize=7];
"CCC_FTLCom" -> "CCC_FTLBrdB" [label="4000", fontsize=7];
"CCC_FTLCom" -> "CCC_BtlCmp" [label="10000", fontsize=7];
"CCC_FTLCom" -> "CCC_HypCom" [label="10000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"CCC_BtlCmp" -> "CCC_IntSens" [label="12000", fontsize=7];
"CCC_BtlCmp" -> "CCC_DatSyn" [label="20000", fontsize=7];
"CCC_BtlCmp" -> "CCC_SnsJam" [label="10000\nHu80 Zu100 Hi70 Ta90 Li80 Mo100", fontsize=7];
"CCC_FTLBrdB" -> "CCC_SpyBm" [label="12000\nHu80 Zu100 Hi80 Ta90 Li90 Mo100", fontsize=7];
"CCC_FTLBrdB" -> "CCC_SpJam" [label="12000\nHu70 Zu90 Hi70 Ta80 Li80 Mo100", fontsize=7];
"CCC_FTLBrdB" -> "CCC_FTLEcon" [label="18000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_FTLBrdB" -> "CCC_ComRaid" [label="5000\nHu0 Zu100 Hi0 Ta0 Li0 Mo0", fontsize=7];
"CCC_FTLEcon" -> "CCC_ComRaid" [label="12000\nHu100 Zu0 Hi0 Ta100 Li100 Mo100", fontsize=7];
"CCC_SpyBm" -> "CCC_SpJam" [label="10000\nHu90 Zu90 Hi90 Ta90 Li90 Mo0", fontsize=7];
"CCC_SpyBm" -> "CCC_NdTrkZul" [label="20000\nHu90 Zu100 Hi90 Ta90 Li90 Mo100", fontsize=7];
"CCC_SpyBm" -> "CCC_NdTrkHum" [label="20000\nHu100 Zu90 Hi90 Ta90 Li90 Mo100", fontsize=7];
"CCC_NdTrkHum" -> "CCC_NdTrkZul" [label="5000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_NdTrkZul" -> "CCC_NdTrkHum" [label="5000\nHu0 Zu100 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_SnsJam" -> "CCC_QntChaf" [label="50000\nHu60 Zu95 Hi50 Ta50 Li80 Mo90", fontsize=7];
"CCC_QntChaf" -> "SLD_Clk" [label="70000\nHu40 Zu80 Hi30 Ta70 Li70 Mo100", fontsize=7];
"CCC_IntSens" -> "CCC_AdvCnC" [label="20000", fontsize=7];
"CCC_IntSens" -> "CCC_AdvSens" [label="20000", fontsize=7];
"CCC_IntSens" -> "CCC_DatCor" [label="15000", fontsize=7];
"CCC_AdvSens" -> "CCC_QntChaf" [label="40000\nHu60 Zu80 Hi50 Ta50 Li90 Mo95", fontsize=7];
"CCC_AdvSens" -> "CCC_TunSens" [label="20000\nHu100 Zu100 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_AdvSens" -> "CCC_ScanSats" [label="20000\nHu100 Zu100 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_DatSyn" -> "CCC_ArmCom" [label="125000", fontsize=7];
"CCC_DatSyn" -> "CCC_QntChaf" [label="30000\nHu30 Zu60 Hi20 Ta40 Li40 Mo95", fontsize=7];
"CCC_DatSyn" -> "CCC_CmbtAlg" [label="70000\nHu80 Zu90 Hi70 Ta80 Li90 Mo95", fontsize=7];
"CCC_CmbtAlg" -> "CCC_AI" [label="140000\nHu40 Zu20 Hi40 Ta30 Li50 Mo100", fontsize=7];
"CCC_CmbtAlg" -> "CCC_HoloTac" [label="75000\nHu80 Zu90 Hi70 Ta80 Li90 Mo95", fontsize=7];
"CCC_ArmCom" -> "CCC_HoloTac" [label="80000\nHu70 Zu90 Hi60 Ta70 Li90 Mo95", fontsize=7];
"CCC_ArmCom" -> "CCC_FCCom" [label="125000", fontsize=7];
"CCC_HoloTac" -> "CCC_AI" [label="120000\nHu50 Zu20 Hi50 Ta40 Li60 Mo90", fontsize=7];
"SLD_Def" -> "SLD_MkOne" [label="15000\nHu50 Zu20 Hi40 Ta70 Li95 Mo100", fontsize=7];
"SLD_Def" -> "SLD_Disr" [label="20000\nHu50 Zu20 Hi40 Ta70 Li95 Mo95", fontsize=7];
"SLD_Def" -> "WEP_IntCan" [label="55000\nHu45 Zu10 Hi30 Ta35 Li55 Mo65", fontsize=7];
"SLD_Clk" -> "SLD_ImpClk" [label="190000\nHu30 Zu50 Hi20 Ta30 Li70 Mo90", fontsize=7];
"SLD_Clk" -> "WEP_ClkMine" [label="120000\nHu20 Zu50 Hi30 Ta60 Li50 Mo90", fontsize=7];
"SLD_ImpClk" -> "SLD_Intang" [label="120000\nHu10 Zu10 Hi10 Ta20 Li50 Mo50", fontsize=7];
"SLD_ImpClk" -> "WEP_ClkMine" [label="90000\nHu60 Zu80 Hi70 Ta80 Li60 Mo70", fontsize=7];
"SLD_MkOne" -> "SLD_MkTwo" [label="20000\nHu50 Zu30 Hi40 Ta70 Li95 Mo100", fontsize=7];
"SLD_MkOne" -> "WEP_ShldDrvr" [label="35000\nHu50 Zu80 Hi60 Ta60 Li80 Mo90", fontsize=7];
"SLD_MkTwo" -> "SLD_MkThree" [label="70000\nHu50 Zu30 Hi40 Ta50 Li90 Mo90", fontsize=7];
"SLD_MkTwo" -> "SLD_Magni" [label="120000\nHu40 Zu10 Hi20 Ta30 Li70 Mo75", fontsize=7];
"SLD_MkThree" -> "SLD_MkFour" [label="110000\nHu30 Zu10 Hi20 Ta40 Li80 Mo90", fontsize=7];
"SLD_MkThree" -> "SLD_Magni" [label="100000\nHu65 Zu20 Hi30 Ta50 Li90 Mo95", fontsize=7];
"SLD_Magni" -> "SLD_MkFour" [label="95000\nHu70 Zu25 Hi30 Ta60 Li85 Mo90", fontsize=7];
"SLD_ErgAb" -> "SLD_MesShld" [label="190000\nHu40 Zu10 Hi30 Ta50 Li85 Mo90", fontsize=7];
"SLD_MesShld" -> "SLD_MkFour" [label="75000\nHu30 Zu30 Hi20 Ta50 Li90 Mo90", fontsize=7];
"SLD_MesShld" -> "SLD_GrvShld" [label="150000\nHu20 Zu10 Hi20 Ta40 Li80 Mo90", fontsize=7];
"SLD_Proj" -> "SLD_MkFour" [label="120000\nHu70 Zu25 Hi30 Ta60 Li85 Mo90", fontsize=7];
"SLD_Proj" -> "SLD_Focus" [label="150000\nHu0 Zu0 Hi0 Ta0 Li100 Mo0", fontsize=7];
"IND_CyberInt" -> "IND_PredGun" [label="17000\nHu70 Zu30 Hi80 Ta90 Li95 Mo95", fontsize=7];
"IND_CyberInt" -> "IND_ExpSys" [label="16000\nHu90 Zu40 Hi75 Ta85 Li95 Mo95", fontsize=7];
"IND_CyberInt" -> "DRN_AdvRob" [label="12000", fontsize=7];
"IND_ExpSys" -> "CCC_AI" [label="120000\nHu40 Zu30 Hi70 Ta50 Li30 Mo80", fontsize=7];
"DRN_AdvRob" -> "DRN_Cmbt" [label="60000", fontsize=7];
"DRN_Cmbt" -> "DRN_COL" [label="90000", fontsize=7];
"DRN_Cmbt" -> "DRN_Squad" [label="75000", fontsize=7];
"DRN_Cmbt" -> "DRN_Sats" [label="35000", fontsize=7];
"DRN_Cmbt" -> "DRN_Auto" [label="12000\nHu0 Zu0 Hi0 Ta0 Li0 Mo100", fontsize=7];
"DRN_Squad" -> "DRN_WingMan" [label="170000", fontsize=7];
"DRN_Squad" -> "DRN_AdvFrm" [label="60000", fontsize=7];
"DRN_WingMan" -> "CCC_AI" [label="100000\nHu40 Zu30 Hi70 Ta50 Li60 Mo80", fontsize=7];
"DRN_WingMan" -> "DRN_BtlRdrs" [label="120000\nHu0 Zu0 Hi0 Ta100 Li0 Mo0", fontsize=7];
"DRN_COL" -> "DRN_CryCOL" [label="55000\nHu80 Zu50 Hi70 Ta80 Li85 Mo100", fontsize=7];
"DRN_COL" -> "DRN_CrakCOL" [label="55000\nHu80 Zu50 Hi90 Ta90 Li55 Mo65", fontsize=7];
"DRN_COL" -> "DRN_TPCOL" [label="85000\nHu70 Zu30 Hi60 Ta70 Li85 Mo95", fontsize=7];
"CCC_AI" -> "CCC_AIAdmin" [label="110000", fontsize=7];
"CCC_AI" -> "CCC_AIFac" [label="110000", fontsize=7];
"CCC_AI" -> "CCC_AIFrCon" [label="110000", fontsize=7];
"CCC_AI" -> "CCC_AIVrus" [label="150000\nHu30 Zu50 Hi30 Ta20 Li40 Mo90", fontsize=7];
"CCC_AIAdmin" -> "CCC_AIVrus" [label="90000\nHu40 Zu50 Hi40 Ta30 Li50 Mo90", fontsize=7];
"CCC_AIFac" -> "CCC_AIVrus" [label="90000\nHu40 Zu50 Hi40 Ta30 Li50 Mo90", fontsize=7];
"CCC_AIFrCon" -> "CCC_AIVrus" [label="90000\nHu40 Zu50 Hi40 Ta30 Li50 Mo90", fontsize=7];
"CCC_AIVrus" -> "CCC_AISlv" [label="180000\nHu50 Zu90 Hi50 Ta40 Li60 Mo90", fontsize=7];
"CCC_TrnsHum" -> "XNC_TrnsHum2" [label="15000", fontsize=7];
"XNC_TrnsHum2" -> "XNC_IncHum" [label="25000", fontsize=7];
"XNC_IncHum" -> "XNC_AdctHum" [label="60000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_IncHum" -> "XNC_TrnsHum3" [label="50000", fontsize=7];
"XNC_IncHum" -> "XNC_TempHum" [label="50000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_TrnsHum3" -> "XNC_SubHum" [label="80000", fontsize=7];
"XNC_SubHum" -> "XNC_AccHum" [label="100000\nHu0 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_SubHum" -> "XNC_ProfHum" [label="100000\nHu0 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"CCC_TrnsHvr" -> "XNC_TrnsHvr2" [label="15000", fontsize=7];
"XNC_TrnsHvr2" -> "XNC_IncHvr" [label="27000", fontsize=7];
"XNC_IncHvr" -> "XNC_AdctHvr" [label="60000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_IncHvr" -> "XNC_TrnsHvr3" [label="40000", fontsize=7];
"XNC_IncHvr" -> "XNC_TempHvr" [label="30000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_TrnsHvr3" -> "XNC_SubHvr" [label="60000", fontsize=7];
"XNC_SubHvr" -> "XNC_AccHvr" [label="100000\nHu100 Zu0 Hi0 Ta100 Li100 Mo100", fontsize=7];
"XNC_SubHvr" -> "XNC_ProfHvr" [label="100000\nHu100 Zu0 Hi0 Ta100 Li100 Mo100", fontsize=7];
"CCC_TrnsTrk" -> "XNC_TrnsTrk2" [label="13000", fontsize=7];
"XNC_TrnsTrk2" -> "XNC_IncTrk" [label="22000", fontsize=7];
"XNC_IncTrk" -> "XNC_AdctTrk" [label="90000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_IncTrk" -> "XNC_TrnsTrk3" [label="40000", fontsize=7];
"XNC_IncTrk" -> "XNC_TempTrk" [label="35000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_TrnsTrk3" -> "XNC_SubTrk" [label="60000", fontsize=7];
"XNC_SubTrk" -> "XNC_AccTrk" [label="100000\nHu100 Zu0 Hi100 Ta0 Li100 Mo100", fontsize=7];
"XNC_SubTrk" -> "XNC_ProfTrk" [label="80000\nHu100 Zu0 Hi100 Ta0 Li100 Mo100", fontsize=7];
"CCC_TrnsLir" -> "XNC_TrnsLir2" [label="25000", fontsize=7];
"XNC_TrnsLir2" -> "XNC_IncLir" [label="35000", fontsize=7];
"XNC_IncLir" -> "XNC_AdctLir" [label="60000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_IncLir" -> "XNC_TrnsLir3" [label="50000", fontsize=7];
"XNC_IncLir" -> "XNC_TempLir" [label="35000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_TrnsLir3" -> "XNC_SubLir" [label="90000", fontsize=7];
"XNC_SubLir" -> "XNC_AccLir" [label="120000\nHu100 Zu0 Hi100 Ta100 Li0 Mo100", fontsize=7];
"XNC_SubLir" -> "XNC_ProfLir" [label="90000\nHu100 Zu0 Hi100 Ta100 Li0 Mo100", fontsize=7];
"CCC_TrnsZul" -> "XNC_TrnsZuul2" [label="30000", fontsize=7];
"XNC_TrnsZuul2" -> "XNC_DomZuul" [label="50000", fontsize=7];
"XNC_DomZuul" -> "XNC_SubZuul" [label="70000", fontsize=7];
"CCC_TrnsMorr" -> "XNC_TrnsMorr2" [label="16000", fontsize=7];
"XNC_TrnsMorr2" -> "XNC_IncMorr" [label="25000", fontsize=7];
"XNC_IncMorr" -> "XNC_AdctMorr" [label="50000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_IncMorr" -> "XNC_TrnsMorr3" [label="50000", fontsize=7];
"XNC_IncMorr" -> "XNC_TempMorr" [label="60000\nHu100 Zu0 Hi100 Ta100 Li100 Mo100", fontsize=7];
"XNC_TrnsMorr3" -> "XNC_SubMorr" [label="70000", fontsize=7];
"XNC_SubMorr" -> "XNC_AccMorr" [label="90000\nHu100 Zu0 Hi100 Ta100 Li100 Mo0", fontsize=7];
"XNC_SubMorr" -> "XNC_ProfMorr" [label="90000\nHu100 Zu0 Hi100 Ta100 Li100 Mo0", fontsize=7];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff