#!/usr/bin/env python3 """Round-trip verifier for the .damage decompiler. Regenerates a whole `.damage` from the compiled record for every chassis and compares it page by page against the authored source: page names, page order, key sets and values. Comparison is semantic. Sources carry an `!include` line and comments, spell numbers freely (`.99` vs `0.99`, `1.0` vs `1`), and Windows path lookup is case-insensitive, so none of those count as differences. python3 verify_damage.py [--show CHASSIS] """ import argparse, collections, glob, os, re, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import damage, subsystems REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs" SRC = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs" NUM = re.compile(r'^-?(?:\d+\.?\d*|\.\d+)$') def canon(v): v = str(v).strip() if NUM.match(v): return f"{float(v):.4g}" if "," in v: # DamageEffect: path,percent path, _, pct = v.rpartition(",") return canon(path) + "," + (f"{float(pct):.4g}" if NUM.match(pct.strip()) else pct) v = re.sub(r'^content[\\/]', '', v.replace("/", "\\"), flags=re.I) return v.lower() def source_pages(path): """-> [(pageName, [(key, value)])] preserving order and repeats.""" txt = open(path, "rb").read().decode("latin-1") txt = re.sub(r'//[^\n]*', '', txt) pages, cur = [], None for line in txt.splitlines(): line = line.strip() if not line or line.startswith("!"): continue m = re.match(r'^\[([^\]]+)\]$', line) if m: cur = (m.group(1), []) pages.append(cur) elif "=" in line and cur is not None: k, v = line.split("=", 1) cur[1].append((k.strip(), v.strip())) return pages def main(): ap = argparse.ArgumentParser() ap.add_argument("--show") args = ap.parse_args() manifest = subsystems.load_manifest(damage.MANIFEST) t = collections.Counter() bad = collections.Counter() examples = [] order_problems = [] for d in sorted(glob.glob(REC + "/*")): ch = os.path.basename(d) src = [p for p in glob.glob(SRC + "/*/*.damage") if os.path.basename(p).lower() == ch.lower() + ".damage"] if not src or not glob.glob(os.path.join(d, "*.damage")): continue t["chassis"] += 1 want = source_pages(src[0]) got = damage.decompile(d, manifest) if args.show and args.show.lower() == ch.lower(): sys.stdout.write(damage.emit(got)) return if [n.lower() for n, _ in want] != [n.lower() for n, _ in got]: order_problems.append((ch, [n for n, _ in want], [n for n, _ in got])) continue t["pages"] += len(want) for (wname, wkv), (_gname, gkv) in zip(want, got): wmap = collections.defaultdict(list) for k, v in wkv: wmap[k.lower()].append(canon(v)) gmap = collections.defaultdict(list) for k, v in gkv: gmap[k.lower()].append(canon(v)) for key in set(wmap) | set(gmap): t["keys"] += 1 w, g = sorted(wmap.get(key, [])), sorted(gmap.get(key, [])) # The writer defaults MaxArmorValue to BaseArmorValue, so a source # that omits it and one that states it equal produce identical # bytes -- 51 pages do state it. The distinction is unrecoverable # and harmless, so emitting it explicitly counts as a match. if not w and key == "maxarmorvalue" and g == sorted(gmap.get("basearmorvalue", [])): t["ok"] += 1 t["implicit_max"] += 1 elif w == g: t["ok"] += 1 else: bad[key] += 1 if len(examples) < 12: examples.append((ch, wname, key, wmap.get(key), gmap.get(key))) print(f"chassis : {t['chassis']}") print(f"pages compared : {t['pages']}") print(f"keys compared : {t['keys']} exact: {t['ok']} wrong: {t['keys'] - t['ok']}") print(f" MaxArmorValue omitted by source, implied by BaseArmorValue: {t['implicit_max']}") if order_problems: print(f"\npage name/order mismatches: {len(order_problems)}") for ch, w, g in order_problems[:3]: print(f" {ch}\n want {w}\n got {g}") if bad: print("\nkeys not matching:") for k, n in bad.most_common(15): print(f" {k:28s} x{n}") print("\nexamples (chassis, page, key, source, decoded):") for e in examples: print(f" {e[0]:14s} {e[1]:22s} {e[2]:22s} {e[3]} != {e[4]}") if __name__ == "__main__": main()