1
0
Fork 0
mirror of https://github.com/notwa/mm synced 2024-11-05 06:29:02 -08:00
mm/z64dump.py

332 lines
8.9 KiB
Python
Raw Normal View History

2015-11-29 06:13:42 -08:00
#!/usr/bin/env python3
2015-03-01 07:58:42 -08:00
import sys
2015-03-02 07:02:11 -08:00
import os, os.path
from io import BytesIO
from hashlib import sha1
2015-03-01 07:58:42 -08:00
# check for cython
try:
import pyximport
except ImportError:
fast = False
else:
pyximport.install()
fast = True
if fast:
import Yaz0_fast as Yaz0
import n64_fast as n64
else:
import Yaz0
import n64
2015-03-02 07:02:11 -08:00
from util import *
from heuristics import detect_format
2015-03-01 07:58:42 -08:00
lament = lambda *args, **kwargs: print(*args, file=sys.stderr, **kwargs)
# shoutouts to spinout182
2015-03-01 07:58:42 -08:00
# assume first entry is makerom (0x1060), and second entry begins from makerom
2015-03-02 07:02:11 -08:00
dma_sig = b"\x00\x00\x00\x00\x00\x00\x10\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x60"
2015-03-01 07:58:42 -08:00
2015-03-04 04:37:37 -08:00
def dump_wrap(data, fn, size):
2016-09-28 20:54:17 -07:00
try:
kind = detect_format(BytesIO(data), fn)
except Exception as e:
lament(fn, e)
kind = None
2015-03-04 04:37:37 -08:00
if kind is not None:
fn += '.' + kind
dump_as(data, fn, size)
def z_dump_file(f, i=0, name=None, uncompress=True):
2015-03-01 07:58:42 -08:00
vs = R4(f.read(4)) # virtual start
ve = R4(f.read(4)) # virtual end
ps = R4(f.read(4)) # physical start
pe = R4(f.read(4)) # physical end
here = f.tell()
2016-04-04 07:37:08 -07:00
dump = uncompress and dump_wrap or dump_as
2015-03-01 07:58:42 -08:00
if vs == ve == ps == pe == 0:
return False
2015-03-03 00:40:28 -08:00
# ve inferred from filesize, and we're making pe be 0
# ps can just be the end of the last file
fn = '{:04} V{:08X}'.format(i, vs)
if name is not None and name is not '':
2015-03-03 00:40:28 -08:00
fn = fn + ' ' + str(name)
2015-03-01 07:58:42 -08:00
size = ve - vs
if ps == 0xFFFFFFFF or pe == 0xFFFFFFFF:
#lament('file does not exist')
2016-04-04 07:37:08 -07:00
dump_as(b'', fn, 0)
2015-03-01 07:58:42 -08:00
elif pe == 0:
#lament('file is uncompressed')
pe = ps + size
f.seek(ps)
data = f.read(pe - ps)
2016-04-04 07:37:08 -07:00
dump(data, fn, size)
2015-03-01 07:58:42 -08:00
else:
#lament('file is compressed')
f.seek(ps)
compressed = f.read(pe - ps)
if compressed[:4] == b'Yaz0':
if uncompress:
data = Yaz0.decode(compressed)
2016-04-04 07:37:08 -07:00
dump(data, fn, size)
else:
2016-04-04 07:37:08 -07:00
dump(compressed, fn+'.Yaz0', len(compressed))
2015-03-01 07:58:42 -08:00
else:
if uncompress:
lament('unknown compression; skipping:', fn)
lament(compressed[:4])
else:
lament('unknown compression:', fn)
2016-04-04 07:37:08 -07:00
dump(compressed, fn, len(compressed))
2015-03-01 07:58:42 -08:00
f.seek(here)
2015-03-04 04:37:37 -08:00
return True
2015-03-01 07:58:42 -08:00
2015-03-02 07:02:11 -08:00
def z_find_dma(f):
2015-03-01 07:58:42 -08:00
while True:
# assume row alignment
data = f.read(16)
if len(data) == 0: # EOF
break
2015-03-02 07:02:11 -08:00
if data == dma_sig[:16]:
rest = dma_sig[16:]
2015-03-01 07:58:42 -08:00
if f.read(len(rest)) == rest:
return f.tell() - len(rest) - 16
else:
f.seek(len(rest), 1)
2016-04-04 07:37:08 -07:00
def z_dump(f, names=None, uncompress=True):
2015-03-03 00:40:28 -08:00
f.seek(0x1060) # skip header when finding dmatable
2015-03-02 07:02:11 -08:00
addr = z_find_dma(f)
2015-03-01 07:58:42 -08:00
if addr == None:
2015-03-02 07:02:11 -08:00
lament("couldn't find file offset table")
return
2015-03-01 07:58:42 -08:00
f.seek(addr - 0x30)
build = f.read(0x30).strip(b'\x00').replace(b'\x00', b'\n')
lament(str(build, 'utf-8'))
f.seek(addr)
i = 0
2015-03-03 00:40:28 -08:00
if names:
for n in names:
2016-04-04 07:37:08 -07:00
if z_dump_file(f, i, n, uncompress):
2015-03-03 00:40:28 -08:00
i += 1
else:
lament("ran out of filenames")
break
2016-04-04 07:37:08 -07:00
while z_dump_file(f, i, None, uncompress):
2015-03-01 07:58:42 -08:00
i += 1
2015-03-03 00:40:28 -08:00
if names and i > len(names):
lament("extraneous filenames")
2015-03-01 07:58:42 -08:00
2016-04-04 07:37:08 -07:00
def dump_rom(fn, uncompress=True):
2015-03-01 07:58:42 -08:00
with open(fn, 'rb') as f:
data = f.read()
2015-03-02 07:02:11 -08:00
with BytesIO(data) as f:
2015-03-01 19:52:12 -08:00
start = f.read(4)
if start == b'\x37\x80\x40\x12':
swap_order(f)
elif start != b'\x80\x37\x12\x40':
2015-03-01 07:58:42 -08:00
lament('not a .z64:', fn)
return
2015-03-01 19:52:12 -08:00
f.seek(0)
2015-03-03 00:40:28 -08:00
romhash = sha1(f.read()).hexdigest()
names = None
if romhash == '50bebedad9e0f10746a52b07239e47fa6c284d03':
# OoT debug rom filenames
f.seek(0xBE80)
names = f.read(0x6490).split(b'\x00')
names = [str(n, 'utf-8') for n in names if n != b'']
if romhash in (
# NTSC 1.0 (U) and (J)
'ad69c91157f6705e8ab06c79fe08aad47bb57ba7',
'c892bbda3993e66bd0d56a10ecd30b1ee612210f',
# NTSC 1.1 (U) and (J)
'd3ecb253776cd847a5aa63d859d8c89a2f37b364',
'dbfc81f655187dc6fefd93fa6798face770d579d',
# NTSC 1.2 (U) and (J)
'41b3bdc48d98c48529219919015a1af22f5057c2',
'fa5f5942b27480d60243c2d52c0e93e26b9e6b86',
):
# filenames inferred from debug rom
with open('fn O US10.txt') as f2:
names = f2.readlines()
names = [n.strip() for n in names]
2015-03-03 00:40:28 -08:00
with SubDir(romhash):
2015-03-01 14:16:30 -08:00
f.seek(0)
2016-04-04 07:37:08 -07:00
z_dump(f, names, uncompress)
2015-03-01 07:58:42 -08:00
def z_read_file(path, fn=None):
if fn == None:
2015-03-01 19:52:12 -08:00
fn = os.path.basename(path)
2015-03-02 07:02:11 -08:00
2015-03-03 00:40:28 -08:00
if len(fn) < 14:
return False, None, None
2015-03-01 07:58:42 -08:00
2015-03-03 00:40:28 -08:00
fn = str(fn[:14])
2015-03-01 07:58:42 -08:00
2015-03-03 00:40:28 -08:00
if fn[4:6] != ' V':
return False, None, None
2015-03-01 07:58:42 -08:00
try:
2015-03-03 00:40:28 -08:00
vs = int(fn[ 6: 14], 16)
2015-03-01 07:58:42 -08:00
except ValueError:
2015-03-03 00:40:28 -08:00
return False, None, None
2015-03-01 07:58:42 -08:00
with open(path, 'rb') as f:
data = f.read()
2015-03-03 00:40:28 -08:00
return True, data, vs
2015-03-01 07:58:42 -08:00
2015-03-02 07:02:11 -08:00
def z_write_dma(f, dma):
dma.sort(key=lambda vf: vf[0]) # sort by vs
2016-09-28 20:54:17 -07:00
assert len(dma) > 2
2015-03-02 07:02:11 -08:00
dma_entry = dma[2] # assumption
vs, ve, ps, pe = dma_entry
# initialize with zeros
dma_size = ve - vs
f.seek(ps)
f.write(bytearray(dma_size))
f.seek(ps)
for vf in dma:
vs, ve, ps, pe = vf
#lament('{:08X} {:08X} {:08X} {:08X}'.format(vs, ve, ps, pe))
f.write(W4(vs))
f.write(W4(ve))
f.write(W4(ps))
f.write(W4(pe))
2016-09-28 20:54:17 -07:00
assert f.tell() <= (pe or ve)
2015-03-02 07:02:11 -08:00
def fix_rom(f):
bootcode = n64.bootcode_version(f)
lament('bootcode:', bootcode)
crc1, crc2 = n64.crc(f, bootcode)
lament('crcs: {:08X} {:08X}'.format(crc1, crc2))
f.seek(0x10)
f.write(W4(crc1))
f.write(W4(crc2))
2016-04-04 07:37:08 -07:00
def align(x):
return (x + 15) // 16 * 16
def create_rom(d, compress=False):
2015-03-02 07:02:11 -08:00
root, _, files = next(os.walk(d))
2015-03-03 00:40:28 -08:00
files.sort()
2015-03-01 07:58:42 -08:00
rom_size = 64*1024*1024
with open(d+'.z64', 'w+b') as f:
2015-03-02 07:02:11 -08:00
dma = []
# initialize with zeros
2015-03-01 07:58:42 -08:00
f.write(bytearray(rom_size))
f.seek(0)
2016-04-04 07:37:08 -07:00
start_v = 0
start_p = 0
2015-03-03 00:40:28 -08:00
for i, fn in enumerate(files):
2015-03-01 07:58:42 -08:00
path = os.path.join(root, fn)
2015-03-03 00:40:28 -08:00
success, data, vs = z_read_file(path, fn)
2015-03-01 07:58:42 -08:00
if not success:
lament('skipping:', fn)
continue
2016-04-04 07:37:08 -07:00
size_v = len(data)
size_p = size_v
unempty = size_v > 0
compressed = size_v >= 4 and data[:4] == b'Yaz0'
2015-03-03 00:40:28 -08:00
if i <= 2:
# makerom, boot, dmadata need to be exactly where they were
2016-04-04 07:37:08 -07:00
start_v = vs
start_p = start_v
2015-03-01 07:58:42 -08:00
else:
2016-04-04 07:37:08 -07:00
start_v = align(start_v)
start_p = align(start_p)
if compress and unempty:
lament('Comp…: {}'.format(fn))
data = Yaz0.encode(data)
size_p = len(data)
lament("Ratio: {:3}%".format(int(size_p / size_v * 100)))
compressed = True
if unempty:
ps = start_p
if compressed:
pe = align(start_p + size_p)
ve = vs + int.from_bytes(data[4:8], 'big')
else:
pe = 0
ve = vs + size_v
2015-03-03 00:40:28 -08:00
else:
ps = 0xFFFFFFFF
pe = 0xFFFFFFFF
2016-04-04 07:37:08 -07:00
ve = vs
2016-09-28 20:54:17 -07:00
assert start_v <= rom_size
assert start_v + size_v <= rom_size
assert vs < rom_size
assert ve <= rom_size
# TODO: do we really want to do any of this?
# i'm not sure how picky the game is with the dmatable.
#ve = align(ve)
#print(fn)
assert vs % 0x10 == 0
assert ve % 0x10 == 0
2015-03-03 00:40:28 -08:00
2016-04-04 07:37:08 -07:00
if unempty:
f.seek(start_p)
2015-03-01 07:58:42 -08:00
f.write(data)
2015-03-02 07:02:11 -08:00
dma.append([vs, ve, ps, pe])
2016-04-04 07:37:08 -07:00
start_v += size_v
start_p += size_p
2015-03-01 07:58:42 -08:00
2015-03-02 07:02:11 -08:00
z_write_dma(f, dma)
fix_rom(f)
2015-03-01 07:58:42 -08:00
def run(args):
2016-04-04 07:37:08 -07:00
compress = False
2016-04-09 04:17:15 -07:00
fix = False
2015-03-01 07:58:42 -08:00
for path in args:
2016-04-04 07:37:08 -07:00
if path == '-c':
compress = not compress
continue
2016-04-09 04:17:15 -07:00
if path == '-f':
fix = not fix
continue
if fix:
with open(path, 'r+b') as f:
fix_rom(f)
continue
2015-03-02 07:02:11 -08:00
# directories are technically files, so check this first
2015-03-01 07:58:42 -08:00
if os.path.isdir(path):
2016-04-04 07:37:08 -07:00
create_rom(path, compress)
2015-03-01 07:58:42 -08:00
elif os.path.isfile(path):
2016-04-04 07:37:08 -07:00
dump_rom(path, not compress)
2015-03-01 07:58:42 -08:00
else:
lament('no-op:', path)
if __name__ == '__main__':
try:
ret = run(sys.argv[1:])
2015-03-02 07:02:11 -08:00
sys.exit(ret)
2015-03-01 07:58:42 -08:00
except KeyboardInterrupt:
sys.exit(1)