mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 12:00:52 +02:00
121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Raw ROM access for the Pokemon Stadium (US) model export.
|
|
|
|
Everything here is stdlib-only: byte-order fixup, the Yay0 decompressor, the
|
|
PERS-SZP wrapper the assets use, and the little archive format that packs many
|
|
files into one segment. That is all it takes to get from baserom.z64 to model
|
|
data, so the export does not need `make init`, splat or crunch64.
|
|
"""
|
|
import hashlib
|
|
import struct
|
|
|
|
# ROM offsets taken from pokestadium-us.yaml.
|
|
POKEMON_MODELS = 0x920000 # archive of the 215 battle models
|
|
BATTLE_DATA = 0x70D3A0 # per-species battle tables, indexed by D_80075BD0
|
|
MAIN_ROM = 0x1000 # main code segment ...
|
|
MAIN_VRAM = 0x80000400 # ... and where it lands in RAM
|
|
PTR_TABLE_VRAM = 0x80075BD0 # D_80075BD0[species - 1] -> offset into BATTLE_DATA
|
|
|
|
US_MD5 = 'ed1378bc12115f71209a77844965ba50'
|
|
|
|
|
|
class Rom:
|
|
def __init__(self, path):
|
|
data = bytearray(open(path, 'rb').read())
|
|
magic = struct.unpack_from('>I', data, 0)[0]
|
|
if magic == 0x37804012: # .v64, byte-swapped pairs
|
|
data[0::2], data[1::2] = data[1::2], data[0::2]
|
|
elif magic == 0x40123780: # .n64, word-reversed
|
|
data = bytearray(b''.join(data[i:i+4][::-1] for i in range(0, len(data), 4)))
|
|
elif magic != 0x80371240: # .z64, native big endian
|
|
raise ValueError(f'{path}: not an N64 ROM (magic {magic:#010x})')
|
|
self.data = bytes(data)
|
|
self.md5 = hashlib.md5(self.data).hexdigest()
|
|
|
|
@property
|
|
def is_expected_us(self):
|
|
return self.md5 == US_MD5
|
|
|
|
def u32(self, o):
|
|
return struct.unpack_from('>I', self.data, o)[0]
|
|
|
|
def vram_to_rom(self, vram):
|
|
return MAIN_ROM + (vram - MAIN_VRAM)
|
|
|
|
# ---- archive ---------------------------------------------------------
|
|
def archive(self, off):
|
|
"""Segments that hold many files start with
|
|
u32 tag, u32 0, u32 totalSize, u32 fileCount
|
|
followed by fileCount { u32 offset, u32 size, u32 pad[2] } records,
|
|
all relative to the start of the segment (tools/unpack_asset.py).
|
|
Only the top three bytes of the first word are reliably zero -- the
|
|
model archive puts a nonzero value in the low byte."""
|
|
if (self.u32(off) & 0xFFFFFF00) != 0 or self.u32(off + 4) != 0:
|
|
return [self.data[off:]]
|
|
count = self.u32(off + 12)
|
|
if not 0 < count < 4096:
|
|
return [self.data[off:]]
|
|
out = []
|
|
for i in range(count):
|
|
rec = off + 0x10 + i * 0x10
|
|
start, size = self.u32(rec), self.u32(rec + 4)
|
|
out.append(self.data[off + start: off + start + size])
|
|
return out
|
|
|
|
|
|
# ------------------------------------------------------------- decompression
|
|
|
|
def yay0_decompress(src):
|
|
"""Nintendo Yay0. Header: magic, decompressed size, link offset, chunk
|
|
offset; then a bitstream where a 1 copies one literal byte and a 0 pulls a
|
|
(distance, length) pair from the link table."""
|
|
if src[:4] != b'Yay0':
|
|
raise ValueError('not Yay0')
|
|
size, link_off, chunk_off = struct.unpack_from('>3I', src, 4)
|
|
out = bytearray(size)
|
|
mask_p, link_p, chunk_p, pos = 0x10, link_off, chunk_off, 0
|
|
mask, bits = 0, 0
|
|
while pos < size:
|
|
if bits == 0:
|
|
mask = struct.unpack_from('>I', src, mask_p)[0]
|
|
mask_p += 4
|
|
bits = 32
|
|
if mask & 0x80000000:
|
|
out[pos] = src[chunk_p]
|
|
chunk_p += 1
|
|
pos += 1
|
|
else:
|
|
link = struct.unpack_from('>H', src, link_p)[0]
|
|
link_p += 2
|
|
dist = link & 0x0FFF
|
|
count = link >> 12
|
|
if count == 0:
|
|
count = src[chunk_p] + 0x12
|
|
chunk_p += 1
|
|
else:
|
|
count += 2
|
|
copy = pos - dist - 1
|
|
for _ in range(count): # overlapping runs are legal
|
|
out[pos] = out[copy]
|
|
pos += 1
|
|
copy += 1
|
|
mask = (mask << 1) & 0xFFFFFFFF
|
|
bits -= 1
|
|
return bytes(out)
|
|
|
|
|
|
def decompress(blob):
|
|
"""Unwrap whatever container an asset arrived in."""
|
|
if blob[:8] == b'PERS-SZP':
|
|
header = struct.unpack_from('>I', blob, 8)[0]
|
|
return yay0_decompress(blob[header:])
|
|
if blob[:4] == b'Yay0':
|
|
return yay0_decompress(blob)
|
|
return blob
|
|
|
|
|
|
def pokemon_models(rom):
|
|
"""Returns the decompressed model fragments, indexed by file number."""
|
|
return [decompress(b) for b in rom.archive(POKEMON_MODELS)]
|