sots-re/verify/design-rules/stock_designs.py

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