132 lines
5.5 KiB
Python
132 lines
5.5 KiB
Python
"""stock_designs.py -- pull every ShipDesign (and each player's researched
|
|
techs) out of real saves via verify/save-reader/save_reader.py and write them
|
|
in the design_rules.py Design format.
|
|
|
|
python3 stock_designs.py [save.sav ...] -> stock_designs.json (next to this file)
|
|
|
|
Save layout decoded here (see SHIP_DESIGN_RULES.md section 7):
|
|
Player.designs / Player.legacyDesigns : [{DesID, Des{faiDes dHide dWep dName sections[5]}}]
|
|
DSec = { DSec{ int species_idx, int section_id }, DGbnk2{ int n, n x DW2 }, DOpts{ int n, n x tech } }
|
|
DW2 = { bool bID, (wid int | wfn string), int did }
|
|
Player.TechTree : per tech TNm St TResCost TResDone TAcq TiAcq Tbd Tfc TUnlck ; St == 4 -> researched
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
SAVE_READER = HERE.parent / "save-reader" / "save_reader.py"
|
|
DEFAULT_SAVES = sorted((HERE.parent / "results" / "saves").glob("*.sav"))
|
|
OUT = HERE / "stock_designs.json"
|
|
|
|
sys.path.insert(0, str(HERE))
|
|
from design_rules import Catalog, SPECIES_INDEX, design_from_save # noqa: E402
|
|
|
|
TECH_STATE_RESEARCHED = 4
|
|
|
|
|
|
def _items(node):
|
|
return node.get("_items", []) if isinstance(node, dict) else []
|
|
|
|
|
|
def _leaf_values(node):
|
|
return [it.get("value") for it in _items(node) if "value" in it]
|
|
|
|
|
|
def read_save_json(path: Path) -> dict:
|
|
res = subprocess.run([sys.executable, str(SAVE_READER), "--json", str(path)],
|
|
capture_output=True, text=True)
|
|
if res.returncode not in (0, 1):
|
|
raise RuntimeError(f"save_reader failed on {path}: {res.stderr[-500:]}")
|
|
return json.loads(res.stdout)
|
|
|
|
|
|
def decode_section_slot(sec: dict) -> dict | None:
|
|
"""One DSec frame -> {"species", "section_id", "weapons", "options"}."""
|
|
out: dict = {"species": 0, "section_id": 0, "weapons": [], "options": [], "bank_count": 0}
|
|
for c in _items(sec):
|
|
nm = c.get("_name", c.get("name"))
|
|
if nm == "DSec":
|
|
vals = _leaf_values(c)
|
|
if len(vals) >= 2:
|
|
out["species"], out["section_id"] = int(vals[0]), int(vals[1])
|
|
elif nm == "DGbnk2":
|
|
its = _items(c)
|
|
out["bank_count"] = int(its[0].get("value", 0)) if its else 0
|
|
for b in its[1:]:
|
|
entry = None
|
|
for dw in _items(b):
|
|
if dw.get("_name") == "DW2":
|
|
vals = {it["name"]: it.get("value") for it in _items(dw) if "name" in it}
|
|
if vals.get("wid") is not None:
|
|
entry = int(vals["wid"])
|
|
elif vals.get("wfn"):
|
|
entry = str(vals["wfn"])
|
|
out.setdefault("rider_design_ids", []).append(vals.get("did"))
|
|
out["weapons"].append(entry)
|
|
elif nm == "DOpts":
|
|
its = _items(c)
|
|
out["options"] = [str(it["value"]) for it in its[1:] if "value" in it]
|
|
return out
|
|
|
|
|
|
def decode_techs(player: dict) -> dict:
|
|
"""tech name -> state record; the state block follows the branch list."""
|
|
techs: dict = {}
|
|
cur = None
|
|
for it in _items(player.get("TechTree")):
|
|
n = it.get("name")
|
|
if n == "TNm":
|
|
cur = it.get("value")
|
|
elif n == "St":
|
|
techs[cur] = {"St": it.get("value")}
|
|
elif n in ("TResCost", "TResDone", "TAcq", "TiAcq", "Tbd", "Tfc", "TUnlck") and cur in techs:
|
|
techs[cur][n] = it.get("value")
|
|
return techs
|
|
|
|
|
|
def extract(path: Path, cat: Catalog) -> list[dict]:
|
|
d = read_save_json(path)
|
|
out = []
|
|
for pe in d["data"]["sim"]["players"]:
|
|
p = pe["Player"]
|
|
techs = decode_techs(p)
|
|
researched = sorted(t for t, s in techs.items() if s.get("St") == TECH_STATE_RESEARCHED)
|
|
species_idx = p.get("Species")
|
|
race = SPECIES_INDEX[species_idx] if isinstance(species_idx, int) and 0 <= species_idx < len(SPECIES_INDEX) else None
|
|
rec = {"save": path.name, "player_id": pe["PlayerID"], "player": p.get("PlryName"),
|
|
"species_idx": species_idx, "race": race, "npc": bool(p.get("NPC")),
|
|
"researched": researched, "n_techs": len(techs), "designs": []}
|
|
for kind in ("designs", "legacyDesigns"):
|
|
for des in p.get(kind) or []:
|
|
dd = des["Des"]
|
|
slots = [decode_section_slot(s) for s in dd.get("sections", [])]
|
|
design = design_from_save(dd.get("dName"), slots, cat, hidden=bool(dd.get("dHide")),
|
|
known_techs=researched, race=None)
|
|
design.update({"design_id": des["DesID"], "list": kind, "faiDes": dd.get("faiDes"),
|
|
"dHide": dd.get("dHide"), "dWep": dd.get("dWep"), "raw_slots": slots,
|
|
"player_race": race})
|
|
rec["designs"].append(design)
|
|
out.append(rec)
|
|
return out
|
|
|
|
|
|
def main(argv):
|
|
saves = [Path(a) for a in argv] or DEFAULT_SAVES
|
|
cat = Catalog.load()
|
|
all_players = []
|
|
for s in saves:
|
|
all_players.extend(extract(s, cat))
|
|
OUT.write_text(json.dumps(all_players, indent=1))
|
|
n = sum(len(p["designs"]) for p in all_players)
|
|
print(f"wrote {OUT}: {len(all_players)} player records, {n} designs from {len(saves)} saves")
|
|
for p in all_players:
|
|
print(f" {p['save']} #{p['player_id']} {p['player']!r} {p['race']} npc={p['npc']} "
|
|
f"designs={len(p['designs'])} researched={len(p['researched'])}/{p['n_techs']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|