#!/usr/bin/env python3 """techId -> tech name, derived offline from the tech tree data. The wire techId is the 0-based index of the tech's name in the master tech list sorted with _stricmp -- case-insensitively. Read from MasterTechTree's constructor: it copies the parse-order list to a second vector, std::sort()s that copy with an inlined `_stricmp(a->name, b->name) < 0`, then walks it writing `def->techId = i`. See findings/subsystems/techid-name-map.md. This is NOT the 10000-based TechID enum, which is a separate 196-entry .rdata table. Usage: tools/techid_table.py # all of them tools/techid_table.py 282 288 144 90 # just these """ import glob import json import os import re import sys ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") # Four (techId, name) pairs observed live, by two lanes on different runs. 282 is the one the # rule was NOT fitted to -- it comes from lane L4's run R2 (findings/subsystems/ai-order-capture.md) # and is the check that this file is right rather than merely self-consistent. OBSERVED = {90: "DRV_PlsFiss", 144: "IND_Waldo", 282: "XNC_TrnsHum2", 288: "XNC_TrnsMorr2"} # Where the 293 names can come from, in order of preference: the raw game file under # $SOTS_GOB_DIR (the same variable the engine's realdata_test uses), then the checked-in parse # oracle in sots-engine's tests, which needs no data tree at all. SOURCES = [p for p in [ os.path.join(os.environ["SOTS_GOB_DIR"], "TechTree", "MasterTechList.tech") if os.environ.get("SOTS_GOB_DIR") else None, os.path.join(ROOT, "..", "sots-engine", "tests", "mars_parse", "build", "oracle", "TechTree", "MasterTechList.tech.json"), ] if p] def names_from_tech_file(path): """Pull every `tech { name "X" ... }` block name out of the raw .tech file.""" text = open(path, encoding="latin-1").read() return re.findall(r'^\s*tech\b[^\n]*\n(?:[^\n]*\n)*?\s*name\s+"([^"]+)"', text, re.M) def names_from_oracle(path): return [t["name"] for t in json.load(open(path))["tech"]] def load_names(): for pat in SOURCES: for path in sorted(glob.glob(pat)): if os.path.exists(path): return (names_from_oracle(path) if path.endswith(".json") else names_from_tech_file(path)), path for path in sorted(glob.glob(os.path.join(ROOT, "**", "MasterTechList.tech"), recursive=True)): return names_from_tech_file(path), path sys.exit("no MasterTechList.tech (or its parse oracle) found; see SOURCES in this file") def table(): names, src = load_names() if len(set(n.lower() for n in names)) != len(names): sys.exit("case-folded duplicate tech names: the sorted order is not total, stop") # _stricmp order. str.lower reproduces it exactly for these pure-ASCII names. return sorted(names, key=str.lower), src def main(argv): ids, src = table() bad = [(i, want, ids[i] if i < len(ids) else "") for i, want in OBSERVED.items() if i >= len(ids) or ids[i] != want] if bad: print(f"# {len(ids)} names parsed from {src}", file=sys.stderr) for i, want, got in bad: print(f"MISMATCH techId {i}: observed {want!r}, derived {got!r}", file=sys.stderr) print("The observed points are the only check this file has. Do not trust the table " "until they agree -- a short parse (a missed block) shifts EVERY id after it.", file=sys.stderr) return 1 print(f"# {len(ids)} techs from {os.path.relpath(src, ROOT)}; " f"{len(OBSERVED)}/{len(OBSERVED)} observed points agree", file=sys.stderr) wanted = [int(a) for a in argv] if argv else range(len(ids)) for i in wanted: if 0 <= i < len(ids): print(f"{i:4d} {ids[i]}{' <- observed' if i in OBSERVED else ''}") else: print(f"{i:4d} ") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))