386 lines
15 KiB
Python
386 lines
15 KiB
Python
#Written by JoshuaMK 2020
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import re
|
|
|
|
try:
|
|
import argparse
|
|
import chardet
|
|
except ImportError as IE:
|
|
print(IE)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
import colorama
|
|
from colorama import Fore, Style
|
|
colorama.init()
|
|
TRESET = Style.RESET_ALL
|
|
TGREEN = Fore.GREEN
|
|
TGREENLIT = Style.BRIGHT + Fore.GREEN
|
|
TYELLOW = Fore.YELLOW
|
|
TYELLOWLIT = Style.BRIGHT + Fore.YELLOW
|
|
TRED = Fore.RED
|
|
TREDLIT = Style.BRIGHT + Fore.RED
|
|
|
|
except ImportError:
|
|
TRESET = ''
|
|
TGREEN = ''
|
|
TGREENLIT = ''
|
|
TYELLOW = ''
|
|
TYELLOWLIT = ''
|
|
TRED = ''
|
|
TREDLIT = ''
|
|
|
|
|
|
|
|
def resource_path(relative_path):
|
|
""" Get absolute path to resource, works for dev and for PyInstaller """
|
|
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
|
|
return os.path.join(base_path, relative_path)
|
|
|
|
def get_size(file, offset=0):
|
|
""" Return a file's size in bytes """
|
|
file.seek(0, 2)
|
|
return(bytes.fromhex('{:08X}'.format(file.tell() + offset)))
|
|
|
|
def getFileAlignment(file, alignment):
|
|
""" Return file alignment, 0 = aligned, non zero = misaligned """
|
|
size = int.from_bytes(get_size(file), byteorder='big', signed=False)
|
|
|
|
if size % alignment != 0:
|
|
pad_number = alignment - (size % alignment)
|
|
else:
|
|
pad_number = 0
|
|
return pad_number
|
|
|
|
def alignFile(file, alignment):
|
|
""" Align a file to be the specified size """
|
|
file.write(bytes.fromhex("00" * getFileAlignment(file, alignment)))
|
|
|
|
|
|
def geckoParser(geckoText, parseAll):
|
|
geckoMagic = '00D0C0DE00D0C0DE'
|
|
geckoTerminate = 'F000000000000000'
|
|
with open(geckoText, 'rb') as gecko:
|
|
result = chardet.detect(gecko.read())
|
|
encodeType = result['encoding']
|
|
|
|
with open(geckoText, 'r', encoding=encodeType) as gecko:
|
|
data = gecko.readlines()
|
|
geckoCodes = ''
|
|
|
|
for line in data:
|
|
if parseAll.lower() == 'all':
|
|
geckoLine = re.findall(r'[A-F0-9]{8}\s[A-F0-9]{8}', line, re.IGNORECASE)
|
|
elif parseAll.lower() == 'active':
|
|
geckoLine = re.findall(r'\*\s[A-F0-9]{8}\s[A-F0-9]{8}', line, re.IGNORECASE)
|
|
else:
|
|
geckoLine = re.findall(r'\*\s[A-F0-9]{8}\s[A-F0-9]{8}', line, re.IGNORECASE)
|
|
|
|
geckoLine = ''.join(geckoLine)
|
|
geckoLine = re.sub(r'\s+', '', geckoLine)
|
|
geckoCodes = geckoCodes + geckoLine.replace('*', '')
|
|
|
|
geckoCodes = geckoMagic + geckoCodes + geckoTerminate
|
|
geckoSize = '{:08X}'.format(len(bytes.fromhex(geckoCodes))).lstrip('0')
|
|
|
|
return [bytes.fromhex(geckoCodes), geckoSize]
|
|
|
|
def build(gctFile, dolFile, size, isText):
|
|
with open(resource_path('geckoloader.bin'), 'rb') as code, open(r'{}'.format(dolFile), 'rb') as dol, open(r'{}'.format(gctFile), 'rb') as gecko, open(resource_path('codehandler.bin'), 'rb') as handler, open('tmp.bin', 'wb+') as tmp, open(os.path.join('BUILD', os.path.basename(dolFile)), 'wb+') as final:
|
|
|
|
if int(get_size(dol).hex(), 16) < int('0x100', 16):
|
|
os.remove('tmp.bin')
|
|
parser.error('DOL header is corrupted. Please provide a clean file')
|
|
|
|
dol.seek(0)
|
|
|
|
'''Initialize the new DOL file'''
|
|
|
|
final.write(dol.read())
|
|
|
|
'''Initialize the geckoloader'''
|
|
|
|
tmp.write(code.read())
|
|
code.seek(0)
|
|
tmp.seek(0)
|
|
|
|
'''Get BSS section for insert'''
|
|
|
|
final.seek(int('D8', 16))
|
|
BSS = int(final.read(4).hex(), 16)
|
|
BSS_length = int(final.read(4).hex(), 16)
|
|
dump_address = '{:08X}'.format(int(BSS + (BSS_length / 2)))[:-2] + '00'
|
|
_START = bytes.fromhex('{:08X}'.format(int(dump_address, 16)))
|
|
cLoader = bytes.fromhex(dump_address)
|
|
|
|
'''Get address split for later'''
|
|
|
|
upperAddr, lowerAddr = dump_address[:int(len(dump_address)/2)], dump_address[int(len(dump_address)/2):]
|
|
|
|
'''Get code initialize address'''
|
|
|
|
final.seek(int('E0', 16))
|
|
_init = [final.read(2), final.read(2)]
|
|
|
|
'''Patch the values for the addresses and such'''
|
|
|
|
heaped = False
|
|
sized = False
|
|
fsized = False
|
|
|
|
if isText == True:
|
|
geckoCheats = geckoParser(gctFile, args.txtcodes)
|
|
|
|
while heaped == False or sized == False or fsized == False:
|
|
try:
|
|
sample = tmp.read(4)
|
|
if sample == HEAP: #Found keyword "HEAP". Goes with the resize of the heap
|
|
if not heaped:
|
|
tmp.seek(-4, 1)
|
|
gpModInfoOffset = tmp.tell()
|
|
if int(lowerAddr, 16) + gpModInfoOffset > int('7FFF', 16): #Absolute addressing
|
|
gpModUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1))
|
|
else:
|
|
gpModUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16)))
|
|
if size == '0' or size == '':
|
|
if isText == False:
|
|
size = get_size(gecko).hex().upper()
|
|
else:
|
|
size = geckoCheats[1]
|
|
tmp.write(bytes.fromhex('{:08X}'.format(int(size, 16))))
|
|
heaped = True
|
|
|
|
elif sample == LOADERSIZE: #Found keyword "LSIZ". Goes with the size of the loader
|
|
if not sized:
|
|
tmp.seek(-4, 1)
|
|
tmp.write(get_size(code))
|
|
sized = True
|
|
|
|
elif sample == FULLSIZE: #Found keyword "FSIZ". Goes with the size of the loader + codes
|
|
if not fsized:
|
|
tmp.seek(-4, 1)
|
|
code.seek(0, 2)
|
|
gecko.seek(0, 2)
|
|
if isText == True:
|
|
tmp.write(get_size(code, int(geckoCheats[1], 16)))
|
|
else:
|
|
tmp.write(get_size(code, gecko.tell()))
|
|
fsized = True
|
|
except Exception as err:
|
|
print(err)
|
|
sys.exit(1)
|
|
|
|
gpDiscOffset = int.from_bytes(get_size(tmp, -4), byteorder="big", signed=True)
|
|
|
|
if int(lowerAddr, 16) + gpDiscOffset > int('7FFF', 16): #Absolute addressing
|
|
gpDiscUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1))
|
|
else:
|
|
gpDiscUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16)))
|
|
|
|
'''Patch all load/store offsets to data'''
|
|
|
|
tmp.seek(0)
|
|
sample = tmp.read(2)
|
|
while sample:
|
|
if sample == DH:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(gpDiscUpperAddr)
|
|
elif sample == DL:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + gpDiscOffset)))
|
|
elif sample == GH:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(gpModUpperAddr)
|
|
elif sample == GL:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + gpModInfoOffset)))
|
|
elif sample == IH:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(_init[0])
|
|
elif sample == IL:
|
|
tmp.seek(-2, 1)
|
|
tmp.write(_init[1])
|
|
sample = tmp.read(2)
|
|
|
|
final.seek(0, 2)
|
|
tmp.seek(0)
|
|
gecko.seek(0)
|
|
|
|
dol_handler_offset = get_size(final)
|
|
final.write(handler.read() + bytes.fromhex('00' * getFileAlignment(handler, 0x100)))
|
|
time.sleep(0.01)
|
|
dol_sme_offset = get_size(final)
|
|
|
|
final.write(tmp.read())
|
|
time.sleep(0.01)
|
|
|
|
if isText == False:
|
|
final.write(gecko.read())
|
|
else:
|
|
final.write(geckoCheats[0])
|
|
final.write(bytes.fromhex('00' * getFileAlignment(final, 0x100)))
|
|
final.seek(0, 0)
|
|
|
|
status = False
|
|
i = 0
|
|
|
|
while i < 6:
|
|
textOffset = int(final.read(4).hex(), 16)
|
|
if textOffset == 0:
|
|
status = True
|
|
offset = i * 4
|
|
|
|
'''Write offset to each section in DOL file header'''
|
|
final.seek(-4, 1)
|
|
final.write(dol_handler_offset)
|
|
final.write(dol_sme_offset)
|
|
|
|
final.seek(int('48', 16) + offset)
|
|
|
|
'''Write in game memory addresses for each section in DOL file header'''
|
|
final.write(bytes.fromhex('80001800'))
|
|
final.write(cLoader)
|
|
final.seek(int('E0', 16))
|
|
|
|
'''Write game entry in DOL file header'''
|
|
final.write(_START)
|
|
|
|
'''Get size of GeckoLoader + gecko codes, and the codehandler'''
|
|
handler_size = int.from_bytes(dol_sme_offset, byteorder="big", signed=False) - int.from_bytes(dol_handler_offset, byteorder="big", signed=False)
|
|
sme_code_size = int.from_bytes(get_size(final), byteorder="big", signed=False) - int.from_bytes(dol_sme_offset, byteorder="big", signed=False)
|
|
|
|
tmp.seek(0, 2)
|
|
gecko.seek(0, 2)
|
|
|
|
'''Write size of each section into DOL file header'''
|
|
final.seek(int('90', 16) + offset)
|
|
final.write(bytes.fromhex('{:08X}'.format(handler_size)))
|
|
final.write(bytes.fromhex('{:08X}'.format(sme_code_size)))
|
|
break
|
|
else:
|
|
i += 1
|
|
|
|
if status == False:
|
|
os.remove('tmp.bin')
|
|
parser.error(TREDLIT + 'Not enough text sections to patch the DOL file! Potentially due to previous mods?\n' + TRESET)
|
|
|
|
if isText == False:
|
|
if int(size, 16) < int(get_size(gecko).hex(), 16):
|
|
print(TYELLOW + '\n :: WARNING: Allocated codespace was smaller than the given codelist. The game will crash if run' + TRESET)
|
|
else:
|
|
if int(size, 16) < int(geckoCheats[1], 16):
|
|
print(TYELLOW + '\n :: WARNING: Allocated codespace was smaller than the given codelist. The game will crash if run' + TRESET)
|
|
|
|
if args.quiet:
|
|
return
|
|
|
|
if int(size, 16) > int('70000', 16):
|
|
print(TYELLOW + '\n :: WARNING: Allocations beyond 70000 will crash certain games. You allocated 0x{}'.format(size) + TRESET)
|
|
|
|
elif int(size, 16) > int('40000', 16):
|
|
print(TYELLOWLIT + '\n :: HINT: Recommended allocation limit is 0x40000. You allocated 0x{}'.format(size) + TRESET)
|
|
|
|
if isText == False:
|
|
codelistSize = get_size(gecko).hex().upper().lstrip('0')
|
|
else:
|
|
codelistSize = geckoCheats[1]
|
|
|
|
if args.verbose >= 2:
|
|
print('')
|
|
info = [TGREENLIT + ' :: GeckoLoader set at address 0x{}, start of game modified to address 0x{}'.format(dump_address.upper(), _START.hex().upper()),
|
|
' :: Game function "_init_registers" located at address 0x{}{}'.format(_init[0].hex(), _init[1].hex().upper()),
|
|
' :: Code allocation is 0x{}; codelist size is 0x{}'.format(size.upper().lstrip('0'), codelistSize),
|
|
' :: Of the 7 text sections in this DOL file, {} were already used'.format(i) + TRESET]
|
|
|
|
for bit in info:
|
|
print(bit)
|
|
|
|
elif args.verbose >= 1:
|
|
print('')
|
|
info = [TGREENLIT + ' :: GeckoLoader set at address 0x{}'.format(dump_address.upper()),
|
|
' :: Code allocation is 0x{} in hex; codelist size is 0x{}'.format(size.upper().lstrip('0'), codelistSize) + TRESET]
|
|
|
|
for bit in info:
|
|
print(bit)
|
|
return
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
isText = False
|
|
|
|
parser = argparse.ArgumentParser(description='Process files and allocations for GeckoLoader')
|
|
parser.add_argument('file', help='First file')
|
|
parser.add_argument('file2', help='Second file')
|
|
parser.add_argument('--alloc', help='Define the size of the code allocation: --alloc hex')
|
|
parser.add_argument('-tc', '--txtcodes', help='What codes get parsed when a txt file is used.\n"ALL" makes all codes get parsed,\n"ACTIVE" makes only activated codes get parsed.', default='active')
|
|
parser.add_argument('-q', '--quiet', help='Print nothing to the console', action='store_true')
|
|
parser.add_argument('-v', '--verbose', help='Print extra info to the console', default=0, action='count')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.alloc:
|
|
size = args.alloc.lstrip('0x')
|
|
try:
|
|
int(size, 16)
|
|
except:
|
|
parser.error('The allocation was invalid\n')
|
|
else:
|
|
size = '0'
|
|
|
|
if os.path.splitext(args.file)[1].lower() == '.dol':
|
|
dolFile = args.file
|
|
elif os.path.splitext(args.file2)[1].lower() == '.dol':
|
|
dolFile = args.file2
|
|
else:
|
|
parser.error('No dol file was passed\n')
|
|
|
|
if os.path.splitext(args.file)[1].lower() == '.gct':
|
|
gctFile = args.file
|
|
isText = False
|
|
elif os.path.splitext(args.file)[1].lower() == '.txt':
|
|
gctFile = args.file
|
|
isText = True
|
|
elif os.path.splitext(args.file2)[1].lower() == '.gct':
|
|
gctFile = args.file2
|
|
isText = False
|
|
elif os.path.splitext(args.file2)[1].lower() == '.txt':
|
|
gctFile = args.file2
|
|
isText = True
|
|
else:
|
|
parser.error('Neither a gct or gecko text file was passed\n')
|
|
|
|
HEAP = bytes.fromhex('48454150')
|
|
LOADERSIZE = bytes.fromhex('4C53495A')
|
|
FULLSIZE = bytes.fromhex('4653495A')
|
|
DH = bytes.fromhex('4448')
|
|
DL = bytes.fromhex('444C')
|
|
GH = bytes.fromhex('4748')
|
|
GL = bytes.fromhex('474C')
|
|
IH = bytes.fromhex('4948')
|
|
IL = bytes.fromhex('494C')
|
|
|
|
try:
|
|
if not os.path.isdir('BUILD'):
|
|
os.mkdir('BUILD')
|
|
|
|
if not os.path.isfile(dolFile):
|
|
parser.error(dolFile + ' Does not exist')
|
|
|
|
if not os.path.isfile(gctFile):
|
|
parser.error(gctFile + ' Does not exist')
|
|
|
|
time1 = time.time()
|
|
|
|
build(gctFile, dolFile, size, isText)
|
|
|
|
os.remove('tmp.bin')
|
|
if not args.quiet:
|
|
print('\n :: Compiled in {:0.4f} seconds!\n'.format(time.time() - time1))
|
|
|
|
except FileNotFoundError as err:
|
|
parser.error(err)
|
|
sys.exit(1)
|