#!/usr/bin/python # Extract assets from a Perfect Dark ROM. # # Place ROM in the main project directory, named pd..z64, then run this # with tools/extract . The directory extracted// 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])