#60 PART 2: the RE-EXPORT -- dark code 90KB -> 41KB, coverage 87.3% -> 93.5%
Installed JDK 21 + Ghidra 12.1.2 (no admin, %LOCALAPPDATA%\bt411-tools beside DXSDK/cmake; runner uses 8.3 SHORT paths because Ghidra's .bat expands %JAVA_HOME% unquoted and the profile has a space). New tooling: reference/ghidra_scripts/ExportGaps.java -- ExportAll's exact output contract PLUS a gap-fill pass (force disassembly + createFunction at E8 call targets outside functions, data->code pointers at a plausible prologue, and the census's discovered starts; iterated to a fixpoint, logged to gapfill_report.tsv). tools/ghidra_reexport.sh (headless runner, 'reprocess' mode) and tools/gapdiff.py (score two censused exports); gapcensus.py now censuses any export dir. Results: 6267 -> 6472 functions (+205 created in 2 rounds: 195 census starts, 6 call targets, 4 data pointers; 56.1KB newly covered), ZERO decompile failures. Dark real code 90.4 -> 40.8 KB (54.8% recovered); game-side dark 53.1 -> 21.1 KB; regions 428 -> 321. EVERY historically dark function now has pseudocode -- including @0x4c05c4 VehicleDead, the absence that opened this issue. VALIDATION: the new pseudocode confirms this week's hand reconstruction of the crouch field-for-field (mapPosture/duckState/squatCapable/myomerEff/ novice gate/SetLegAnimation/ForceUpdate/stability alarm) -- and exposed one branch the raw pass missed: AIRBORNE AUTO-RISE (mode 3|4 && legState 1 -> forced squ), now implemented in mech4.cpp and re-benched un-regressed. PROMOTION: the re-export is canonical reference/decomp/; the previous export is preserved at reference/decomp/archive_2025export/ so old `part_0NN.c:LINE` citations still resolve (addresses are stable across both; line/shard membership is NOT -- cite @ADDR). New lead recorded: @0x4c0904 is the MASTER BTPlayer Performance (team resolution, EndMission console post, score heartbeat) -- our @0x4c083c PlayerSimulation attribution needs a re-check. KB: source-completeness, gotcha #20 (the rule is cheap now -- look it up), CLAUDE.md router/layout. Log: phases/phase-04-gap-census.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e69c0d7aa8
commit
f44be87ab2
@@ -0,0 +1,122 @@
|
||||
#!/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 <dir>).
|
||||
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()
|
||||
Reference in New Issue
Block a user