This commit is contained in:
MaddoScientisto 2026-04-12 14:45:08 +02:00
commit a9153546ae
56 changed files with 6731 additions and 258 deletions

View file

@ -0,0 +1,8 @@
from PIL import Image
IN = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
OUT = r"K:\ghidra\Crusader_Decomp\binary\crop_weapon_row.png"
box = (80,44,360,92)
img = Image.open(IN)
crop = img.crop(box)
crop.save(OUT)
print('wrote', OUT)

27
tools/find_bytes.py Normal file
View file

@ -0,0 +1,27 @@
import argparse
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--byte', required=True, help='byte value in hex (e.g. 0x0c)')
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000)
p.add_argument('--start', type=lambda x: int(x,0), default=0x80064000)
p.add_argument('--end', type=lambda x: int(x,0), default=0x80064800)
args = p.parse_args()
bval = int(args.byte, 0)
with open(args.file, 'rb') as f:
f.seek(0,2)
size = f.tell()
s = args.start - args.base
e = args.end - args.base
s = max(0, s)
e = min(size, e)
f.seek(s)
data = f.read(e-s)
for i, b in enumerate(data):
if b == bval:
addr = args.base + s + i
print(hex(addr), hex(s+i))
if __name__ == '__main__':
main()

10
tools/find_cd_bytes2.py Normal file
View file

@ -0,0 +1,10 @@
import sys
fname = sys.argv[1]
start = int(sys.argv[2], 0)
length = int(sys.argv[3], 0)
with open(fname, 'rb') as f:
f.seek(start)
d = f.read(length)
for i, b in enumerate(d):
if b in (0x0c, 0x0d):
print(hex(0x80000000 + start + i), hex(start + i), hex(b))

45
tools/hexdump_region.py Normal file
View file

@ -0,0 +1,45 @@
import argparse
import sys
def hexdump(data, base=0, width=16):
for i in range(0, len(data), width):
chunk = data[i:i+width]
hex_bytes = ' '.join(f"{b:02x}" for b in chunk)
ascii_repr = ''.join((chr(b) if 32 <= b < 127 else '.') for b in chunk)
print(f"{base+i:08x}: {hex_bytes:<48} {ascii_repr}")
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--addr', required=True, help='PSX virtual address (e.g. 0x80064355)')
p.add_argument('--before', type=int, default=64)
p.add_argument('--after', type=int, default=256)
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000, help='PSX RAM base address used for dump offset')
args = p.parse_args()
addr = int(args.addr, 0)
base = args.base
offset = addr - base
if offset < 0:
print(f"Computed negative offset {offset} for addr {hex(addr)} base {hex(base)}", file=sys.stderr)
sys.exit(2)
try:
with open(args.file, 'rb') as f:
f.seek(0, 2)
size = f.tell()
if offset >= size:
print(f"Offset {offset:#x} beyond file size {size:#x}")
sys.exit(3)
start = max(0, offset - args.before)
f.seek(start)
data = f.read(args.before + args.after)
print(f"File: {args.file}")
print(f"PSX addr: {hex(addr)}, file offset: {hex(offset)}, dump start: {hex(start)}, len: {len(data)}")
hexdump(data, base=start)
except FileNotFoundError:
print(f"File not found: {args.file}")
sys.exit(4)
if __name__ == '__main__':
main()

107
tools/hud_icon_match.py Normal file
View file

@ -0,0 +1,107 @@
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
# regions
HUD_BOX = (80,44,360,92) # left,top,right,bot
VSTRIP_X0, VSTRIP_X1 = 956, 1023
import os, sys
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build RGB rows
rows = []
for y in range(H):
row = []
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.append((r,g,b))
rows.append(row)
# extract HUD crop
lx,ty,rx,by = HUD_BOX
w = rx-lx; h = by-ty
hud = [[rows[y][x] for x in range(lx,rx)] for y in range(ty,by)]
# build mask for HUD
hud_mask = [[(1 if any(ch!=0 for ch in hud[y][x]) else 0) for x in range(w)] for y in range(h)]
# extract vstrip area and find blobs
vx0,vx1 = VSTRIP_X0, VSTRIP_X1
vw = vx1-vx0+1
vrows = [[rows[y][x] for x in range(vx0,vx1+1)] for y in range(H)]
# mask and flood-fill
mask = [[1 if any(ch!=0 for ch in vrows[y][x]) else 0 for x in range(vw)] for y in range(H)]
visited = [[0]*vw for _ in range(H)]
from collections import deque
blobs = []
for y in range(H):
for x in range(vw):
if mask[y][x] and not visited[y][x]:
q=deque([(x,y)])
visited[y][x]=1
xs=[]; ys=[]
while q:
cx,cy=q.popleft()
xs.append(cx); ys.append(cy)
for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx,ny = cx+dx, cy+dy
if 0<=nx<vw and 0<=ny<H and mask[ny][nx] and not visited[ny][nx]:
visited[ny][nx]=1; q.append((nx,ny))
x0,x1 = min(xs), max(xs)
y0,y1 = min(ys), max(ys)
area = len(xs)
blobs.append((x0,y0,x1,y1,area))
# sort blobs by y (top to bottom)
blobs.sort(key=lambda b: b[1])
print('Found', len(blobs), 'blobs in vstrip')
for i,b in enumerate(blobs):
x0,y0,x1,y1,area = b
print(i, 'blob box (vstrip coords)=', (x0,y0,x1,y1), 'area=',area)
# extract blob images
def extract_from_rows(rr, x0,y0,x1,y1):
w = x1-x0+1; h = y1-y0+1
img = [[rr[y+y0][x+x0] for x in range(w)] for y in range(h)]
return img
blob_imgs = [extract_from_rows(vrows, *b[:4]) for b in blobs]
# template match each blob against hud with sliding window
import math
results = []
for bi, img in enumerate(blob_imgs):
bh = len(img); bw = len(img[0])
if bh<4 or bw<4: continue
best = (1e12, -1,-1)
# convert flattened arrays for speed
tmpl = [c for row in img for px in row for c in px]
for y in range(0, h-bh+1):
for x in range(0, w-bw+1):
ssd=0
for j in range(bh):
for i in range(bw):
r1,g1,b1 = img[j][i]
r2,g2,b2 = hud[j+y][i+x]
dr=r1-r2; dg=g1-g2; db=b1-b2
ssd += dr*dr + dg*dg + db*db
if ssd>best[0]: break
if ssd>best[0]: break
if ssd < best[0]: best = (ssd,x,y)
results.append((bi,bw,bh,best[0],best[1],best[2]))
# sort by score
results.sort(key=lambda x: x[3])
print('\nTop matches:')
for bi,bw,bh,ssd,x,y in results[:10]:
# compute HUD pixel coordinates and vram offsets
hud_x = lx + x; hud_y = ty + y
start_idx = hud_y*W + hud_x
end_idx = (hud_y+bh-1)*W + (hud_x + bw -1)
so = start_idx*2; eo = (end_idx+1)*2 -1
vb = blobs[bi]
v_x0 = vx0 + vb[0]; v_y0 = vb[1]; v_x1 = vx0 + vb[2]; v_y1 = vb[3]
print(f'blob#{bi} vbox=({v_x0},{v_y0})-({v_x1},{v_y1}) size={bw}x{bh} bestssd={ssd} hudpos=({hud_x},{hud_y}) bytes=0x{so:06x}-0x{eo:06x}')
# If results empty, report
if not results:
print('No matches found')

89
tools/hud_row_scan.py Normal file
View file

@ -0,0 +1,89 @@
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
TOP, BOT = 44, 92 # y range (inclusive start, exclusive end)
import os
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build mask for this row range
col_counts = [0]*W
for y in range(TOP, BOT):
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
if val & 0x7fff:
col_counts[x] += 1
# find runs where col_counts exceeds threshold
maxc = max(col_counts)
thr = max(3, int(maxc*0.2))
runs = []
inside=False
for x,c in enumerate(col_counts):
if c>thr and not inside:
sx = x; inside=True
if c<=thr and inside:
ex = x-1; inside=False; runs.append((sx,ex))
if inside: runs.append((sx,W-1))
print('max per-col count', maxc, 'threshold', thr)
print('runs:', runs)
# compute byte offsets and produce crops (no PIL)
import struct, zlib
OUT_DIR = r"K:\ghidra\Crusader_Decomp\binary\hud_candidates"
os.makedirs(OUT_DIR, exist_ok=True)
with open(IN_BIN,'rb') as f:
bin_data = f.read()
# build full rows RGB
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = bin_data[off] | (bin_data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5)&0x1F) << 3
r = ((val >>10)&0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# for each run, create a crop with padding
for i,(sx,ex) in enumerate(runs):
pad = 8
x0 = max(0, sx-pad)
x1 = min(W-1, ex+pad)
y0 = TOP; y1 = BOT-1
w = x1 - x0 + 1
h = y1 - y0 + 1
rawrows = []
for y in range(y0, y1+1):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw,9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
import zlib
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(OUT_DIR, f'cand_{i}_{x0}_{y0}.png')
with open(out,'wb') as f:
f.write(png)
start_idx = y0*W + x0
end_idx = y1*W + x1
so = start_idx*2
eo = (end_idx+1)*2 -1
print(f'wrote {out} box=({x0},{y0})-({x1},{y1}) bytes=0x{so:06x}-0x{eo:06x} col_counts_max={max(col_counts[x0:x1+1])}')
# also print top columns with counts
top_cols = sorted(((c,x) for x,c in enumerate(col_counts)), reverse=True)[:30]
print('\nTop columns (count,x):')
for c,x in top_cols:
print(c,x)

52
tools/make_crop_no_pil.py Normal file
View file

@ -0,0 +1,52 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PNG = r"K:\ghidra\Crusader_Decomp\binary\crop_weapon_row_nopil.png"
W,H = 1024,512
# box: left,top,right,bot (exclusive right/bot matching PIL convention)
left,top,right,bot = 80,44,360,92
w = right - left
h = bot - top
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build rows
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# compose crop raw (PNG filter 0 per row)
rawrows = []
for y in range(top, top+h):
rawrows.append(b"\x00" + rows[y][left*3:(left+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw, level=9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
with open(OUT_PNG, 'wb') as f:
f.write(png)
# compute file offsets
start_idx = top*W + left
end_idx = (top+h-1)*W + (left + w -1)
so = start_idx * 2
eo = ((end_idx)+1)*2 -1
print('wrote', OUT_PNG)
print(f'pixel box=({left},{top})-({left+w-1},{top+h-1}) size={w}x{h}')
print(f'vram byte offsets: 0x{so:06x}-0x{eo:06x} (inclusive)')

View file

@ -0,0 +1,26 @@
import argparse
def main():
p = argparse.ArgumentParser()
p.add_argument('--file', required=True)
p.add_argument('--addr', required=True)
p.add_argument('--base', type=lambda x: int(x,0), default=0x80000000)
p.add_argument('--stride', type=int, default=10)
p.add_argument('--field_idx', type=int, default=9)
p.add_argument('--count', type=int, default=20)
args = p.parse_args()
addr = int(args.addr, 0)
offset = addr - args.base
with open(args.file, 'rb') as f:
for ch in range(args.count):
idx = offset + ch*args.stride + args.field_idx
f.seek(idx)
b = f.read(1)
if not b:
print(f"ch {ch:02d}: EOF")
break
print(f"ch {ch:02d}: addr {hex(addr + ch*args.stride + args.field_idx)} offset {hex(idx)} value {b[0]:02x} ({b[0]})")
if __name__ == '__main__':
main()

View file

@ -0,0 +1,57 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_DIR = r"K:\ghidra\Crusader_Decomp\binary\vstrip_crops"
W,H = 1024,512
x0 = 956
x1 = 1023
w = x1 - x0 + 1
step_h = 48
import math
os.makedirs(OUT_DIR, exist_ok=True)
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build full RGB rows
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# produce crops
import struct, zlib
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
for i in range(0, H, step_h):
y0 = i
y1 = min(H-1, i+step_h-1)
h = y1 - y0 + 1
rawrows = []
for y in range(y0, y1+1):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw,9)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(OUT_DIR, f'vstrip_{y0:03d}_{y1:03d}.png')
with open(out,'wb') as f:
f.write(png)
start_idx = y0*W + x0
end_idx = y1*W + x1
so = start_idx*2
eo = (end_idx+1)*2 -1
print(out, f'box=({x0},{y0})-({x1},{y1}) bytes=0x{so:06x}-0x{eo:06x}')

60
tools/vram_analyze.py Normal file
View file

@ -0,0 +1,60 @@
from collections import deque
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
import os
with open(IN_PATH,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# build mask of non-black pixels
mask = bytearray(W*H)
for i in range(count):
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
if r|g|b:
mask[i] = 1
# flood-fill connected components (4-neigh)
visited = bytearray(W*H)
bbs = []
for idx in range(W*H):
if mask[idx] and not visited[idx]:
q = deque([idx])
visited[idx]=1
xs = []
ys = []
while q:
v = q.popleft()
y = v // W
x = v % W
xs.append(x); ys.append(y)
# neighbors
for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx = x+dx; ny = y+dy
if 0<=nx<W and 0<=ny<H:
ni = ny*W+nx
if mask[ni] and not visited[ni]:
visited[ni]=1
q.append(ni)
x0,x1 = min(xs), max(xs)
y0,y1 = min(ys), max(ys)
area = len(xs)
start_idx = y0*W + x0
end_idx = y1*W + x1
start_off = start_idx*2
end_off = (end_idx+1)*2 - 1
bbs.append((x0,y0,x1,y1,area,start_off,end_off))
# sort by area descending
bbs.sort(key=lambda x: -x[4])
print('Found', len(bbs), 'components')
for i,(x0,y0,x1,y1,area,so,eo) in enumerate(bbs[:20]):
print(f'[{i}] box=({x0},{y0})-({x1},{y1}) area={area} bytes=0x{so:06x}-0x{eo:06x}')
# Save the top few as small BMP crops for inspection
from PIL import Image
img = Image.open(r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png")
for i,(x0,y0,x1,y1,area,so,eo) in enumerate(bbs[:12]):
crop = img.crop((x0,y0,x1+1,y1+1))
out = r"K:\ghidra\Crusader_Decomp\binary\crop_%02d.png"%i
crop.save(out)
print('wrote', out)

60
tools/vram_crop_grid.py Normal file
View file

@ -0,0 +1,60 @@
import struct, zlib, os
IN_BIN = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
IN_PNG = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
W,H = 1024,512
# simple PNG crop writer: read full PNG rows from our previous generated PNG file
# read raw image bytes (we will load via simple approach using original binary->RGB conversion again)
with open(IN_BIN,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
# make full RGB array
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y*W + x
if i < count:
off = i*2
val = data[off] | (data[off+1]<<8)
b = (val & 0x1F) << 3
g = ((val >>5) & 0x1F) << 3
r = ((val >>10) & 0x1F) << 3
else:
r=g=b=0
row.extend([r,g,b])
rows.append(bytes(row))
# crop grid params
xs = list(range(0, W, 128))
ys = list(range(0, 192, 64))
out_dir = r"K:\ghidra\Crusader_Decomp\binary\crops_grid"
os.makedirs(out_dir, exist_ok=True)
import zlib, struct
for y0 in ys:
for x0 in xs:
w = min(256, W-x0)
h = min(128, H-y0)
# build raw rows top-to-bottom
rawrows = []
for y in range(y0, y0+h):
rawrows.append(b"\x00" + rows[y][x0*3:(x0+w)*3])
raw = b"".join(rawrows)
comp = zlib.compress(raw, level=9)
def chunk(t,d):
out = struct.pack('>I', len(d)) + t + d
import zlib
crc = zlib.crc32(t + d) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += chunk(b'IDAT', comp)
png += chunk(b'IEND', b'')
out = os.path.join(out_dir, f'crop_{x0}_{y0}.png')
with open(out,'wb') as f:
f.write(png)
# compute file-offset range
start_idx = y0*W + x0
end_idx = (y0+h-1)*W + (x0+w-1)
so = start_idx*2
eo = (end_idx+1)*2 -1
print(out, f'box=({x0},{y0})-({x0+w-1},{y0+h-1}) bytes=0x{so:06x}-0x{eo:06x}')

View file

@ -0,0 +1,61 @@
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
W,H = 1024,512
with open(IN_PATH,'rb') as f:
data = f.read()
count = min(len(data)//2, W*H)
mask = [0]*(W*H)
for i in range(count):
off = i*2
val = data[off] | (data[off+1]<<8)
if val & 0x7fff:
mask[i]=1
# row sums
row_sums = [sum(mask[y*W:(y+1)*W]) for y in range(H)]
col_sums = [sum(mask[x::W]) for x in range(W)]
# find row bands where row_sum > threshold
thr_row = max(10, int(max(row_sums)*0.05))
runs = []
inside = False
for y, s in enumerate(row_sums):
if s>thr_row and not inside:
start = y; inside=True
if s<=thr_row and inside:
end = y-1; inside=False; runs.append((start,end))
if inside: runs.append((start,H-1))
print('row runs (threshold=%d):'%thr_row, runs)
# print top runs with sums
for (s,e) in runs:
tot = sum(row_sums[s:e+1])
print('run',s,e,'rows=',e-s+1,'sum=',tot)
# find column runs similarly
thr_col = max(5, int(max(col_sums)*0.05))
cruns=[]; inside=False
for x,s in enumerate(col_sums):
if s>thr_col and not inside:
sx=x; inside=True
if s<=thr_col and inside:
ex=x-1; inside=False; cruns.append((sx,ex))
if inside: cruns.append((sx,W-1))
print('col runs (threshold=%d):'%thr_col, cruns[:10])
# Identify intersection boxes by combining top few row runs and col runs
candidates=[]
for (ry0,ry1) in runs:
for (cx0,cx1) in cruns:
# compute density
area = (ry1-ry0+1)*(cx1-cx0+1)
s = 0
for y in range(ry0,ry1+1):
s += sum(mask[y*W+cx0 : y*W+cx1+1])
if s > max(200, area*0.02):
candidates.append((cx0,ry0,cx1,ry1,s,area))
candidates.sort(key=lambda x:-x[4])
print('\nCandidates:')
for i,(x0,y0,x1,y1,s,area) in enumerate(candidates[:12]):
so = (y0*W + x0)*2
eo = ((y1*W + x1)+1)*2 -1
print(f'[{i}] box=({x0},{y0})-({x1},{y1}) area_pixels={area} nonzero={s} bytes=0x{so:06x}-0x{eo:06x}')
# print a few rows around top to locate HUD band
print('\nTop rows with nonzero counts (y:count)')
for y in range(0,160):
if row_sums[y]>0:
print(y, row_sums[y])

60
tools/vram_to_bmp.py Normal file
View file

@ -0,0 +1,60 @@
import struct, os
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PATH = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.bmp"
W, H = 1024, 512
with open(IN_PATH, 'rb') as f:
data = f.read()
exp = W * H * 2
if len(data) < exp:
print(f"Warning: expected {exp} bytes, got {len(data)} bytes")
# read pixels (little-endian 16bpp)
count = min(len(data) // 2, W * H)
pixels = [None] * (W * H)
for i in range(count):
off = i * 2
val = data[off] | (data[off+1] << 8)
b = (val & 0x1F) << 3
g = ((val >> 5) & 0x1F) << 3
r = ((val >> 10) & 0x1F) << 3
pixels[i] = (r, g, b)
# fill remaining if any
for i in range(count, W*H):
pixels[i] = (0,0,0)
# BMP row padding
row_bytes_unpadded = 3 * W
row_size = (row_bytes_unpadded + 3) // 4 * 4
pixel_data = bytearray()
for y in range(H-1, -1, -1):
row_start = y * W
for x in range(W):
r,g,b = pixels[row_start + x]
pixel_data.extend(bytes((b, g, r)))
pad = row_size - row_bytes_unpadded
if pad:
pixel_data.extend(b"\x00" * pad)
# headers
bfType = b'BM'
bfSize = 14 + 40 + len(pixel_data)
bfReserved1 = 0
bfReserved2 = 0
bfOffBits = 14 + 40
bmp = bytearray()
bmp.extend(bfType)
bmp.extend(struct.pack('<IHHI', bfSize, bfReserved1, bfReserved2, bfOffBits))
# DIB header (BITMAPINFOHEADER)
biSize = 40
biWidth = W
biHeight = H
biPlanes = 1
biBitCount = 24
biCompression = 0
biSizeImage = len(pixel_data)
biXPelsPerMeter = 2835
biYPelsPerMeter = 2835
biClrUsed = 0
biClrImportant = 0
bmp.extend(struct.pack('<IIIHHIIIIII', biSize, biWidth, biHeight, biPlanes, biBitCount, biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant))
bmp.extend(pixel_data)
with open(OUT_PATH, 'wb') as f:
f.write(bmp)
print('Wrote', OUT_PATH, 'bytes=', len(bmp))

46
tools/vram_to_png.py Normal file
View file

@ -0,0 +1,46 @@
import struct, zlib
IN_PATH = r"K:\ghidra\Crusader_Decomp\binary\Crusader - No Remorse Memdump Weapons.bin"
OUT_PATH = r"K:\ghidra\Crusader_Decomp\binary\vram_weapons.png"
W, H = 1024, 512
with open(IN_PATH, 'rb') as f:
data = f.read()
exp = W * H * 2
if len(data) < exp:
print(f"Warning: expected {exp} bytes, got {len(data)} bytes")
count = min(len(data) // 2, W * H)
# build raw RGB rows top-to-bottom
rows = []
for y in range(H):
row = bytearray()
for x in range(W):
i = y * W + x
if i < count:
off = i * 2
val = data[off] | (data[off+1] << 8)
b = (val & 0x1F) << 3
g = ((val >> 5) & 0x1F) << 3
r = ((val >> 10) & 0x1F) << 3
else:
r = g = b = 0
row.extend([r, g, b])
rows.append(b"\x00" + bytes(row))
raw = b"".join(rows)
comp = zlib.compress(raw, level=9)
# PNG helpers
def chunk(ch_type, data):
out = struct.pack('>I', len(data)) + ch_type + data
import zlib
crc = zlib.crc32(ch_type + data) & 0xffffffff
out += struct.pack('>I', crc)
return out
png = b"\x89PNG\r\n\x1a\n"
# IHDR
ihdr = struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0) # 8-bit, truecolor, no interlace
png += chunk(b'IHDR', ihdr)
# IDAT
png += chunk(b'IDAT', comp)
# IEND
png += chunk(b'IEND', b'')
with open(OUT_PATH, 'wb') as f:
f.write(png)
print('Wrote', OUT_PATH, 'size=', len(png))