Add preliminary PS1 DDR support
This commit is contained in:
parent
318cbd1e92
commit
7983f56dc6
215
filedata-tool.py
215
filedata-tool.py
|
@ -9,48 +9,173 @@ import csv
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
class FileData:
|
class FileData:
|
||||||
|
# Length of the PS-X EXE is used as the key
|
||||||
|
table_info_ps1 = {
|
||||||
|
0x149000: {
|
||||||
|
'name': 'DDR 1st (JP)',
|
||||||
|
'offset': 0x5da88,
|
||||||
|
'num_entries': 95,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x15e800: {
|
||||||
|
'name': 'DDR 2nd Remix (JP)',
|
||||||
|
'offset': 0x672c8,
|
||||||
|
'num_entries': 171,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x8b800: {
|
||||||
|
'name': 'DDR 2nd Remix - Append Club Ver 1 (JP)',
|
||||||
|
'offset': 0x5b260,
|
||||||
|
'num_entries': 122,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x8c000: {
|
||||||
|
'name': 'DDR 2nd Remix - Append Club Ver 2 (JP)',
|
||||||
|
'offset': 0x5b9f8,
|
||||||
|
'num_entries': 119,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xd1000: {
|
||||||
|
'name': 'DDR 3rd (DDR1_APD.EXE) (JP)', # READ_DT2.BIN
|
||||||
|
'offset': 0x7cd38,
|
||||||
|
'num_entries': 3,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xcc800: {
|
||||||
|
'name': 'DDR 3rd (DDR2_APD.EXE) (JP)', # READ_DT2.BIN
|
||||||
|
'offset': 0x7c948,
|
||||||
|
'num_entries': 3,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xb6800: {
|
||||||
|
'name': 'DDR 3rd (JP)',
|
||||||
|
'offset': 0x752ac,
|
||||||
|
'num_entries': 269,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xcf800: {
|
||||||
|
'name': 'DDR 4th (JP)',
|
||||||
|
'offset': 0x85194,
|
||||||
|
'num_entries': 201,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x101000: {
|
||||||
|
'name': 'DDR 5th (JP)',
|
||||||
|
'offset': 0x929f8,
|
||||||
|
'num_entries': 445,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xe5800: {
|
||||||
|
'name': 'DDR Best Hits (JP)',
|
||||||
|
'offset': 0x85efc,
|
||||||
|
'num_entries': 141,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x8001f630: { # Clashes with DDR 4th so use entry point addr instead
|
||||||
|
'name': 'DDR Extra Mix (JP)',
|
||||||
|
'offset': 0x83a7c,
|
||||||
|
'num_entries': 231,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xcf000: {
|
||||||
|
'name': 'DDR Konamix (US)',
|
||||||
|
'offset': 0x839f0,
|
||||||
|
'num_entries': 178,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xe1800: {
|
||||||
|
'name': 'DDR USA Mix (US)',
|
||||||
|
'offset': 0x85718,
|
||||||
|
'num_entries': 138,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x8001eb4c: { # Clashes with DDR Konamix so use entry point addr instead
|
||||||
|
'name': 'DS Party Edition (EU)',
|
||||||
|
'offset': 0x84bec,
|
||||||
|
'num_entries': 173,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xd2000: {
|
||||||
|
'name': 'DS Euromix (EU)',
|
||||||
|
'offset': 0x88a18,
|
||||||
|
'num_entries': 148,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xd0000: {
|
||||||
|
'name': 'DS ft Dreams Come True (JP)',
|
||||||
|
'offset': 0x6341c,
|
||||||
|
'num_entries': 79,
|
||||||
|
'is_ps1': True,
|
||||||
|
'is_split_bin': True,
|
||||||
|
},
|
||||||
|
0x114000: {
|
||||||
|
'name': 'DS ft True Kiss Destination (JP)',
|
||||||
|
'offset': 0x79ddc,
|
||||||
|
'num_entries': 60,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xc9800: {
|
||||||
|
'name': 'DS Fever (EU)',
|
||||||
|
'offset': 0x7e598,
|
||||||
|
'num_entries': 106,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0x9b800: {
|
||||||
|
'name': 'DS Fusion (EU)',
|
||||||
|
'offset': 0x79350,
|
||||||
|
'num_entries': 235,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
0xe7000: {
|
||||||
|
'name': 'DDR Oha Star (JP)',
|
||||||
|
'offset': 0x8519c,
|
||||||
|
'num_entries': 203,
|
||||||
|
'is_ps1': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Length of the ELF is used as the key
|
# Length of the ELF is used as the key
|
||||||
table_info = {
|
table_info_ps2 = {
|
||||||
0x176C2C: {
|
0x176C2C: {
|
||||||
# Tested working
|
# Tested working
|
||||||
'name': 'MAX JP',
|
'name': 'DDR MAX (JP)',
|
||||||
'offset': 0x175B18,
|
'offset': 0x175B18,
|
||||||
'num_entries': 374
|
'num_entries': 374
|
||||||
},
|
},
|
||||||
0x19D568: {
|
0x19D568: {
|
||||||
# Tested working
|
# Tested working
|
||||||
'name': 'MAX US',
|
'name': 'DDR MAX (US)',
|
||||||
'offset': 0x168320,
|
'offset': 0x168320,
|
||||||
'num_entries': 577
|
'num_entries': 577
|
||||||
},
|
},
|
||||||
0x1DAC88: {
|
0x1DAC88: {
|
||||||
'name': 'MAX 2 JP',
|
'name': 'DDR MAX 2 (JP)',
|
||||||
'offset': 0x17DDE8,
|
'offset': 0x17DDE8,
|
||||||
'num_entries': 675
|
'num_entries': 675
|
||||||
},
|
},
|
||||||
0x265854: {
|
0x265854: {
|
||||||
'name': 'MAX 2 US',
|
'name': 'DDR MAX 2 (US)',
|
||||||
'offset': 0x1A0810,
|
'offset': 0x1A0810,
|
||||||
'num_entries': 795
|
'num_entries': 795
|
||||||
},
|
},
|
||||||
0x12A608: {
|
0x12A608: {
|
||||||
'name': 'MAX 2 E3 Demo US',
|
'name': 'DDR MAX 2 E3 Demo (US)',
|
||||||
'offset': 0x1842F0,
|
'offset': 0x1842F0,
|
||||||
'num_entries': 223
|
'num_entries': 223
|
||||||
},
|
},
|
||||||
2672124: {
|
2672124: {
|
||||||
# Tested working
|
# Tested working
|
||||||
'name': 'Extreme JP',
|
'name': 'DDR Extreme (JP)',
|
||||||
'offset': 0x1B3130,
|
'offset': 0x1B3130,
|
||||||
'num_entries': 656
|
'num_entries': 656
|
||||||
},
|
},
|
||||||
3871008: {
|
3871008: {
|
||||||
'name': 'Extreme E3 Demo US',
|
'name': 'DDR Extreme E3 Demo (US)',
|
||||||
'offset': 0x17C880,
|
'offset': 0x17C880,
|
||||||
'num_entries': 680
|
'num_entries': 680
|
||||||
},
|
},
|
||||||
2725576: {
|
2725576: {
|
||||||
'name': 'Party Collection JP',
|
'name': 'DDR Party Collection (JP)',
|
||||||
'offset': 0x1A1548,
|
'offset': 0x1A1548,
|
||||||
'num_entries': 459
|
'num_entries': 459
|
||||||
}
|
}
|
||||||
|
@ -60,18 +185,23 @@ class FileData:
|
||||||
|
|
||||||
def _get_table_info(self, elf_path):
|
def _get_table_info(self, elf_path):
|
||||||
length = os.path.getsize(elf_path)
|
length = os.path.getsize(elf_path)
|
||||||
try:
|
result = self.table_info_ps2.get(length, self.table_info_ps1.get(length, None)) # PS2 -> PS1 -> None
|
||||||
return self.table_info[length]
|
|
||||||
except:
|
if result is None:
|
||||||
return None
|
# Try as if it's a PS-X EXE and grab the entry point
|
||||||
|
eip_data = open(elf_path, "rb").read(0x14)[-4:]
|
||||||
|
eip = struct.unpack("<I", eip_data)[0]
|
||||||
|
result = self.table_info_ps1.get(eip, None)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def _guess_filetype(self, stream, length):
|
def _guess_filetype(self, stream, length):
|
||||||
''' Guess the filetype of a binary stream
|
''' Guess the filetype of a binary stream
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
stream: file stream at start of data
|
stream: file stream at start of data
|
||||||
length: length of data
|
length: length of data
|
||||||
|
|
||||||
Return value:
|
Return value:
|
||||||
Dictionary with description and extension keys.
|
Dictionary with description and extension keys.
|
||||||
The file pointer will be returned to its original position.
|
The file pointer will be returned to its original position.
|
||||||
|
@ -103,24 +233,38 @@ class FileData:
|
||||||
self.table_info = self._get_table_info(elf_path)
|
self.table_info = self._get_table_info(elf_path)
|
||||||
if self.table_info == None:
|
if self.table_info == None:
|
||||||
raise LookupError('Unknown ELF; Cannot extract filetable.')
|
raise LookupError('Unknown ELF; Cannot extract filetable.')
|
||||||
|
|
||||||
def load_from_elf(self):
|
def load_from_elf(self):
|
||||||
''' Read table contents from ELF file. '''
|
''' Read table contents from ELF file. '''
|
||||||
filedata_file = open(self.filedata_path, 'rb')
|
filedata_file = open(self.filedata_path, 'rb')
|
||||||
|
start_sector = 0
|
||||||
with open(self.elf_path, 'rb') as elf_file:
|
with open(self.elf_path, 'rb') as elf_file:
|
||||||
elf_file.seek(self.table_info['offset'], os.SEEK_SET)
|
elf_file.seek(self.table_info['offset'], os.SEEK_SET)
|
||||||
for _ in range(self.table_info['num_entries']):
|
for id in range(self.table_info['num_entries']):
|
||||||
data = struct.unpack('<HBBBBBB', elf_file.read(8))
|
if self.table_info.get('is_ps1', False):
|
||||||
id = data[0]
|
# PSX file
|
||||||
offset = int.from_bytes([data[1],data[2],data[3]], byteorder='little', signed=False) * 0x800
|
data = struct.unpack('<II', elf_file.read(8))
|
||||||
length = int.from_bytes([data[4],data[5],data[6]], byteorder='little', signed=False) * 0x800
|
|
||||||
|
if id == 0:
|
||||||
|
start_sector = data[1]
|
||||||
|
|
||||||
|
length = data[0]
|
||||||
|
offset = (data[1] - start_sector) * 0x800
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = struct.unpack('<HBBBBBB', elf_file.read(8))
|
||||||
|
id = data[0]
|
||||||
|
offset = int.from_bytes([data[1],data[2],data[3]], byteorder='little', signed=False) * 0x800
|
||||||
|
length = int.from_bytes([data[4],data[5],data[6]], byteorder='little', signed=False) * 0x800
|
||||||
|
|
||||||
filedata_file.seek(offset)
|
filedata_file.seek(offset)
|
||||||
filetype = self._guess_filetype(filedata_file, length)
|
filetype = self._guess_filetype(filedata_file, length)
|
||||||
filename = '{}.{}'.format(id, filetype['extension'])
|
filename = '{}.{}'.format(id, filetype['extension'])
|
||||||
|
|
||||||
|
print("%08x %08x %08x" % (id, offset, length))
|
||||||
|
|
||||||
self.table.append({
|
self.table.append({
|
||||||
'id': data[0],
|
'id': id,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'length': length,
|
'length': length,
|
||||||
'description': filetype['description'],
|
'description': filetype['description'],
|
||||||
|
@ -131,6 +275,12 @@ class FileData:
|
||||||
table_sorted = list(sorted(self.table, key=lambda k: k['offset']))
|
table_sorted = list(sorted(self.table, key=lambda k: k['offset']))
|
||||||
for i in range(len(table_sorted) - 1):
|
for i in range(len(table_sorted) - 1):
|
||||||
expected_next_offset = table_sorted[i]['offset'] + table_sorted[i]['length']
|
expected_next_offset = table_sorted[i]['offset'] + table_sorted[i]['length']
|
||||||
|
|
||||||
|
if self.table_info.get('is_ps1', False):
|
||||||
|
# Padded to 0x800 boundaries
|
||||||
|
if (expected_next_offset % 0x800) != 0:
|
||||||
|
expected_next_offset += 0x800 - (expected_next_offset % 0x800)
|
||||||
|
|
||||||
if not table_sorted[i+1]['offset'] == expected_next_offset:
|
if not table_sorted[i+1]['offset'] == expected_next_offset:
|
||||||
print('Found hidden data at {}'.format(expected_next_offset))
|
print('Found hidden data at {}'.format(expected_next_offset))
|
||||||
hidden_length = table_sorted[i+1]['offset'] - expected_next_offset
|
hidden_length = table_sorted[i+1]['offset'] - expected_next_offset
|
||||||
|
@ -145,11 +295,18 @@ class FileData:
|
||||||
'filename': hidden_filename
|
'filename': hidden_filename
|
||||||
})
|
})
|
||||||
|
|
||||||
if not table_sorted[-1]['offset'] + table_sorted[-1]['length'] == os.path.getsize(self.filedata_path):
|
# Check very end of file
|
||||||
|
expected_next_offset = table_sorted[-1]['offset'] + table_sorted[-1]['length']
|
||||||
|
if self.table_info.get('is_ps1', False):
|
||||||
|
# Padded to 0x800 boundaries
|
||||||
|
if (expected_next_offset % 0x800) != 0:
|
||||||
|
expected_next_offset += 0x800 - (expected_next_offset % 0x800)
|
||||||
|
|
||||||
|
if not expected_next_offset == os.path.getsize(self.filedata_path):
|
||||||
# Found hidden data at the end of filedata.bin not listed in the ELF.
|
# Found hidden data at the end of filedata.bin not listed in the ELF.
|
||||||
print('Found hidden data at {}'.format(expected_next_offset))
|
hidden_length = os.path.getsize(self.filedata_path) - expected_next_offset
|
||||||
hidden_length = os.path.getsize(self.filedata_path) - (table_sorted[-1]['offset'] + table_sorted[-1]['length'])
|
|
||||||
hidden_offset = os.path.getsize(self.filedata_path) - hidden_length
|
hidden_offset = os.path.getsize(self.filedata_path) - hidden_length
|
||||||
|
print('Found hidden data at {}'.format(hidden_offset))
|
||||||
filedata_file.seek(hidden_offset)
|
filedata_file.seek(hidden_offset)
|
||||||
hidden_filetype = self._guess_filetype(filedata_file, hidden_length)
|
hidden_filetype = self._guess_filetype(filedata_file, hidden_length)
|
||||||
hidden_filename = 'hidden_{}.{}'.format(hidden_offset, hidden_filetype['extension'])
|
hidden_filename = 'hidden_{}.{}'.format(hidden_offset, hidden_filetype['extension'])
|
||||||
|
@ -179,7 +336,7 @@ class FileData:
|
||||||
''' Read table contents from CSV file. '''
|
''' Read table contents from CSV file. '''
|
||||||
with open(csv_path, 'r') as csv_file:
|
with open(csv_path, 'r') as csv_file:
|
||||||
self.table = list(csv.DictReader(csv_file))
|
self.table = list(csv.DictReader(csv_file))
|
||||||
|
|
||||||
def save_to_csv(self, csv_path):
|
def save_to_csv(self, csv_path):
|
||||||
''' Save table contents to CSV file. '''
|
''' Save table contents to CSV file. '''
|
||||||
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
||||||
|
@ -198,7 +355,7 @@ class FileData:
|
||||||
filedata.seek(row['offset'], os.SEEK_SET)
|
filedata.seek(row['offset'], os.SEEK_SET)
|
||||||
out.write(filedata.read(row['length']))
|
out.write(filedata.read(row['length']))
|
||||||
print('')
|
print('')
|
||||||
|
|
||||||
def create(self, input_directory):
|
def create(self, input_directory):
|
||||||
''' Create filedata.bin file, updating the internal table if file sizes have changed. '''
|
''' Create filedata.bin file, updating the internal table if file sizes have changed. '''
|
||||||
sorted_indices = [i[0] for i in sorted(enumerate(self.table), key=lambda x: int(x[1]['offset']))]
|
sorted_indices = [i[0] for i in sorted(enumerate(self.table), key=lambda x: int(x[1]['offset']))]
|
||||||
|
@ -211,7 +368,7 @@ class FileData:
|
||||||
with open(file_path, 'rb') as input:
|
with open(file_path, 'rb') as input:
|
||||||
filedata.write(input.read())
|
filedata.write(input.read())
|
||||||
filedata.write(bytearray(self.table[i]['length'] % 0x800))
|
filedata.write(bytearray(self.table[i]['length'] % 0x800))
|
||||||
|
|
||||||
# Update offsets to account for potentially updated lengths
|
# Update offsets to account for potentially updated lengths
|
||||||
offset = 0
|
offset = 0
|
||||||
for i in sorted_indices:
|
for i in sorted_indices:
|
||||||
|
|
Loading…
Reference in New Issue