verify: mars data parsers proven 100% on 1595 files; catalogs; tech-tree graph
This commit is contained in:
parent
9378cec2fa
commit
7139dee219
20 changed files with 154269 additions and 2 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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]])
|
||||
|
|
|
|||
326
findings/subsystems/data-parsers.md
Normal file
326
findings/subsystems/data-parsers.md
Normal 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
|
||||
|
|
@ -1 +0,0 @@
|
|||
# placeholder — populated during the campaign
|
||||
BIN
verify/parsers/__pycache__/effect_txt.cpython-310.pyc
Normal file
BIN
verify/parsers/__pycache__/effect_txt.cpython-310.pyc
Normal file
Binary file not shown.
BIN
verify/parsers/__pycache__/flat_kv.cpython-310.pyc
Normal file
BIN
verify/parsers/__pycache__/flat_kv.cpython-310.pyc
Normal file
Binary file not shown.
BIN
verify/parsers/__pycache__/manifest.cpython-310.pyc
Normal file
BIN
verify/parsers/__pycache__/manifest.cpython-310.pyc
Normal file
Binary file not shown.
BIN
verify/parsers/__pycache__/mars_data.cpython-310.pyc
Normal file
BIN
verify/parsers/__pycache__/mars_data.cpython-310.pyc
Normal file
Binary file not shown.
98
verify/parsers/effect_txt.py
Normal file
98
verify/parsers/effect_txt.py
Normal 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
127
verify/parsers/flat_kv.py
Normal 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
121
verify/parsers/manifest.py
Normal 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
211
verify/parsers/mars_data.py
Normal 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
634
verify/parsers/verify.py
Normal 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]))
|
||||
1240
verify/results/data-catalogs/crosslink.json
Normal file
1240
verify/results/data-catalogs/crosslink.json
Normal file
File diff suppressed because it is too large
Load diff
1681
verify/results/data-catalogs/schema_stats.json
Normal file
1681
verify/results/data-catalogs/schema_stats.json
Normal file
File diff suppressed because it is too large
Load diff
121233
verify/results/data-catalogs/shipsections.json
Normal file
121233
verify/results/data-catalogs/shipsections.json
Normal file
File diff suppressed because it is too large
Load diff
5198
verify/results/data-catalogs/strings.json
Normal file
5198
verify/results/data-catalogs/strings.json
Normal file
File diff suppressed because it is too large
Load diff
650
verify/results/data-catalogs/tech_tree.dot
Normal file
650
verify/results/data-catalogs/tech_tree.dot
Normal 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];
|
||||
}
|
||||
10387
verify/results/data-catalogs/tech_tree.json
Normal file
10387
verify/results/data-catalogs/tech_tree.json
Normal file
File diff suppressed because it is too large
Load diff
12360
verify/results/data-catalogs/weapons.json
Normal file
12360
verify/results/data-catalogs/weapons.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue