#!/usr/bin/env python3 """Emit include/generated/sots_addresses.h for sots-engine from ghidra/addresses.json. Only facts (name, RVA, convention, prototype, status) — never decompiler text.""" import glob, json, os, subprocess, sys, datetime root = os.path.join(os.path.dirname(__file__), "..") j = json.load(open(os.path.join(root, "ghidra", "addresses.json"))) base = int(j["image_base"], 16) # Per-lane fragments. addresses.json is a single shared file, so concurrent lanes kept # sweeping each other's in-flight entries into the wrong commit. A lane may instead write # ghidra/addresses.d/.json ({"entries": [...]}) — its own file, no shared-line edits. # Fragments are merged here in sorted order; a duplicate name is an error, not a silent # last-wins, because two lanes disagreeing about an address is exactly what we must not paper over. # Two collisions are possible and BOTH are errors: # same name, different address -> two lanes disagree about a fact. # same address, different name -> two lanes fork the vocabulary. This one is worse, because # nothing downstream notices: the header just grows a synonym and later readers cannot tell # that `RNG_NextInt` and `RNG_NextIntInclusive` are one function. A lane caught exactly that # by hand on the campaign's most-used RNG primitive; the check below is so nobody has to. def _key(e): return ("offset", e["offset"].lower()) if "offset" in e else ("addr", e["addr"].lower()) seen = {e["name"]: "addresses.json" for e in j["entries"]} # Offsets are only meaningful per owning struct, so they are not globally unique — key addresses only. by_addr = {_key(e)[1]: (e["name"], "addresses.json") for e in j["entries"] if "addr" in e} for frag_path in sorted(glob.glob(os.path.join(root, "ghidra", "addresses.d", "*.json"))): frag_name = os.path.basename(frag_path) for e in json.load(open(frag_path))["entries"]: if e["name"] in seen: sys.exit(f"duplicate address entry {e['name']!r}: in {seen[e['name']]} and {frag_name}") if "addr" in e: a = e["addr"].lower() if a in by_addr: other, where = by_addr[a] sys.exit(f"address {e['addr']} named twice: {other!r} in {where}, " f"{e['name']!r} in {frag_name} — pick one name and record the agreement") by_addr[a] = (e["name"], frag_name) seen[e["name"]] = frag_name j["entries"].append(e) try: rev = subprocess.check_output(["git","-C",root,"rev-parse","--short","HEAD"]).decode().strip() except Exception: rev = "unknown" out = [ "// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).", f"// Source: sots-re ghidra/addresses.json @ {rev}, generated {datetime.date.today()} by tools/gen_addresses.py", "// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).", "#pragma once", "#include ", "", "namespace sots::addr {", f"constexpr uint32_t IMAGE_BASE = 0x{base:08x};", "" ] for e in j["entries"]: # Two entry shapes: an "addr" (absolute VA -> RVA) or an "offset" (a field offset inside a # struct, emitted verbatim). Offsets are facts about layout, not load addresses. val = int(e["offset"], 16) if "offset" in e else int(e["addr"], 16) - base out.append(f"// {e['convention']:8s} {e['prototype']} [{e['status']}]") out.append(f"constexpr uint32_t {e['name']} = 0x{val:08x};") out += ["", "} // namespace sots::addr", ""] dest = sys.argv[1] if len(sys.argv) > 1 else os.path.join(root, "ghidra", "generated", "sots_addresses.h") os.makedirs(os.path.dirname(dest), exist_ok=True); open(dest, "w").write("\n".join(out)); print("wrote", dest, f"({len(j['entries'])} entries)")