Research
This commit is contained in:
parent
28cbbe3470
commit
a9153546ae
56 changed files with 6731 additions and 258 deletions
142
scripts/analyze_weapons_table_region.py
Normal file
142
scripts/analyze_weapons_table_region.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
FN = r"binary/Crusader - No Remorse Weapons Main Ram.bin"
|
||||
OFFSETS = [0x133000, 0x133416, 0x1335d4]
|
||||
WINDOW_BEFORE = 0x100
|
||||
WINDOW_AFTER = 0x200
|
||||
|
||||
def hexdump(buf, base):
|
||||
lines = []
|
||||
for i in range(0, len(buf), 16):
|
||||
chunk = buf[i:i+16]
|
||||
hexs = ' '.join(f"{b:02x}" for b in chunk)
|
||||
ascii_ = ''.join((chr(b) if 32 <= b < 127 else '.') for b in chunk)
|
||||
lines.append(f"{base+i:08x}: {hexs:<47} {ascii_}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def analyze_region(buf, base):
|
||||
print(f"\n-- Analysis for region base 0x{base:x}, length {len(buf):x} --")
|
||||
ctr = Counter(buf)
|
||||
print("Top byte frequencies:")
|
||||
for b,c in ctr.most_common(12):
|
||||
print(f" 0x{b:02x}: {c}")
|
||||
# positions of 0x0c/0x0d
|
||||
pos0c = [i for i,b in enumerate(buf) if b==0x0c]
|
||||
pos0d = [i for i,b in enumerate(buf) if b==0x0d]
|
||||
print(f"Count 0x0c: {len(pos0c)}, sample positions (rel): {pos0c[:12]}")
|
||||
print(f"Count 0x0d: {len(pos0d)}, sample positions (rel): {pos0d[:12]}")
|
||||
|
||||
# stride detection via start-similarity
|
||||
best = []
|
||||
for stride in range(4,129):
|
||||
n = len(buf)//stride
|
||||
if n < 3:
|
||||
continue
|
||||
matches = 0
|
||||
total = 0
|
||||
for i in range(n-1):
|
||||
a = buf[i*stride:i*stride+8]
|
||||
b = buf[(i+1)*stride:(i+1)*stride+8]
|
||||
total += 8
|
||||
matches += sum(1 for x,y in zip(a,b) if x==y)
|
||||
score = matches/total
|
||||
best.append((score, stride, n))
|
||||
best.sort(reverse=True)
|
||||
print("Top candidate strides (score, stride, record_count):")
|
||||
for s,stride,n in best[:8]:
|
||||
print(f" {s:.3f}, {stride}, {n}")
|
||||
|
||||
if best:
|
||||
top_stride = best[0][1]
|
||||
print(f"\nSample records using stride {top_stride} (showing first 8 bytes of each record):")
|
||||
n = len(buf)//top_stride
|
||||
for i in range(min(n,12)):
|
||||
rec = buf[i*top_stride:(i+1)*top_stride]
|
||||
print(f" rec#{i:02d} @ {base + i*top_stride:08x}: {' '.join(f'{b:02x}' for b in rec[:12])}")
|
||||
|
||||
# look for small incrementing sequences at any fixed offset inside stride
|
||||
def find_incrementing(offset_within, length=6):
|
||||
vals = []
|
||||
for i in range(0, (len(buf)-offset_within)//top_stride):
|
||||
pos = i*top_stride + offset_within
|
||||
vals.append(buf[pos])
|
||||
# find runs of increasing or consistent values
|
||||
if len(vals) < 3:
|
||||
return None
|
||||
return vals[:min(32,len(vals))]
|
||||
|
||||
# search offsets 0..min(32, stride-1)
|
||||
inc_candidates = []
|
||||
for off in range(0, min(32, top_stride)):
|
||||
vals = []
|
||||
nrecs = len(buf)//top_stride
|
||||
for i in range(nrecs):
|
||||
vals.append(buf[i*top_stride + off])
|
||||
# measure monotonic segments
|
||||
diffs = sum(1 for i in range(1,len(vals)) if vals[i] != vals[i-1])
|
||||
if diffs > 0:
|
||||
inc_candidates.append((diffs, off, vals[:16]))
|
||||
inc_candidates.sort(reverse=True)
|
||||
if inc_candidates:
|
||||
print('\nTop changing offsets within stride (changes, offset, sample_values):')
|
||||
for d,off,sample in inc_candidates[:8]:
|
||||
print(f" {d}, {off}, {sample}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
with open(FN, 'rb') as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError:
|
||||
print('ERROR: file not found:', FN)
|
||||
sys.exit(2)
|
||||
|
||||
for off in OFFSETS:
|
||||
start = max(0, off - WINDOW_BEFORE)
|
||||
end = min(len(data), off + WINDOW_AFTER)
|
||||
region = data[start:end]
|
||||
print('\n' + '='*60)
|
||||
print(f"Dump around 0x{off:08x} (file offsets 0x{start:08x}-0x{end:08x})")
|
||||
print(hexdump(region, start))
|
||||
analyze_region(region, start)
|
||||
|
||||
# unified larger window covering the three offsets
|
||||
big_start = max(0, min(OFFSETS) - 0x200)
|
||||
big_end = min(len(data), max(OFFSETS) + 0x300)
|
||||
big = data[big_start:big_end]
|
||||
print('\n' + '='*60)
|
||||
print(f"Unified window 0x{big_start:08x}-0x{big_end:08x}, length {len(big):x}")
|
||||
# run stride search on big window
|
||||
ctr = Counter(big)
|
||||
print('Unified top bytes:', ctr.most_common(12))
|
||||
best = []
|
||||
for stride in range(4,129):
|
||||
n = len(big)//stride
|
||||
if n < 4:
|
||||
continue
|
||||
matches = 0
|
||||
total = 0
|
||||
for i in range(n-1):
|
||||
a = big[i*stride:i*stride+8]
|
||||
b = big[(i+1)*stride:(i+1)*stride+8]
|
||||
total += 8
|
||||
matches += sum(1 for x,y in zip(a,b) if x==y)
|
||||
score = matches/total
|
||||
best.append((score, stride, n))
|
||||
best.sort(reverse=True)
|
||||
print('Unified top candidate strides (score, stride, n):')
|
||||
for s,stride,n in best[:12]:
|
||||
print(f" {s:.3f}, {stride}, {n}")
|
||||
|
||||
# show sample records for top unified stride
|
||||
if best:
|
||||
top = best[0][1]
|
||||
print(f"\nUnified sample records with stride {top}:")
|
||||
n = len(big)//top
|
||||
for i in range(min(n,12)):
|
||||
rec = big[i*top:(i+1)*top]
|
||||
print(f" rec#{i:02d} @ {big_start + i*top:08x}: {' '.join(f'{b:02x}' for b in rec[:16])}")
|
||||
|
||||
print('\nDone')
|
||||
35
scripts/dump_channel_table_at.py
Normal file
35
scripts/dump_channel_table_at.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
fn = os.path.join('binary','Crusader - No Remorse Weapons Main Ram.bin')
|
||||
if not os.path.exists(fn):
|
||||
print('file missing', fn); raise SystemExit(2)
|
||||
with open(fn,'rb') as f:
|
||||
data = f.read()
|
||||
size = len(data)
|
||||
base = 0x6466A
|
||||
stride = 0x26
|
||||
|
||||
def read_name(defOff):
|
||||
end = defOff + stride
|
||||
s = ''
|
||||
for i in range(defOff, min(end, size)):
|
||||
c = data[i]
|
||||
if 32 <= c <= 126:
|
||||
s += chr(c)
|
||||
else:
|
||||
if len(s) >= 2:
|
||||
return s
|
||||
s = ''
|
||||
return s
|
||||
|
||||
for tableStart in (0x64355, 0x64340, 0x64330):
|
||||
print(f'\nDumping table @0x{tableStart:X}')
|
||||
rec = 10
|
||||
for ch in range(40):
|
||||
idxOff = tableStart + ch*rec + 9
|
||||
if idxOff >= size:
|
||||
break
|
||||
sel = data[idxOff]
|
||||
defOff = base + sel*stride
|
||||
name = read_name(defOff) if defOff < size else ''
|
||||
print(f'chan {ch:02d}: sel=0x{sel:02X} ({sel}) -> def@0x{defOff:X} -> {name} (idxOff=0x{idxOff:X})')
|
||||
208
scripts/dump_weapon_commit_table.py
Normal file
208
scripts/dump_weapon_commit_table.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#!/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})')
|
||||
Loading…
Add table
Add a link
Reference in a new issue