#!/usr/bin/env python
#***************************************************************************
#* *
#* PopSX - convert a PSX CD to a POPStation EBOOT.PBP *
#* *
#* by Chilly Willy, based on code by Dark_AleX, Tinnus, and Rck *
#* *
#***************************************************************************
#***************************************************************************
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU General Public License as published by *
#* the Free Software Foundation; either version 2 of the License, or *
#* (at your option) any later version. *
#* *
#***************************************************************************
import sys
import os
import string
import array
import struct
import zlib
# binary helper functions
def get_byte(buffer, offset):
"get the byte at offset in the buffer"
return struct.unpack("B", buffer[offset])[0]
def get_word_le(buffer, offset):
"get the word at offset in the buffer"
return struct.unpack("<H", buffer[offset:offset+2])[0]
def get_long_le(buffer, offset):
"get the longword at offset in the buffer"
return struct.unpack("<I", buffer[offset:offset+4])[0]
def put_byte(val):
"return a string with the byte passed in"
return struct.pack("B", val)
def put_word_le(val):
"return a string with the word passed in"
return struct.pack("<H", val)
def put_long_le(val):
"return a string with the longword passed in"
return struct.pack("<I", val)
# format helper functions
def maybebcd(n):
"return the BCD equivalent of the hex number passed in if < 0xA0"
if n < 0xA0:
return ((n / 10) << 4) | (n % 10)
else:
return n
def hex2bcd(n):
"return the BCD equivalent of the hex number passed in"
return ((n / 10) << 4) | (n % 10)
# convert toc.dat from readcd into PSX format
def get_toc():
"read TOC via readcd, return string with PSX TOC"
# try to run readcd
print 'Attempting to read TOC from CDROM -'
print
try:
os.spawnlp(os.P_WAIT, 'readcd', 'readcd', 'dev=/dev/cdrom', '-fulltoc')
except:
print 'Could not run readcd'
print
# try to read toc.dat
try:
toc_file = open('toc.dat', 'rb')
except:
print 'Could not open toc.dat'
return ''
toc_data = toc_file.read()
toc_file.close()
toc_size = len(toc_data)
# find the start of the first session
toc_start = 0
while toc_start < len(toc_data):
if get_byte(toc_data, toc_start) == 0xA0:
break
else:
toc_start += 1
if toc_start == len(toc_data):
print 'toc.dat has invalid data'
return ''
toc_start -= 3
print 'Found TOC start at offset', toc_start
toc_entries = (len(toc_data) - toc_start) / 11
print 'This disc has', toc_entries, 'entries'
toc_list = [] # PSX TOC list starts empty
# convert from normal TOC format to PSX TOC format
for entry in range(toc_entries):
# ctrl:adr field
toc_list.append((get_byte(toc_data, toc_start+entry*11) & 15) | ((get_byte(toc_data, toc_start+entry*11+1) & 15) << 4))
#reserved
toc_list.append(get_byte(toc_data, toc_start+entry*11+2))
# point
toc_list.append(maybebcd(get_byte(toc_data, toc_start+entry*11+3)))
# amin, asec, afrm
if entry < 3:
toc_list.append(hex2bcd(get_byte(toc_data, toc_start+entry*11+4)))
toc_list.append(hex2bcd(get_byte(toc_data, toc_start+entry*11+5)))
toc_list.append(hex2bcd(get_byte(toc_data, toc_start+entry*11+6)))
else:
amin = get_byte(toc_data, toc_start+entry*11+8)
asec = get_byte(toc_data, toc_start+entry*11+9)
afrm = get_byte(toc_data, toc_start+entry*11+10)
if entry == 3:
afrm += 1
else:
asec -= 2
if asec < 0:
amin -= 1
asec += 60
toc_list.append(hex2bcd(amin))
toc_list.append(hex2bcd(asec))
toc_list.append(hex2bcd(afrm))
# reserved
toc_list.append(get_byte(toc_data, toc_start+entry*11+7))
# pmin, psec, pfrm
pmin = get_byte(toc_data, toc_start+entry*11+8)
psec = get_byte(toc_data, toc_start+entry*11+9)
pfrm = get_byte(toc_data, toc_start+entry*11+10)
if entry == 0:
toc_list.append(hex2bcd(pmin))
toc_list.append(psec)
toc_list.append(hex2bcd(pfrm))
elif entry < 4:
toc_list.append(hex2bcd(pmin))
toc_list.append(hex2bcd(psec))
toc_list.append(hex2bcd(pfrm))
else:
psec -= 2
if psec < 0:
pmin -= 1
psec += 60
toc_list.append(hex2bcd(pmin))
toc_list.append(hex2bcd(psec))
toc_list.append(hex2bcd(pfrm))
return struct.pack(repr(toc_entries * 10) + 'B', *toc_list)
# get file size
def get_file_size(infile):
infile.seek(0, 2) # seek to the end
size = infile.tell()
infile.seek(0) # seek to the beginning
return size
# check game code
def check_code(code):
"check if the code passed in is valid, if not pass a replacement"
codes = set(['SCUS','SLUS','SLES','SCES','SCED','SLPS','SLPM','SCPS','SLED','SIPS','ESPM','PBPX'])
if code[0 : 4] in codes:
return code[0 : 9]
return 'PBPX12345'
# get extra data files from the current directory or from BASE.PBP
def get_sfo(base_data):
"get SFO from BASE.PBP"
sfo = base_data[get_long_le(base_data, 0x08) : get_long_le(base_data, 0x0C)]
# set TITLE and DISC_ID in SFO
title = sys.argv[1]
if len(title) > 127:
title = title[0 : 127] # trim title to 127 characters
disc_id = check_code(sys.argv[2])
fields_table_offs = get_long_le(sfo, 0x08)
values_table_offs = get_long_le(sfo, 0x0C)
num_items = get_long_le(sfo, 0x10)
for i in range(num_items):
item_type = get_byte(sfo, i * 0x10 + 0x17)
if item_type != 2:
continue # not a string, skip
field_offs = get_word_le(sfo, i * 0x10 + 0x14)
item = ''
while get_byte(sfo, fields_table_offs + field_offs) != 0:
item += sfo[ fields_table_offs + field_offs ]
field_offs += 1
if item == 'TITLE':
length = get_long_le(sfo, i * 0x10 + 0x18)
size = get_long_le(sfo, i * 0x10 + 0x1C)
val_offs = get_word_le(sfo, i * 0x10 + 0x20)
val_start = values_table_offs + val_offs
val_end = val_start + len(title) + 1
sfo = sfo[: i * 0x10 + 0x18] + \
put_long_le(len(title) + 1) + \
sfo[i * 0x10 + 0x1C : val_start] + \
title + put_byte(0) + \
sfo[val_end :]
if item == 'DISC_ID':
length = get_long_le(sfo, i * 0x10 + 0x18)
size = get_long_le(sfo, i * 0x10 + 0x1C)
val_offs = get_word_le(sfo, i * 0x10 + 0x20)
val_start = values_table_offs + val_offs
val_end = val_start + len(disc_id) + 1
sfo = sfo[: i * 0x10 + 0x18] + \
put_long_le(len(disc_id) + 1) + \
sfo[i * 0x10 + 0x1C : val_start] + \
disc_id + put_byte(0) + \
sfo[val_end :]
return sfo
def get_icon0(base_data):
"get ICON0.PNG from file, or BASE.PBP if no file"
try:
icon0_file = open('ICON0.PNG', 'rb')
icon0_data = icon0_file.read()
icon0_file.close()
return icon0_data
except:
return base_data[get_long_le(base_data, 0x0C) : get_long_le(base_data, 0x10)]
def get_icon1(base_data):
"get ICON1.PMF from file, or BASE.PBP if no file"
try:
icon1_file = open('ICON1.PMF', 'rb')
icon1_data = icon1_file.read()
icon1_file.close()
return icon1_data
except:
return base_data[get_long_le(base_data, 0x10) : get_long_le(base_data, 0x14)]
def get_pic0(base_data):
"get PIC0.PNG from file, or BASE.PBP if no file"
try:
pic0_file = open('PIC0.PNG', 'rb')
pic0_data = pic0_file.read()
pic0_file.close()
return pic0_data
except:
return base_data[get_long_le(base_data, 0x14) : get_long_le(base_data, 0x18)]
def get_pic1(base_data):
"get PIC1.PNG from file, or BASE.PBP if no file"
try:
pic1_file = open('PIC1.PNG', 'rb')
pic1_data = pic1_file.read()
pic1_file.close()
return pic1_data
except:
return base_data[get_long_le(base_data, 0x18) : get_long_le(base_data, 0x1C)]
def get_snd0(base_data):
"get SND0.AT3 from file, or BASE.PBP if no file"
try:
snd0_file = open('SND0.AT3', 'rb')
snd0_data = snd0_file.read()
snd0_file.close()
return snd0_data
except:
return base_data[get_long_le(base_data, 0x1C) : get_long_le(base_data, 0x20)]
def get_boot():
"get BOOT.PNG from file"
try:
boot_file = open('BOOT.PNG', 'rb')
boot_data = boot_file.read()
boot_file.close()
return boot_data
except:
return ''
# M:S:F helper function
def get_msf(loc):
"return tuple of minutes : seconds : frames given location on CD"
frames = int(loc / 2352) # 2353 bytes per frame (sector) in raw/XA mode
seconds = int(frames / 75) # 75 frames per second
minutes = int(seconds / 60) # 60 seconds in a minute
seconds -= (minutes * 60)
frames -= ((minutes * 60 + seconds) * 75)
return minutes, seconds, frames
#**** main entry point ****
print 'popsx v1.0'
print
# try to run cdrdao
print 'Attempting to read CDROM data -'
print
# 0 = skip dump, 1 = dump CD (debug thingy)
if 1:
try:
os.spawnlp(os.P_WAIT, 'cdrdao', 'cdrdao', 'read-cd', '--read-raw', '--datafile', 'temp.bin', \
'--device', '/dev/cdrom', '--driver', 'generic-mmc:0x00020000', 'temp.toc')
except:
print 'Could not run cdrdao'
else:
print 'Skipped CD dumping'
print
in_file = open('temp.bin', 'rb')
isosize = isorealsize = get_file_size(in_file)
# 1 block = 16 sectors (16 * 2352 = 0x9300), round ISO size to block boundary
if (isosize % 0x9300) != 0:
isosize += (0x9300 - (isosize % 0x9300))
#print isorealsize, isosize
print
# try to get the TOC
psx_toc = get_toc()
#psx_toc = '' # test fake TOC
if len(psx_toc) == 0:
# whoops! no TOC... better make a fake one
print 'Making fake TOC...'
pmin, psec, pfrm = get_msf(isorealsize + 2 * 75 * 2352)
pmin = hex2bcd(pmin)
psec = hex2bcd(psec)
pfrm = hex2bcd(pfrm)
psx_toc = struct.pack("BBBBBBBBBB", 0x41,0,0xA0,0,0,0,0,1,0x20,0) # 1st track = 1, XA
psx_toc += struct.pack("BBBBBBBBBB", 0x41,0,0xA1,0,0,0,0,1,0,0) # last track = 1
psx_toc += struct.pack("BBBBBBBBBB", 0x01,0,0xA2,0,0,0,0,pmin,psec,pfrm) # lead-out
psx_toc += struct.pack("BBBBBBBBBB", 0x41,0,1,0,2,1,0,0,2,0) # 1st track start
else:
print 'Actual TOC being used'
for i in range(len(psx_toc)):
if i % 10 == 0:
print
if get_byte(psx_toc, i) < 16:
print '0%X ' % get_byte(psx_toc, i),
else:
print '%X ' % get_byte(psx_toc, i),
print
print
# try to read BASE.PBP
try:
base_file = open('BASE.PBP', 'rb')
except:
print 'Could not open BASE.PBP'
sys.exit()
base_data = base_file.read()
base_file.close()
base_size = len(base_data)
# get SFO data
sfo = get_sfo(base_data)
# get ICON0.PNG data
icon0 = get_icon0(base_data)
# get ICON1.PMF data
icon1 = get_icon1(base_data)
# get PIC0.PNG data
pic0 = get_pic0(base_data)
# get PIC1.PNG data
pic1 = get_pic1(base_data)
# get SND0.AT3 data
snd0 = get_snd0(base_data)
# construct the header
header = put_long_le(0x50425000)
header += put_long_le(0x00010000)
curroffs = 0x28
header += put_long_le(curroffs) # offset to sfo
curroffs += len(sfo)
header += put_long_le(curroffs) # offset to ICON0.PNG
curroffs += len(icon0)
header += put_long_le(curroffs) # offset to ICON1.PMF
curroffs += len(icon1)
header += put_long_le(curroffs) # offset to PIC0.PNG
curroffs += len(pic0)
header += put_long_le(curroffs) # offset to PIC1.PNG
curroffs += len(pic1)
header += put_long_le(curroffs) # offset to SND0.AT3
curroffs += len(snd0)
header += put_long_le(curroffs) # offset to PSP header
psp_hdr_offs = get_long_le(base_data, 0x20)
prx_size = get_long_le(base_data, psp_hdr_offs + 0x2C)
xoffs = curroffs + prx_size
if (xoffs % 0x00010000) != 0:
xoffs += (0x00010000 - (xoffs % 0x00010000)) # round xoffs to next 0x00010000 boundary
header += put_long_le(xoffs) # offset to ISO header
# open output file and write header
out_file = open('EBOOT.PBP', 'wb')
print 'Writing header...'
out_file.write(header)
# now write the SFO
print 'Writing SFO...'
out_file.write(sfo)
# now write ICON0.PNG
print 'Writing ICON0.PNG...'
out_file.write(icon0)
# now write ICON1.PMF
print 'Writing ICON1.PMF...'
out_file.write(icon1)
# now write PIC0.PNG
print 'Writing PIC0.PNG...'
out_file.write(pic0)
# now write PIC1.PNG
print 'Writing PIC1.PNG...'
out_file.write(pic1)
# now write SND0.AT3
print 'Writing SND0.AT3...'
out_file.write(snd0)
# now write DATA.PSP
data_psp = base_data[psp_hdr_offs : psp_hdr_offs + prx_size]
print 'Writing DATA.PSP...'
out_file.write(data_psp)
# now pad to next 0x00010000 boundary
#print 'Padding to next 0x00010000 boundary...'
for i in range(xoffs - curroffs - prx_size):
out_file.write(put_byte(0))
iso_offs = out_file.tell() # remember where this is
# now write the ISO header
print 'Writing ISO header...'
out_file.write('PSISOIMG0000')
p1offs = out_file.tell() # pointer to special data at end of ISO
out_file.write(put_long_le(isosize + 0x00100000))
# pad to 0x100 longs
for i in range(0xFC):
out_file.write(put_long_le(0))
# now write the game code
disc_id = check_code(sys.argv[2])
game_code = '_' + disc_id[: 4] + '_' + disc_id[4 :]
out_file.write(game_code)
# pad out to TOC
for i in range(0x3F5):
out_file.write(put_byte(0))
# now write the TOC
print 'Writing TOC...'
out_file.write(psx_toc)
# misc until reach title
cnt = iso_offs + 0x0BFE - out_file.tell()
# pad to next data
for i in range(cnt):
out_file.write(put_byte(0))
out_file.write(put_byte(0x10))
cnt = iso_offs + 0x1220 - out_file.tell()
# pad to next data
for i in range(cnt):
out_file.write(put_byte(0))
p2offs = out_file.tell()
out_file.write(put_long_le(isosize + 0x00100000 + 0x2D31))
out_file.write(put_long_le(0))
out_file.write(put_long_le(0x7FF))
# now write the title
title = sys.argv[1]
if len(title) > 127:
title = title[0 : 127] # trim title to 127 characters
out_file.write(title)
out_file.write(put_byte(0))
cnt = iso_offs + 0x12AC - out_file.tell()
# pad to next data
for i in range(cnt):
out_file.write(put_byte(0))
out_file.write(put_long_le(3))
cnt = (iso_offs + 0x4000 - out_file.tell()) / 4
# pad to next data
for i in range(cnt):
out_file.write(put_long_le(0))
# now write dummy index table
index_offset = out_file.tell()
print 'Writing dummy indexes...'
for i in range(isosize / 0x9300):
# write an IsoIndex for each block in the ISO
out_file.write(put_long_le(0)) # offset
out_file.write(put_long_le(0)) # length
out_file.write(put_long_le(0)) # dummy[6]
out_file.write(put_long_le(0))
out_file.write(put_long_le(0))
out_file.write(put_long_le(0))
out_file.write(put_long_le(0))
out_file.write(put_long_le(0))
# pad to next 1M boundary from ISO header
cnt = iso_offs + 0x00100000 - out_file.tell()
for i in range(cnt):
out_file.write(put_byte(0))
# now write ISO data, building index table as go
comp_lvl = int(sys.argv[3])
if comp_lvl > 9:
comp_lvl = 9
print 'Compression level =', comp_lvl
offset = 0
indicies = []
print 'Writing ISO...'
for i in range(isosize / 0x9300):
# do a block of 16 sectors at a time
iso_block = in_file.read(0x9300)
# check if last block needs padding
if len(iso_block) < 0x9300:
# last block, pad to 0x9300
cnt = 0x9300 - len(iso_block)
for j in range(cnt):
iso_block += put_byte(0)
# check if we are compressing
if comp_lvl != 0:
# compress the block
try:
comp_block = zlib.compress(iso_block, comp_lvl)
except:
# if an error occurs, just use the uncompressed data
comp_block = iso_block
if len(comp_block) < len(iso_block):
iso_block = comp_block # only use compressed data if smaller
indicies.append(offset)
indicies.append(len(iso_block))
indicies.append(0)
indicies.append(0)
indicies.append(0)
indicies.append(0)
indicies.append(0)
indicies.append(0)
#print 'Block', i, 'is', len(iso_block), 'long'
out_file.write(iso_block)
offset += len(iso_block)
# pad to next 16 byte boundary
offset = out_file.tell()
if offset % 0x10 != 0:
cnt = 0x10 - (offset % 0x10)
for i in range(cnt):
out_file.write(put_byte(0))
end_offset = out_file.tell()
# now write the index table
print 'Writing actual indexes...'
out_file.seek(index_offset, 0)
out_file.write(struct.pack('<' + repr(isosize / 0x9300 * 8) + 'I', *indicies))
out_file.seek(p1offs, 0)
out_file.write(put_long_le(end_offset - iso_offs))
end_offset += 0x2D31
out_file.seek(p2offs, 0)
out_file.write(put_long_le(end_offset - iso_offs))
# now write the special data
offset = get_long_le(base_data, 0x24) + 12
offset = get_long_le(base_data, offset) + 0x50000
data_buf = base_data[offset : offset + 8]
if data_buf != 'STARTDAT':
print 'Cannot find STARTDAT in BASE.PBP'
print 'Not a valid PSX eboot.pbp'
sys.exit()
out_file.seek(0, 2) # go to end of file
# get BOOT.PNG
boot_pic = get_boot()
if len(boot_pic) == 0:
# no BOOT.PNG found
print 'Writing special data...'
out_file.write(base_data[offset :])
else:
# offset + 0x10 has size of header data = offset to boot png
# offset + 0x14 has size of boot png = offset to rest of data
seg1_offs = offset # has header data
seg2_offs = seg1_offs + get_long_le(base_data, offset + 0x10) # has boot.png data
seg3_offs = seg2_offs + get_long_le(base_data, offset + 0x14) # has all the rest
# write segment 1 (header data)
out_file.write(base_data[seg1_offs : seg1_offs + 0x14])
out_file.write(put_long_le(len(boot_pic)))
out_file.write(base_data[seg1_offs + 0x18 : seg2_offs])
# write segment 2 (boot.png)
print 'Writing BOOT.PNG...'
out_file.write(boot_pic)
# write segment 3 (everything else)
print 'Writing special data...'
out_file.write(base_data[seg3_offs :])
in_file.close()
out_file.close()
print
print 'All done! EBOOT.PBP has your PSX game ready for POPStation.'
print