#!/usr/bin/env python3 """Recover a sub-record's FULL schema field order by merging every unit .tbl. Each .tbl's string pool lists only the fields that .tbl declares, in schema order. Merging them is a topological sort over the pairwise "k[i] precedes k[i+1]" constraints of all 110 tables: any single table is a subsequence of the schema, so the union order is the schema order — provided the constraints are acyclic, which is itself the check that the premise holds. Usage: schema_order.py [solved.txt] """ import sys from collections import defaultdict sys.path.insert(0, ".") from order_check import section, keys_in_order, solved_offsets # noqa: E402 SUBS = {"Generic", "Maneuver", "Shield", "Explosion", "Mass", "Effect", "SE"} def merged_order(dump, sub): tbls = [l.split()[1] for l in open(dump) if l.startswith("RAW ")] succ, indeg, nodes = defaultdict(set), defaultdict(int), [] seqs = [] for t in tbls: ks = keys_in_order(section(dump, t, sub, SUBS)) if ks: seqs.append(ks) for k in ks: if k not in nodes: nodes.append(k) for a, b in zip(ks, ks[1:]): if b not in succ[a]: succ[a].add(b) indeg[b] += 1 # Kahn, breaking ties by first-seen order so the result is deterministic ready = [n for n in nodes if indeg[n] == 0] out, cycles = [], False while ready: ready.sort(key=nodes.index) n = ready.pop(0) out.append(n) for m in sorted(succ[n], key=nodes.index): indeg[m] -= 1 if indeg[m] == 0: ready.append(m) if len(out) != len(nodes): cycles = True out += [n for n in nodes if n not in out] return out, seqs, cycles def main(): dump, sub = sys.argv[1], sys.argv[2] order, seqs, cycles = merged_order(dump, sub) print(f"# {sub}: {len(order)} fields merged from {len(seqs)} tables" f"{' !! CYCLE — order is not consistent' if cycles else ''}") # every table must be a subsequence of the merged order — the real check pos = {k: i for i, k in enumerate(order)} bad = [s for s in seqs if [pos[k] for k in s] != sorted(pos[k] for k in s)] print(f"# tables that are NOT a subsequence of the merged order: {len(bad)}") if len(sys.argv) > 3: solved = solved_offsets(sys.argv[3]) anchors = [(i, k, solved[k]) for i, k in enumerate(order) if k in solved] votes = defaultdict(list) for i, k, (off, *_) in anchors: votes[off - 4 * i].append((i, k)) print(f"# {len(anchors)} solved anchors; base votes:") for b, ks in sorted(votes.items(), key=lambda kv: -len(kv[1])): idx = sorted(i for i, _ in ks) print(f"# base {b:#07x}: {len(ks):3d} anchors, indices {idx[0]}..{idx[-1]}") for i, k in enumerate(order): s = solved.get(k) note = f"solver={s[0]:#x} {s[1]} agree={s[2]} dist={s[3]}" if s else "" print(f" [{i:3d}] {k:<34} {note}") if __name__ == "__main__": main()