Crusader_Decomp/scripts/dump_weapon_commit_table.py

208 lines
6.7 KiB
Python
Raw Normal View History

2026-04-12 14:45:08 +02:00
#!/usr/bin/env python3
import os
import sys
from collections import Counter
fn = os.path.join('binary','Crusader - No Remorse Weapons Main Ram.bin')
if not os.path.exists(fn):
print('ERROR: file not found:', fn)
sys.exit(2)
with open(fn,'rb') as f:
data = f.read()
size = len(data)
print(f'File: {fn} size=0x{size:X} ({size} bytes)')
# Try weapon table base/stride used earlier
base_candidates = [0x6466A, 0x64640, 0x64680, 0x64000]
stride = 0x26
def extract_weapon_table(base, stride, max_rows=128):
rows = []
for i in range(max_rows):
off = base + i*stride
if off >= size:
break
row = data[off:off+stride]
# extract ASCII-like name at start
name_bytes = bytearray()
for b in row:
if 32 <= b <= 126:
name_bytes.append(b)
else:
if len(name_bytes)>0:
break
name = name_bytes.decode('ascii',errors='replace')
rows.append((i, off, name))
return rows
found = None
for base in base_candidates:
rows = extract_weapon_table(base, stride, max_rows=64)
non_empty = [r for r in rows if r[2]]
if len(non_empty) >= 8:
found = (base, rows)
break
if not found:
# fallback: scan for repeated ASCII names with stride 0x26
print('Primary bases not successful; scanning for candidate bases...')
candidates = []
for b in range(0, min(size-0x26*8, 0x200000), 0x10):
rows = extract_weapon_table(b, stride, max_rows=12)
non_empty = sum(1 for r in rows if r[2])
if non_empty >= 6:
candidates.append((b, non_empty))
candidates.sort(key=lambda x:-x[1])
if candidates:
base = candidates[0][0]
print(f'Picked candidate base 0x{base:X} (hits={candidates[0][1]})')
found = (base, extract_weapon_table(base, stride, max_rows=128))
if not found:
print('ERROR: could not find weapon table automatically.')
sys.exit(3)
base, rows = found
print(f'Weapon table base=0x{base:X} stride=0x{stride:X} rows scanned={len(rows)}')
weapon_rows = [r for r in rows if r[2]]
for idx, off, name in weapon_rows:
print(f' idx {idx:02X} @0x{off:X} -> {name}')
max_index = max((r[0] for r in weapon_rows), default=-1)
print(f'Weapon rows discovered: {len(weapon_rows)} max idx {max_index}')
# build name map
name_map = {r[0]: r[2] for r in weapon_rows}
# Search for candidate commit tables of byte-sized indices
min_len = 12
scan_len = 24
candidates = []
limit = size - scan_len
for off in range(0, limit, 1):
window = data[off:off+scan_len]
valid = sum(1 for b in window if b <= max_index and b >= 0)
if valid >= int(scan_len*0.75):
# extend forward while valid fraction remains high
end = off+scan_len
while end < size:
b = data[end]
window_len = end-off+1
if b <= max_index:
end += 1
continue
# if occasional invalid, allow up to 25% invalid
win = data[off:end+1]
v = sum(1 for x in win if x <= max_index)
if v >= int(len(win)*0.75):
end += 1
continue
break
length = end-off
seq = list(data[off:off+min(64,length)])
# deduplicate nearby overlaps by only keeping when off is first in a run
if candidates and off < candidates[-1]['end'] + 4:
continue
candidates.append({'off':off,'end':end,'len':length,'sample':seq[:64]})
if len(candidates) >= 16:
break
print('\nCandidate byte-sized commit tables found:')
if not candidates:
print(' none')
else:
for c in candidates[:10]:
off = c['off']; l=c['len']
print(f' table @0x{off:X} len={l}')
# print first 24 entries mapped
n = min(24,l)
entries = list(data[off:off+n])
for i,v in enumerate(entries):
name = name_map.get(v,'')
print(f' ch {i:02} -> 0x{v:02X} {name}')
# check for 0x0C/0x0D
hits = [(i,v) for i,v in enumerate(entries) if v in (0x0C,0x0D)]
if hits:
for i,v in hits:
print(f' ** contains 0x{v:02X} at entry {i}')
# Also search for 16-bit big-endian indices sequences
print('\nScanning for 16-bit big-endian index sequences (min_len=12 entries)...')
be_candidates = []
min_entries = 12
for off in range(0, size-2*min_entries, 1):
# read min_entries big-endian 16-bit values
ok = True
vals = []
for i in range(min_entries):
idx = off + i*2
v = (data[idx]<<8) | data[idx+1]
if v > max_index:
ok = False
break
vals.append(v)
if ok:
be_candidates.append((off, vals[:min_entries]))
if len(be_candidates) >= 8:
break
if not be_candidates:
print(' none')
else:
for off,vals in be_candidates:
print(f' table @0x{off:X} (big-endian 16-bit entries) sample:')
for i,v in enumerate(vals):
print(f' ch {i:02} -> 0x{v:04X} {name_map.get(v,"")}')
# Summary: look for any channel mapping to 0x0C or 0x0D anywhere in file as single bytes
print('\nSummary scan for bytes 0x0C or 0x0D in likely index contexts:')
positions = []
for val in (0x0C,0x0D):
offs = [i for i,b in enumerate(data) if b==val]
# filter to positions where surrounding bytes align with many valid indices
filtered = []
for o in offs:
left = max(0,o-4); right = min(size,o+5)
win = data[left:right]
valid = sum(1 for b in win if b<=max_index)
if valid >= int(len(win)*0.7):
filtered.append(o)
print(f' byte 0x{val:02X}: total occurrences={len(offs)} filtered likely-context={len(filtered)}')
if filtered:
for o in filtered[:10]:
print(f' at 0x{o:X} (file offset)')
print('\nDone.')
# Extra: dump candidate channel commit tables at known offsets with more rows
def read_name_at(defOff):
end = defOff + stride - 1
s = ''
best = ''
for i in range(defOff, min(end+1, size)):
c = data[i]
if 32 <= c <= 126:
s += chr(c)
else:
if len(s) >= 2:
best = s
break
else:
s = ''
return best
for tableStart in (0x64355, 0x64340, 0x64330):
if tableStart >= size:
continue
rec = 10
rows = 40
print(f'\nChannel commit table @0x{tableStart:X} (rec={rec}) rows up to {rows}:')
for ch in range(rows):
idxOff = tableStart + ch*rec + 9
if idxOff >= size:
break
sel = data[idxOff]
defOff = base + sel*stride
name = read_name_at(defOff) if defOff < size else ''
print(f' chan {ch:02d}: sel=0x{sel:02X} ({sel}) -> def@0x{defOff:X} -> name: {name} (idxOff=0x{idxOff:X})')