perfect_dark/tools/extract

252 lines
6.7 KiB
Python
Executable File

#!/usr/bin/python
# Extract assets from a Perfect Dark ROM.
#
# Place ROM in the main project directory, named pd.<romid>.z64, then run this
# with tools/extract <romid>. The directory extracted/<romid>/ will be created
# and populated.
#
# Supported ROM IDs are:
# ntsc-beta
# ntsc-1.0
# ntsc-final
# pal-beta
# pal-final
# jap-final
import os, sys, zlib
class Extractor:
def extract(self, romid):
self.romid = romid
filename = 'pd.%s.z64' % romid
fd = open(filename, 'rb')
self.rom = fd.read()
fd.close()
self.setup = self.decompress(self.rom[self.val('setup'):])
self.extract_all()
def extract_all(self):
self.extract_audio()
self.extract_files()
self.extract_fonts()
self.extract_globals()
self.extract_textures()
self.extract_ucodes()
#
# Audio
#
def extract_audio(self):
sfxctl = self.val('sfxctl')
sfxtbl = sfxctl + 0x2fb80
musicctl = sfxtbl + 0x4c2160
musictbl = musicctl + 0xa060
seqtbl = musictbl + 0x17c070
self.write('audio/sfx.ctl', self.rom[sfxctl:sfxtbl])
self.write('audio/sfx.tbl', self.rom[sfxtbl:musicctl])
self.write('audio/music.ctl', self.rom[musicctl:musictbl])
self.write('audio/music.tbl', self.rom[musictbl:seqtbl])
seqtbllen = 0x563b0 if self.romid == 'ntsc-beta' else 0x563a0
sequences = self.rom[seqtbl:seqtbl+seqtbllen]
self.write('audio/sequences.bin', sequences)
# Extract sequences
count = int.from_bytes(sequences[0:2], 'big')
i = 0
while i < count:
sequence = self.extract_sequence(sequences, i)
self.write('audio/sequences/%03d.seq' % i, sequence)
i += 1
def extract_sequence(self, sequences, index):
pos = 4 + index * 8
offset = int.from_bytes(sequences[pos:pos+4], 'big')
return self.decompress(sequences[offset:])
#
# Files
#
def extract_files(self):
offsets = self.get_file_offsets()
names = self.get_file_names(offsets[len(offsets) - 1])
for (index, offset) in enumerate(offsets):
if index == 0:
continue
try:
endoffset = offsets[index + 1]
except:
return
content = self.rom[offset:endoffset]
name = names[index]
self.write('files/%s' % name, content)
unzippedname = name[:-1] if name.endswith('Z') else name
if name.endswith('tilesZ'):
self.write('files/bgdata/%s.bin' % unzippedname, self.decompress(content))
if name.startswith('C'):
self.write('files/chrs/%s.bin' % unzippedname, self.decompress(content))
if name.startswith('G'):
self.write('files/guns/%s.bin' % unzippedname, self.decompress(content))
if name.startswith('L'):
self.write('files/lang/%s.bin' % unzippedname, self.decompress(content))
if name.startswith('P') and len(content):
self.write('files/props/%s.bin' % unzippedname, self.decompress(content))
if name.startswith('U'):
self.write('files/setup/%s.bin' % unzippedname, self.decompress(content))
def get_file_offsets(self):
i = self.val('files')
offsets = []
while True:
offset = int.from_bytes(self.setup[i:i+4], 'big')
if offset == 0 and len(offsets):
return offsets
offsets.append(offset)
i += 4
def get_file_names(self, tableaddr):
i = tableaddr
names = []
while True:
offset = int.from_bytes(self.rom[i:i+4], 'big')
if offset == 0 and len(names):
return names
names.append(self.read_name(tableaddr + offset))
i += 4
def read_name(self, address):
nullpos = self.rom[address:].index(0)
return str(self.rom[address:address + nullpos], 'utf-8')
#
# Fonts
#
def extract_fonts(self):
# Not implemented
pass
#
# Globals
#
def extract_globals(self):
self.write('ucode/setup.bin', self.setup)
#
# Textures
#
def extract_textures(self):
# Not implemented
pass
#
# Ucodes
#
def extract_ucodes(self):
self.write('ucode/rspboot.bin', self.rom[0x40:0x1000])
self.write('ucode/boot.bin', self.rom[0x1000:0x3050])
self.write('ucode/library.bin', self.decompress(self.rom[0x3050:]))
self.write('ucode/rarezip.bin', self.rom[0x4e850:0x4fc40])
self.extract_ucode_game()
def extract_ucode_game(self):
binary = bytes()
start = i = self.val('game')
while True:
romoffset = start + int.from_bytes(self.rom[i:i+4], 'big') + 2
peek = int.from_bytes(self.rom[romoffset:romoffset+2], 'big')
if peek == 0:
break
part = self.decompress(self.rom[romoffset:romoffset+0x1000])
binary += part
if len(part) != 0x1000:
break
i += 4
self.write('ucode/game.bin', binary)
#
# Misc functions
#
def decompress(self, buffer):
header = int.from_bytes(buffer[0:2], 'big')
assert(header == 0x1173)
return zlib.decompress(buffer[5:], wbits=-15)
def write(self, filename, data):
filename = 'extracted/%s/%s' % (self.romid, filename)
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
fd = open(filename, 'wb')
fd.write(data)
fd.close()
def val(self, name):
return self.vals[self.romid][name]
vals = {
'ntsc-final': {
'game': 0x4fc40,
'files': 0x28080,
'setup': 0x39850,
'sfxctl': 0x80a250,
},
'ntsc-1.0': {
'game': 0x4fc40,
'files': 0x28080,
'setup': 0x39850,
'sfxctl': 0x80a250,
},
'ntsc-beta': {
'game': 0x43c40,
'files': 0x29160,
'setup': 0x30850,
'sfxctl': 0x7be940,
},
'pal-final': {
'game': 0x4fc40,
'files': 0x28910,
'setup': 0x39850,
'sfxctl': 0x7f87e0,
},
'pal-beta': {
'game': 0x4fc40,
'files': 0x29b90,
'setup': 0x39850,
'sfxctl': 0x7f87e0,
},
'jap-final': {
'game': 0x4fc40,
'files': 0x28800,
'setup': 0x39850,
'sfxctl': 0x7fc670,
},
}
extractor = Extractor()
extractor.extract(sys.argv[1])