#!/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})')