1
0
Fork 0
This repository has been archived on 2024-02-06. You can view files and clone it, but cannot push or open issues or pull requests.
GeckoLoader/GeckoLoader.py

523 lines
21 KiB
Python
Raw Normal View History

#Written by JoshuaMK 2020
import sys
import os
import time
import re
import shutil
import dolreader
from io import BytesIO, RawIOBase
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:
return alignment - (size % alignment)
else:
return 0
def alignFile(file, alignment):
""" Align a file to be the specified size """
file.write(bytes.fromhex("00" * getFileAlignment(file, alignment)))
class GCT(object):
def __init__(self, f):
self.codelist = BytesIO(f.read())
self.rawlinecount = int.from_bytes(get_size(f), byteorder='big', signed=True) >> 3
self.linecount = self.rawlinecount - 2
self.size = int.from_bytes(get_size(f), byteorder='big', signed=True)
f.seek(0)
class CodeHandler(object):
def __init__(self, f, gctFile, isText):
self.codehandler = BytesIO(f.read())
'''Get codelist pointer'''
f.seek(0xFA, 0)
codelistUpper = f.read(2).hex()
f.seek(0xFE, 0)
codelistLower = f.read(2).hex()
self.codelistpointer = int(codelistUpper[2:] + codelistLower[2:], 16)
self.handlerlength = int.from_bytes(get_size(f), byteorder='big', signed=True)
self.initaddress = 0x80001800
self.startaddress = 0x800018A8
if self.handlerlength < 0x900:
self.type = "Mini"
else:
self.type = "Full"
if isText == True:
self.geckocodes = self.geckoParser(gctFile, args.txtcodes)
else:
with open(r'{}'.format(gctFile), 'rb') as gct:
self.geckocodes = GCT(gct)
f.seek(0)
def geckoParser(self, geckoText, parseAll):
geckoMagic = '00D0C0DE00D0C0DE'
geckoTerminate = 'F000000000000000'
with open(r'{}'.format(geckoText), 'rb') as gecko:
result = chardet.detect(gecko.read())
encodeType = result['encoding']
with open(r'{}'.format(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('*', '')
with open(os.path.join('tmp', 'gct.bin'), 'wb+') as code:
code.write(bytes.fromhex(geckoMagic + geckoCodes + geckoTerminate))
code.seek(0)
gct = GCT(code)
return gct
def build(gctFile, dolFile, codehandlerFile, size):
global isText, _allocation, _codehook
with open(resource_path(os.path.join('bin', 'geckoloader.bin')), 'rb') as code, open(r'{}'.format(dolFile), 'rb') as dol, open(resource_path(os.path.join('bin', r'{}'.format(codehandlerFile))), 'rb') as handler, open(os.path.join('tmp', 'tmp.bin'), 'wb+') as tmp, open(os.path.join('BUILD', os.path.basename(dolFile)), 'wb+') as final:
if int(get_size(dol).hex(), 16) < 0x100:
shutil.rmtree('tmp')
parser.error('DOL header is corrupted. Please provide a clean file')
dol.seek(0)
'''Initialize the new DOL file'''
final.write(dol.read())
final.seek(0)
dolfile = dolreader.DolFile(final)
'''Initialize our codehandler + codes'''
codehandler = CodeHandler(handler, gctFile, isText)
'''Get entrypoint (or BSS midpoint) for insert'''
if args.init:
dump_address = args.init.lstrip("0x").upper()
else:
dump_address = '{:08X}'.format(dolfile._bssoffset + (dolfile._bsssize >> 1))[:-2] + '00'
'''Is insertion legacy?'''
if args.movecodes == 'LEGACY':
_allocation = '{:X}'.format(0x80003000 - (codehandler.initaddress + codehandler.handlerlength))
patchLegacyHandler(codehandler, tmp, dolfile)
legacy = True
elif args.movecodes == 'ARENA':
patchGeckoLoader(code, codehandler, tmp, dolfile, dump_address)
legacy = False
else: #Auto decide area
if codehandler.initaddress + codehandler.handlerlength + codehandler.geckocodes.size > 0x80002FFF:
patchGeckoLoader(code, codehandler, tmp, dolfile, dump_address)
legacy = False
else:
_allocation = '{:X}'.format(0x80003000 - (codehandler.initaddress + codehandler.handlerlength))
patchLegacyHandler(codehandler, tmp, dolfile)
legacy = True
dolfile.save(final)
if int(_allocation, 16) < codehandler.geckocodes.size:
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(_allocation, 16) > int('70000', 16):
print(TYELLOW + '\n :: WARNING: Allocations beyond 0x70000 will crash certain games. You allocated 0x{}'.format(_allocation.upper().lstrip('0')) + TRESET)
elif int(_allocation, 16) > int('40000', 16):
print(TYELLOWLIT + '\n :: HINT: Recommended allocation limit is 0x40000. You allocated 0x{}'.format(_allocation.upper().lstrip('0')) + TRESET)
if args.verbose >= 2:
print('')
if legacy == False:
info = [TGREENLIT + ' :: GeckoLoader set at address 0x{}, start of game modified to address 0x{}'.format(dump_address.upper().lstrip('0'), dump_address.upper().lstrip('0')),
' :: Game function "_init_registers" located at address 0x{:X}'.format(dolfile._init),
' :: Codehandler hooked at 0x{}'.format(_codehook.upper().lstrip('0')),
' :: Code allocation is 0x{}; codelist size is 0x{:X}'.format(_allocation.upper().lstrip('0'), codehandler.geckocodes.size),
' :: Codehandler is of type "{}"'.format(codehandler.type),
' :: Of the 7 text sections in this DOL file, {} were already used'.format(len(dolfile._text)) + TRESET]
else:
info = [TGREENLIT + ' :: Game function "_init_registers" located at address 0x{:X}'.format(dolfile._init),
' :: Codehandler hooked at 0x{}'.format(_codehook.upper().lstrip('0')),
' :: Code allocation is 0x{}; codelist size is 0x{:X}'.format(_allocation.upper().lstrip('0'), codehandler.geckocodes.size),
' :: Codehandler is of type "{}"'.format(codehandler.type),
' :: Of the 7 text sections in this DOL file, {} were already used'.format(len(dolfile._text)) + TRESET]
for bit in info:
print(bit)
elif args.verbose >= 1:
print('')
if legacy == False:
info = [TGREENLIT + ' :: GeckoLoader set at address 0x{}'.format(dump_address.upper()),
' :: Codehandler is of type "{}"'.format(args.handler),
' :: Code allocation is 0x{} in hex; codelist size is 0x{:X}'.format(_allocation.upper().lstrip('0'), codehandler.handlerlength) + TRESET]
else:
info = [TGREENLIT + ' :: Codehandler is of type "{}"'.format(args.handler),
' :: Code allocation is 0x{} in hex; codelist size is 0x{:X}'.format(_allocation.upper().lstrip('0'), codehandler.handlerlength) + TRESET]
for bit in info:
print(bit)
return
def patchGeckoLoader(fLoader, codehandler, tmp, dolfile, entrypoint):
tmp.write(fLoader.read())
geckoloader_offset = dolfile.getsize()
figureLoaderData(tmp, fLoader, codehandler, entrypoint,
[bytes.fromhex('{:X}'.format(dolfile._init)[:4]), bytes.fromhex('{:X}'.format(dolfile._init)[4:])])
tmp.seek(0)
dolfile._rawdata.seek(0, 2)
dolfile._rawdata.write(tmp.read())
dolfile.align(256)
assertTextSections(dolfile, 6, [[int(entrypoint, 16), geckoloader_offset]])
'''Write game entry in DOL file header'''
dolfile.setInitPoint(int(entrypoint, 16))
def patchLegacyHandler(codehandler, tmp, dolfile):
handler_offset = dolfile.getsize()
dolfile._rawdata.seek(0, 2)
dolfile._rawdata.write(codehandler.codehandler.read() + codehandler.geckocodes.codelist.read())
dolfile.align(256)
assertTextSections(dolfile, 6, [[0x80001800, handler_offset]])
determineCodeHook(dolfile, codehandler)
def assertTextSections(dolfile, textsections, sections_list):
offset = len(dolfile._text) << 2
if len(sections_list) + len(dolfile._text) <= 7:
'''Write offset to each section in DOL file header'''
dolfile._rawdata.seek(offset)
for section_offset in sections_list:
dolfile._rawdata.write(bytes.fromhex('{:08X}'.format(section_offset[1]))) #offset in file
dolfile._rawdata.seek(0x48 + offset)
'''Write in game memory addresses for each section in DOL file header'''
for section_addr in sections_list:
dolfile._rawdata.write(bytes.fromhex('{:08X}'.format(section_addr[0]))) #absolute address in game
'''Get size of GeckoLoader + gecko codes, and the codehandler'''
size_list = []
for i, section_offset in enumerate(sections_list, start=1):
if i > len(sections_list) - 1:
size_list.append(dolfile.getsize() - section_offset[1])
else:
size_list.append(sections_list[i][1] - section_offset[1])
'''Write size of each section into DOL file header'''
dolfile._rawdata.seek(0x90 + offset)
for size in size_list:
dolfile._rawdata.write(bytes.fromhex('{:08X}'.format(size)))
else:
shutil.rmtree('tmp')
parser.error(TREDLIT + 'Not enough text sections to patch the DOL file! Potentially due to previous mods?\n' + TRESET)
def figureLoaderData(tmp, fLoader, codehandler, entrypoint, initpoint):
global _allocation, _codehook
upperAddr, lowerAddr = entrypoint[:int(len(entrypoint)/2)], entrypoint[int(len(entrypoint)/2):]
tmp.seek(0)
sample = tmp.read(4)
while sample:
if sample == HEAP: #Found keyword "HEAP". Goes with the resize of the heap
tmp.seek(-4, 1)
gpModInfoOffset = tmp.tell()
if int(lowerAddr, 16) + gpModInfoOffset > 0x7FFF: #Absolute addressing
gpModUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1))
else:
gpModUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16)))
if _allocation == None:
_allocation = '{:08X}'.format(codehandler.handlerlength + codehandler.geckocodes.size)
tmp.write(bytes.fromhex(_allocation))
elif sample == LOADERSIZE: #Found keyword "LSIZ". Goes with the size of the loader
tmp.seek(-4, 1)
tmp.write(get_size(fLoader))
elif sample == HANDLERSIZE: #Found keyword "HSIZ". Goes with the size of the codehandler
tmp.seek(-4, 1)
tmp.write(codehandler.handlerlength.to_bytes(4, byteorder='big', signed=True))
elif sample == CODESIZE: #Found keyword "CSIZ". Goes with the size of the codes
tmp.seek(-4, 1)
tmp.write(codehandler.geckocodes.size.to_bytes(4, byteorder='big', signed=True))
elif sample == CODEHOOK:
tmp.seek(-4, 1)
if _codehook == None:
tmp.write(b'\x00\x00\x00\x00')
else:
tmp.write(bytes.fromhex(_codehook))
sample = tmp.read(4)
gpDiscOffset = int.from_bytes(get_size(tmp, -4), byteorder="big", signed=False)
if int(lowerAddr, 16) + gpDiscOffset > 0x7FFF: #Absolute addressing
gpDiscUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1))
else:
gpDiscUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16)))
fillLoaderData(tmp, initpoint, lowerAddr, [gpModUpperAddr, gpModInfoOffset], [gpDiscUpperAddr, gpDiscOffset])
tmp.seek(0, 2)
tmp.write(codehandler.codehandler.read() + codehandler.geckocodes.codelist.read())
def fillLoaderData(tmp, _init, lowerAddr, gpModInfo, gpDiscInfo):
tmp.seek(0)
sample = tmp.read(2)
while sample:
if sample == DH:
tmp.seek(-2, 1)
tmp.write(gpDiscInfo[0])
elif sample == DL:
tmp.seek(-2, 1)
tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + gpDiscInfo[1])))
elif sample == GH:
tmp.seek(-2, 1)
tmp.write(gpModInfo[0])
elif sample == GL:
tmp.seek(-2, 1)
tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + gpModInfo[1])))
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)
def sortArgFiles(fileA, fileB):
global isText
if os.path.splitext(fileA)[1].lower() == '.dol':
dolFile = fileA
elif os.path.splitext(fileB)[1].lower() == '.dol':
dolFile = fileB
else:
parser.error('No dol file was passed\n')
if os.path.splitext(fileA)[1].lower() == '.gct':
gctFile = fileA
isText = False
elif os.path.splitext(fileA)[1].lower() == '.txt':
gctFile = fileA
isText = True
elif os.path.splitext(fileB)[1].lower() == '.gct':
gctFile = fileB
isText = False
elif os.path.splitext(fileB)[1].lower() == '.txt':
gctFile = fileB
isText = True
else:
parser.error('Neither a gct or gecko text file was passed\n')
return dolFile, gctFile
def determineCodeHook(dolfile, codehandler):
global GCNVIHOOK, WIIVIHOOK, _codehook
if _codehook == None:
assertCodeHook(dolfile, codehandler, GCNVIHOOK, WIIVIHOOK)
else:
insertCodeHook(dolfile, codehandler, int(_codehook, 16))
def assertCodeHook(dolfile, codehandler, GCNVIHOOK, WIIVIHOOK):
for offset, address, size in dolfile._text:
dolfile.seek(address, 0)
sample = dolfile.read(size)
if sample.find(GCNVIHOOK) != -1 or sample.find(WIIVIHOOK):
sample = dolfile.read(4)
while sample != b'4E800020':
sample = dolfile.read(4)
dolfile.seek(-4, 1)
insertCodeHook(dolfile, codehandler, dolfile.tell())
def insertCodeHook(dolfile, codehandler, address):
dolfile.seek(address)
if dolfile.read(4) == bytes.fromhex('4E800020'):
lk = 1
else:
parser.error("Codehandler hook given is not a blr")
dolfile.seek(-4, 1)
dolfile.write(((codehandler.startaddress - address) & 0x3FFFFFFF | 0x48000000 | lk).to_bytes(4, byteorder='big', signed=False))
if __name__ == "__main__":
isText = False
if not os.path.isdir('tmp'):
os.mkdir('tmp')
parser = argparse.ArgumentParser(prog='GeckoLoader',
description='Process files and allocations for GeckoLoader',
allow_abbrev=False)
parser.add_argument('file', help='First file')
parser.add_argument('file2', help='Second file')
parser.add_argument('-a', '--alloc',
help='Define the size of the code allocation in hex, only applies when using the ARENA space',
metavar ='SIZE')
parser.add_argument('-i', '--init',
help='Define where geckoloader is injected in hex',
metavar='ADDRESS')
parser.add_argument('-m', '--movecodes',
help='''Choose if geckoloader moves the codes to OSArenaHi,
or the legacy space. Default is "AUTO",
which auto decides where to insert the codes''',
default='AUTO',
choices=['AUTO', 'LEGACY', 'ARENA'],
metavar='TYPE')
parser.add_argument('-tc', '--txtcodes',
help='''What codes get parsed when a txt file is used.
"ALL" makes all codes get parsed,
"ACTIVE" makes only activated codes get parsed.''',
default='active',
metavar='TYPE')
parser.add_argument('--handler',
help='''Which codehandler gets used. "MINI" uses a smaller codehandler
which only supports (0x, 2x, Cx, and E0 types) and supports up to
600 lines of gecko codes when using the legacy codespace.
"FULL" is the standard codehandler, supporting up to 350 lines of code
in the legacy codespace.
"MINI" should only be considered if using the legacy codespace''',
default='FULL',
choices=['MINI', 'FULL'],
metavar='TYPE')
parser.add_argument('--codehook',
help='''Choose where the codehandler hooks to, needs to exist at a blr instruction''',
metavar='ADDRESS')
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:
try:
_allocation = '{:08X}'.format(int(args.alloc.lstrip('0x'), 16))
except:
parser.error('The allocation was invalid\n')
else:
_allocation = None
if args.codehook:
if int(args.codehook, 16) < 0x80000000 or int(args.codehook, 16) >= 0x81800000:
parser.error('The codehandler hook address was beyond bounds\n')
else:
try:
_codehook = '{:08X}'.format(int(args.codehook.lstrip('0x'), 16))
except:
parser.error('The codehandler hook address was invalid\n')
else:
_codehook = None
if args.handler:
if args.handler == 'MINI':
codehandlerFile = 'codehandler-mini.bin'
else:
codehandlerFile = 'codehandler.bin'
else:
codehandlerFile = 'codehandler.bin'
dolFile, gctFile = sortArgFiles(args.file, args.file2)
HEAP = b'HEAP'
LOADERSIZE = b'LSIZ'
HANDLERSIZE = b'HSIZ'
CODESIZE = b'CSIZ'
CODEHOOK = b'HOOK'
DH = b'DH'
DL = b'DL'
GH = b'GH'
GL = b'GL'
IH = b'IH'
IL = b'IL'
WIIVIHOOK = b'7CE33B783887003438A7003838C7004C'
GCNVIHOOK = b'7C030034388300205485083C7C7F2A14A00300007C7D2A1420A4003FB0030000'
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, codehandlerFile, _allocation)
shutil.rmtree('tmp')
if not args.quiet:
print(TGREENLIT + '\n :: Compiled in {:0.4f} seconds!\n'.format(time.time() - time1) + TRESET)
except FileNotFoundError as err:
parser.error(err)
sys.exit(1)