#!/usr/bin/env python3 """gapdiff.py -- score a candidate re-export against the canonical one. python tools/gapdiff.py [oldDecompDir] [newDecompDir] defaults: reference/decomp reference/decomp/reexport Both dirs must already have been censused (tools/gapcensus.py ). Reports: index/coverage deltas, dark regions closed, and -- the part that matters for reconstruction -- which previously-dark addresses the KB already cites (past raw-disasm digs) now have real pseudocode, plus how much of the never-touched game-side dark came into the light. """ import os import re import sys import glob ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OLD = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.path.join(ROOT, 'reference', 'decomp') NEW = os.path.abspath(sys.argv[2]) if len(sys.argv) > 2 else os.path.join(ROOT, 'reference', 'decomp', 'reexport') # addresses the KB/port cite (a past dig reached them) -- the practical audience CITE = set() _pat = re.compile(r'(?:@|FUN_|0x)0{0,2}(4[0-9a-f]{5})\b', re.I) for base in ('game', 'context', 'docs'): for dp, _d, fs in os.walk(os.path.join(ROOT, base)): for fn in fs: if fn.lower().endswith(('.cpp', '.hpp', '.h', '.md')): try: for m in _pat.finditer(open(os.path.join(dp, fn), encoding='utf-8', errors='replace').read()): CITE.add(int(m.group(1), 16)) except OSError: pass def load_index(d): idx = {} p = os.path.join(d, 'functions_index.tsv') for line in open(p, encoding='utf-8', errors='replace'): f = line.rstrip('\n').split('\t') if len(f) < 4: continue try: idx[int(f[0], 16)] = (int(f[1]), f[2], f[3]) except ValueError: pass return idx def load_regions(d): p = os.path.join(d, 'gap_census.tsv') out = [] for line in open(p, encoding='utf-8', errors='replace').read().splitlines()[1:]: f = line.split('\t') if len(f) < 8: f += [''] * (8 - len(f)) out.append({'lo': int(f[0], 16), 'hi': int(f[1], 16), 'size': int(f[2]), 'code': int(f[3]), 'visited': f[6], 'tu': f[7]}) return out def exported_addrs(d): got = set() hdr = re.compile(r'/\* @(00[0-9a-f]{6}) ') for p in sorted(glob.glob(os.path.join(d, 'all', 'part_*.c'))): for m in hdr.finditer(open(p, encoding='utf-8', errors='replace').read()): got.add(int(m.group(1), 16)) return got def main(): oi, ni = load_index(OLD), load_index(NEW) orr, nr = load_regions(OLD), load_regions(NEW) oe, ne = exported_addrs(OLD), exported_addrs(NEW) o_dark = sum(r['code'] for r in orr) n_dark = sum(r['code'] for r in nr) o_game = sum(r['code'] for r in orr if 'bt/' in r['tu'] or 'bt_l4/' in r['tu']) n_game = sum(r['code'] for r in nr if 'bt/' in r['tu'] or 'bt_l4/' in r['tu']) new_fns = sorted(set(ni) - set(oi)) # which new functions land in what USED to be dark def in_old_dark(a): for r in orr: if r['lo'] <= a < r['hi']: return True return False new_in_dark = [a for a in new_fns if in_old_dark(a)] cited_new = [a for a in new_in_dark if a in CITE] print('=== GAP DIFF: %s -> %s' % (os.path.basename(OLD), os.path.basename(NEW))) print('indexed functions : %d -> %d (+%d)' % (len(oi), len(ni), len(ni) - len(oi))) print('exported pseudocode: %d -> %d (+%d)' % (len(oe), len(ne), len(ne) - len(oe))) print('dark regions : %d -> %d (%+d)' % (len(orr), len(nr), len(nr) - len(orr))) print('dark REAL code : %.1f KB -> %.1f KB (%+.1f KB, %.1f%% recovered)' % (o_dark / 1024, n_dark / 1024, (n_dark - o_dark) / 1024, 100.0 * (o_dark - n_dark) / o_dark if o_dark else 0)) print(' game-side dark : %.1f KB -> %.1f KB (%+.1f KB)' % (o_game / 1024, n_game / 1024, (n_game - o_game) / 1024)) print('new functions in previously-DARK bytes: %d' % len(new_in_dark)) print(' ...of which the KB/port already cites (past raw-disasm digs): %d' % len(cited_new)) if cited_new: print(' ' + ' '.join('%x' % a for a in cited_new[:24]) + (' ...' if len(cited_new) > 24 else '')) # celebrity check -- the addresses that historically burned us fam = [(0x4c05c4, 'VehicleDead handler'), (0x4b838c, 'Searchlight ToggleLamp'), (0x4a07b8, 'TakeDamage death tail'), (0x4a9b5c, 'master perf'), (0x4b8be3, 'myomer integrator'), (0x4aa011, 'duck consumer'), (0x486467, 'btmssn dispatch cluster'), (0x4c090a, 'btplayer hole')] print('--- celebrities (function created? exported?)') for a, what in fam: # nearest containing function in the new index owner = max((x for x in ni if x <= a and x + ni[x][0] > a), default=None) print(' %-24s %s' % (what, ('IN %06x (%s)%s' % (owner, ni[owner][2], ' +pseudocode' if owner in ne else '')) if owner else 'still dark')) if __name__ == '__main__': main()