perfect_dark/tools/extract

319 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
import os, 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.data = self.decompress(self.rom[self.val('data'):])
self.extract_all()
def extract_all(self):
self.extract_accessingpak()
self.extract_animations()
self.extract_audio()
self.extract_boot()
self.extract_copyright()
self.extract_data()
self.extract_files()
self.extract_firingrange()
self.extract_fonts()
self.extract_game()
self.extract_garbage1()
self.extract_garbage2()
self.extract_lib()
self.extract_mpconfigs()
self.extract_mpstrings()
self.extract_rspboot()
self.extract_textures()
self.extract_textureconfig()
self.extract_unknown2()
def extract_accessingpak(self):
# ntsc-beta doesn't have this texture
if self.romid != 'ntsc-beta':
addr = self.val('copyright') + 0xb30
data = self.decompress(self.rom[addr:addr+0x8b0])
self.write('segments/accessingpak.bin', data)
def extract_animations(self):
start = self.val('animations')
end = self.val('mpconfigs')
self.write('segments/animations.bin', self.rom[start:end])
def extract_audio(self):
sfxctl = self.val('sfxctl')
sfxtbl = sfxctl + 0x2fb80
seqctl = sfxtbl + 0x4c2160
seqtbl = seqctl + 0xa060
sequencestbl = seqtbl + 0x17c070
self.write('segments/sfx.ctl.bin', self.rom[sfxctl:sfxtbl])
self.write('segments/sfx.tbl.bin', self.rom[sfxtbl:seqctl])
self.write('segments/seq.ctl.bin', self.rom[seqctl:seqtbl])
self.write('segments/seq.tbl.bin', self.rom[seqtbl:sequencestbl])
length = 0x563b0 if self.romid == 'ntsc-beta' else 0x563a0
sequences = self.rom[sequencestbl:sequencestbl+length]
self.write('segments/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('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:])
def extract_boot(self):
self.write('segments/boot.bin', self.rom[0x1000:0x3050])
def extract_copyright(self):
addr = self.val('copyright')
data = self.decompress(self.rom[addr:addr+0xb30])
self.write('segments/copyright.bin', data)
def extract_data(self):
self.write('segments/data.bin', self.data)
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]
# If the content is zipped then the last byte might be padding. So
# unzip it to see.
unzipped = None
if content[0:2] == b'\x11\x73':
(unzipped, unused) = self.decompressandgetunused(content)
if len(unused):
content = content[:-len(unused)]
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, unzipped)
if name.startswith('C'):
self.write('files/chrs/%s.bin' % unzippedname, unzipped)
if name.startswith('G'):
self.write('files/guns/%s.bin' % unzippedname, unzipped)
if name.startswith('L'):
self.write('files/lang/%s.bin' % unzippedname, unzipped)
if name.startswith('P') and len(content):
self.write('files/props/%s.bin' % unzippedname, unzipped)
if name.startswith('U'):
self.write('files/setup/%s.bin' % unzippedname, unzipped)
def get_file_offsets(self):
i = self.val('files')
offsets = []
while True:
offset = int.from_bytes(self.data[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')
def extract_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('segments/game.bin', binary)
def extract_inflate(self):
self.write('segments/inflate.bin', self.rom[0x4e850:0x4fc40])
def extract_garbage1(self):
start = self.val('garbage1')
end = self.val('data')
self.write('segments/garbage1.bin', self.rom[start:end])
def extract_garbage2(self):
start = self.val('garbage2')
end = self.val('animations')
self.write('segments/garbage2.bin', self.rom[start:end])
def extract_lib(self):
self.write('segments/lib.bin', self.decompress(self.rom[0x3050:]))
def extract_mpconfigs(self):
addr = self.val('mpconfigs')
self.write('segments/mpconfigs.bin', self.rom[addr:addr+0x68*44])
def extract_mpstrings(self):
self.extract_mpstrings_lang(0, 'E')
self.extract_mpstrings_lang(1, 'J')
self.extract_mpstrings_lang(2, 'P')
self.extract_mpstrings_lang(3, 'G')
self.extract_mpstrings_lang(4, 'F')
self.extract_mpstrings_lang(5, 'S')
self.extract_mpstrings_lang(6, 'I')
def extract_mpstrings_lang(self, index, lang):
addr = self.val('mpconfigs') + 0x68 * 44 + 0x3700 * index
self.write('segments/mpstrings%s.bin' % lang, self.rom[addr:addr+0x3700])
def extract_firingrange(self):
addr = self.val('firingrange')
self.write('segments/firingrange.bin', self.rom[addr:addr+0x1550])
def extract_font(self, startvar, endvar, fontname):
start = self.val(startvar)
end = self.val(endvar)
self.write('segments/font%s.bin' % fontname, self.rom[start:end])
def extract_fonts(self):
self.extract_font('font0', 'font1', 'bankgothic')
self.extract_font('font1', 'font2', 'zurich')
self.extract_font('font2', 'font3', 'tahoma')
self.extract_font('font3', 'font4', 'numeric')
self.extract_font('font4', 'font5', 'handelgothicsm')
self.extract_font('font5', 'font6', 'handelgothicxs')
self.extract_font('font6', 'font7', 'handelgothicmd')
self.extract_font('font7', 'font8', 'handelgothiclg')
self.extract_font('font8', 'font9', 'ocramd')
self.extract_font('font9', 'sfxctl', 'ocralg')
def extract_rspboot(self):
self.write('segments/rspboot.bin', self.rom[0x40:0x1000])
def extract_textures(self):
base = self.val('textures')
datalen = 0x294960 if self.romid == 'jpn-final' else 0x291d60
tablepos = base + datalen
index = 0
while True:
start = int.from_bytes(self.rom[tablepos+1:tablepos+4], 'big')
end = int.from_bytes(self.rom[tablepos+9:tablepos+12], 'big')
if int.from_bytes(self.rom[tablepos+12:tablepos+16], 'big') != 0:
break
texturedata = self.rom[base+start:base+end]
self.write('textures/%04x.bin' % index, texturedata)
index += 1
tablepos += 8
tablepos += 8
self.write('segments/textures.bin', self.rom[base:tablepos])
def extract_textureconfig(self):
addr = self.val('textureconfig')
self.write('segments/textureconfig.bin', self.rom[addr:addr+0xb50])
def extract_unknown2(self):
addr = self.val('textureconfig') + 0xb50
self.write('segments/unknown2.bin', self.rom[addr:addr+0x65d0])
#
# 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 decompressandgetunused(self, buffer):
header = int.from_bytes(buffer[0:2], 'big')
assert(header == 0x1173)
obj = zlib.decompressobj(wbits=-15)
bin = obj.decompress(buffer[5:])
return (bin, obj.unused_data)
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):
index = ['ntsc-beta','ntsc-1.0','ntsc-final','pal-beta','pal-final','jpn-final'].index(self.romid)
return self.vals[name][index]
vals = {
# ntsc-beta ntsc-1.0 ntsc-final pal-beta pal-final jpn-final
'game': [0x43c40, 0x4fc40, 0x4fc40, 0x4fc40, 0x4fc40, 0x4fc40, ],
'garbage1': [0x0, 0x2ea22, 0x2ea6c, 0x0, 0x0, 0x0, ],
'files': [0x29160, 0x28080, 0x28080, 0x29b90, 0x28910, 0x28800, ],
'data': [0x30850, 0x39850, 0x39850, 0x39850, 0x39850, 0x39850, ],
'animations': [0x155dc0, 0x1a15c0, 0x1a15c0, 0x18cdc0, 0x18cdc0, 0x190c50, ],
'garbage2': [0x0, 0x1574a0, 0x157800, 0x0, 0x0, 0x0, ],
'mpconfigs': [0x785130, 0x7d0a40, 0x7d0a40, 0x7bc240, 0x7bc240, 0x7c00d0, ],
'firingrange': [0x79e410, 0x7e9d20, 0x7e9d20, 0x7d5520, 0x7d5520, 0x7d93b0, ],
'textureconfig': [0x79f960, 0x7eb270, 0x7eb270, 0x7d6a70, 0x7d6a70, 0x7da900, ],
'font0': [0x7f2390, 0x7f2390, 0x7f2390, 0x7ddb90, 0x7ddb90, 0x7e1a20, ],
'font1': [0x7f4930, 0x7f4930, 0x7f4930, 0x7e0130, 0x7e0130, 0x7e3fc0, ],
'font2': [0x7f7860, 0x7f7860, 0x7f7860, 0x7e3060, 0x7e3060, 0x7e6ef0, ],
'font3': [0x7f8b20, 0x7f8b20, 0x7f8b20, 0x7e4320, 0x7e4320, 0x7e81b0, ],
'font4': [0x7f9d30, 0x7f9d30, 0x7f9d30, 0x7e5530, 0x7e5530, 0x7e93b0, ],
'font5': [0x7fbfb0, 0x7fbfb0, 0x7fbfb0, 0x7e87b0, 0x7e87b0, 0x7ec640, ],
'font6': [0x7fdd80, 0x7fdd80, 0x7fdd80, 0x7eae20, 0x7eae20, 0x7eecb0, ],
'font7': [0x8008e0, 0x8008e0, 0x8008e0, 0x7eee70, 0x7eee70, 0x7f2d00, ],
'font8': [0x803da0, 0x803da0, 0x803da0, 0x7f2330, 0x7f2330, 0x7f61c0, ],
'font9': [0x806ac0, 0x806ac0, 0x806ac0, 0x7f5050, 0x7f5050, 0x7f8ee0, ],
'sfxctl': [0x7be940, 0x80a250, 0x80a250, 0x7f87e0, 0x7f87e0, 0x7fc670, ],
'textures': [0x1d12fe0, 0x1d65f40, 0x1d65f40, 0x1d5bb50, 0x1d5ca20, 0x1d61f90, ],
'copyright': [0x1fabac0, 0x1ffea20, 0x1ffea20, 0x1ff4630, 0x1ff5500, 0x1ffd6b0, ],
}
extractor = Extractor()
extractor.extract(os.environ['ROMID'])