"""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:])