#!/usr/bin/env python3 """Read EVERY field of the live unit-definition objects, using the code-derived layout instead of solving offsets from the disc. `unit_runtime.py` scores `(field, offset, encoding)` triples against the disc records, so it can only place fields the disc actually values — 58 of 153 on a single-mission snapshot. The layout in `data/unit_definition_layout.txt` was read out of the title's own loader (`sub_82341A20`: the field NAME of every store is a string in the image), so it places **all** of them, including the ones no disc record ever sets. That is exactly the Route-B target: the defaulted fields. Discipline is unchanged — every field the disc DOES value is cross-checked against the object, and a mismatch is reported rather than written. Angle fields are degrees on disc and radians in the object (see the layout header), so those are compared after conversion. Usage: unit_dump_layout.py ... [--csv out.csv] """ import csv import math import re import struct import sys sys.path.insert(0, __file__.rsplit("/", 1)[0]) import unit_runtime as ur # object locator + disc token reader # Angle fields can carry a prefix (`AB_AA_PitchPlus` = afterburner), so match # the AV_/AA_ token anywhere, not just at the start — anchoring it left two # player-craft fields looking like contradictions when they were 15°/16° in # radians. ANGLE = re.compile(r"(^|_)(AV|AA)_|Bank|Turn_AngularVelocity") def read_layout(path): """[(offset, kind, name)] from the '# offset kind field' table.""" out = [] for line in open(path): line = line.strip() if not line or line.startswith("#"): continue parts = line.split() if len(parts) < 3: continue off, kind, name = parts[0], parts[1].lower(), parts[2] try: # The layout table writes offsets in DECIMAL (`48 f32 Size_X`), while # the solver's CSV prints them in hex — parsing this file as hex puts # every field 0x18 bytes late and makes the disc cross-check fail on # everything, which is how the mistake announces itself. out.append((int(off, 10), kind, name)) except ValueError: continue return out def value_at(raw, off, kind): if off + 4 > len(raw): return None if kind in ("f32", "float"): return struct.unpack_from(">f", raw, off)[0] if kind in ("word", "u32", "int", "bool"): return struct.unpack_from(">I", raw, off)[0] return None # str: a guest pointer, not a value def as_float(s): try: return float(str(s).rstrip("fF")) except ValueError: return None def main(): layout_path, tokens_path = sys.argv[1], sys.argv[2] snaps = [a for a in sys.argv[3:] if not a.startswith("--")] out_csv = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--csv=")), None) layout = read_layout(layout_path) disc, _ambiguous = ur.read_tokens(tokens_path) objs, conflicts = ur.runtime_objects(snaps) print(f"# layout fields: {len(layout)} runtime objects: {len(objs)} " f"disc units: {len(disc)} matched: {len(set(objs) & set(disc))}") if conflicts: print(f"# !! objects differing between snapshots: {conflicts}") rows, agree, disagree = [], 0, 0 for unit in sorted(objs): raw = objs[unit] drec = disc.get(unit, {}) for off, kind, name in layout: v = value_at(raw, off, kind) if v is None: continue dv = as_float(drec.get(name)) if name in drec else None if dv is not None: # The disc values this field: it is a CHECK, not a new value. cmpv = math.degrees(v) if ANGLE.search(name) and isinstance(v, float) else v ok = abs(cmpv - dv) <= max(1e-3, abs(dv) * 1e-3) agree, disagree = (agree + 1, disagree) if ok else (agree, disagree + 1) if not ok: print(f"# MISMATCH {unit} {name} @{off:#05x}: object {cmpv!r} vs disc {dv!r}") rows.append([unit, name, f"{off:#05x}", kind, repr(v), "disc", "verified" if ok else "CONTRADICTED"]) else: rows.append([unit, name, f"{off:#05x}", kind, repr(v), "defaulted-on-disc", "layout-derived"]) print(f"# cross-check against the disc: {agree} agree, {disagree} disagree") print(f"# rows: {len(rows)} ({sum(1 for r in rows if r[5] == 'defaulted-on-disc')} defaulted)") if out_csv: w = csv.writer(open(out_csv, "w", newline="")) w.writerow(["unit", "field", "offset", "enc", "value", "source", "conf"]) w.writerows(rows) print(f"# wrote {out_csv}") if __name__ == "__main__": main()