351 lines
15 KiB
Python
351 lines
15 KiB
Python
#Written by JoshuaMK 2020
|
|
|
|
import sys
|
|
import os
|
|
import re
|
|
import time
|
|
import shutil
|
|
import random
|
|
import argparse
|
|
|
|
from kernel import *
|
|
|
|
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: str):
|
|
""" 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 build(gctFile, dolFile, codehandlerFile, tmpdir, allocation: int, codehook: int):
|
|
with open(resource_path(os.path.join('bin', 'geckoloader.bin')), 'rb') as kernel, open(r'{}'.format(dolFile), 'rb') as dol, open(resource_path(os.path.join('bin', r'{}'.format(codehandlerFile))), 'rb') as handler, open(os.path.join(tmpdir, 'tmp.bin'), 'wb+') as tmp, open(os.path.join('BUILD', os.path.basename(dolFile)), 'wb+') as final:
|
|
|
|
if get_size(dol) < 0x100:
|
|
shutil.rmtree(tmpdir)
|
|
parser.error('DOL header is corrupted. Please provide a clean file')
|
|
|
|
dol.seek(0)
|
|
geckoKernel = KernelLoader(kernel)
|
|
|
|
'''Initialize the new DOL file'''
|
|
|
|
final.write(dol.read())
|
|
final.seek(0)
|
|
|
|
dolfile = DolFile(final)
|
|
|
|
'''Initialize our codehandler + codes'''
|
|
|
|
codehandler = CodeHandler(handler)
|
|
codehandler.allocation = allocation
|
|
codehandler.hookaddress = codehook
|
|
|
|
if '.' in gctFile:
|
|
if os.path.splitext(gctFile)[1].lower() == '.txt':
|
|
with open(os.path.join(tmpdir, 'gct.bin'), 'wb+') as temp:
|
|
temp.write(bytes.fromhex('00D0C0DE'*2 + codehandler.geckoParser(gctFile, args.txtcodes) + 'F000000000000000'))
|
|
temp.seek(0)
|
|
codehandler.geckocodes = GCT(temp)
|
|
elif os.path.splitext(gctFile)[1].lower() == '.gct':
|
|
with open(r'{}'.format(gctFile), 'rb') as gct:
|
|
codehandler.geckocodes = GCT(gct)
|
|
else:
|
|
parser.error('No valid gecko code file found')
|
|
else:
|
|
with open(os.path.join(tmpdir, 'gct.bin'), 'wb+') as temp:
|
|
temp.write(bytes.fromhex('00D0C0DE'*2))
|
|
|
|
for file in os.listdir(gctFile):
|
|
if os.path.isfile(os.path.join(gctFile, file)):
|
|
if os.path.splitext(file)[1].lower() == '.txt':
|
|
temp.write(bytes.fromhex(codehandler.geckoParser(os.path.join(gctFile, file), args.txtcodes)))
|
|
elif os.path.splitext(file)[1].lower() == '.gct':
|
|
with open(os.path.join(gctFile, file), 'rb') as gct:
|
|
temp.write(gct.read()[8:-8])
|
|
else:
|
|
print(TYELLOW + ' :: WARNING: {} is not a .txt or .gct file'.format(file) + TRESET)
|
|
|
|
temp.write(bytes.fromhex('F000000000000000'))
|
|
temp.seek(0)
|
|
codehandler.geckocodes = GCT(temp)
|
|
|
|
if args.optimize == True:
|
|
codehandler.geckocodes.optimize_codelist(dolfile)
|
|
|
|
'''Get entrypoint (or BSS midpoint) for insert'''
|
|
|
|
if args.init:
|
|
dump_address = args.init.lstrip("0x").upper()
|
|
try:
|
|
dolfile.resolve_address(int(dump_address, 16))
|
|
print(TYELLOW + '\n :: WARNING: Init address specified for GeckoLoader (0x{}) clobbers existing dol sections'.format(dump_address) + TRESET)
|
|
except RuntimeError:
|
|
pass
|
|
else:
|
|
dump_address = '{:08X}'.format(dolfile.bssOffset + (dolfile.bssSize >> 1))[:-2] + '00'
|
|
dump_address = '{:08X}'.format(dolfile.seek_safe_address(int(dump_address, 16), get_size(geckoKernel.rawData) + codehandler.handlerlength + codehandler.geckocodes.size))
|
|
geckoKernel.rawData.seek(0)
|
|
|
|
'''Is insertion legacy?'''
|
|
|
|
if codehandler.geckocodes.size <= 0x10:
|
|
dolfile.save(final)
|
|
if args.verbose >= 1:
|
|
print(TGREENLIT + '\n :: All codes have been successfully pre patched' + TRESET)
|
|
return
|
|
|
|
if args.movecodes == 'LEGACY':
|
|
codehandler.allocation = 0x80003000 - (codehandler.initaddress + codehandler.handlerlength)
|
|
status = geckoKernel.patch_legacy(codehandler, tmp, dolfile)
|
|
if status is False:
|
|
determine_codehook(dolfile, codehandler)
|
|
legacy = True
|
|
elif args.movecodes == 'ARENA':
|
|
status = geckoKernel.patch_arena(codehandler, tmp, dolfile, dump_address)
|
|
legacy = False
|
|
else: #Auto decide area
|
|
if codehandler.initaddress + codehandler.handlerlength + codehandler.geckocodes.size > 0x80002FFF:
|
|
status = geckoKernel.patch_arena(codehandler, tmp, dolfile, dump_address)
|
|
legacy = False
|
|
else:
|
|
codehandler.allocation = 0x80003000 - (codehandler.initaddress + codehandler.handlerlength)
|
|
status = geckoKernel.patch_legacy(codehandler, tmp, dolfile)
|
|
if status is False:
|
|
determine_codehook(dolfile, codehandler)
|
|
legacy = True
|
|
|
|
if status is False:
|
|
shutil.rmtree(tmpdir)
|
|
parser.error(TREDLIT + 'Not enough text sections to patch the DOL file! Potentially due to previous mods?\n' + TRESET)
|
|
|
|
dolfile.save(final)
|
|
|
|
if codehandler.allocation < 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 codehandler.allocation > 0x70000:
|
|
print(TYELLOW + f'\n :: WARNING: Allocations beyond 0x70000 will crash certain games. You allocated 0x{codehandler.allocation:X}' + TRESET)
|
|
|
|
elif codehandler.allocation > 0x40000:
|
|
print(TYELLOWLIT + f'\n :: HINT: Recommended allocation limit is 0x40000. You allocated 0x{codehandler.allocation:X}' + TRESET)
|
|
|
|
if args.verbose >= 2:
|
|
print('')
|
|
if legacy == False:
|
|
info = [TGREENLIT + f' :: GeckoLoader set at address 0x{dump_address.upper()}, start of game modified to address 0x{dump_address.upper()}',
|
|
f' :: Game function "_entryPoint_registers" located at address 0x{dolfile.entryPoint:X}'.format(),
|
|
f' :: Code allocation is 0x{codehandler.allocation:X}; codelist size is 0x{codehandler.geckocodes.size:X}',
|
|
f' :: Codehandler is of type "{codehandler.type}"'
|
|
f' :: Of the 7 text sections in this DOL file, {len(dolfile.textSections)} were already used' + TRESET]
|
|
if codehandler.hookaddress is not None:
|
|
info.insert(2, f' :: Codehandler hooked at 0x{codehandler.hookaddress:08X}')
|
|
|
|
else:
|
|
info = [TGREENLIT + f' :: Game function "_entryPoint_registers" located at address 0x{dolfile.entryPoint:X}',
|
|
f' :: Code allocation is 0x{codehandler.allocation:X}; codelist size is 0x{codehandler.geckocodes.size:X}',
|
|
f' :: Codehandler is of type "{codehandler.type}"',
|
|
f' :: Of the 7 text sections in this DOL file, {len(dolfile.textSections)} were already used' + TRESET]
|
|
if codehandler.hookaddress is not None:
|
|
info.insert(1, f' :: Codehandler hooked at 0x{codehandler.hookaddress:08X}')
|
|
for bit in info:
|
|
print(bit)
|
|
|
|
elif args.verbose >= 1:
|
|
print('')
|
|
if legacy == False:
|
|
info = [TGREENLIT + f' :: GeckoLoader set at address 0x{dump_address.upper()}',
|
|
f' :: Codehandler is of type "{args.handler}"',
|
|
f' :: Code allocation is 0x{codehandler.allocation} in hex; codelist size is 0x{codehandler.geckocodes.size:X}' + TRESET]
|
|
else:
|
|
info = [TGREENLIT + f' :: Codehandler is of type "{args.handler}"',
|
|
f' :: Code allocation is 0x{codehandler.allocation} in hex; codelist size is 0x{codehandler.geckocodes.size:X}' + TRESET]
|
|
|
|
for bit in info:
|
|
print(bit)
|
|
|
|
def determine_codehook(dolfile: DolFile, codehandler: CodeHandler):
|
|
if codehandler.hookaddress == None:
|
|
assert_code_hook(dolfile, codehandler, GCNVIHOOK, WIIVIHOOK)
|
|
else:
|
|
insert_code_hook(dolfile, codehandler, codehandler.hookaddress)
|
|
|
|
def assert_code_hook(dolfile: DolFile, codehandler: CodeHandler, gcnhook: bytes, wiihook: bytes):
|
|
for offset, address, size in dolfile.textSections:
|
|
dolfile.seek(address, 0)
|
|
sample = dolfile.read(size)
|
|
|
|
result = sample.find(gcnhook)
|
|
if result >= 0:
|
|
dolfile.seek(address, 0)
|
|
dolfile.seek(result, 1)
|
|
else:
|
|
result = sample.find(wiihook)
|
|
if result >= 0:
|
|
dolfile.seek(address, 0)
|
|
dolfile.seek(result, 1)
|
|
else:
|
|
continue
|
|
|
|
sample = dolfile.read(4)
|
|
while sample != b'\x4E\x80\x00\x20':
|
|
sample = dolfile.read(4)
|
|
|
|
dolfile.seek(-4, 1)
|
|
codehandler.hookaddress = dolfile.tell()
|
|
|
|
insert_code_hook(dolfile, codehandler, codehandler.hookaddress)
|
|
return
|
|
|
|
parser.error('Failed to find a hook address. Try using option --codehook to use your own address')
|
|
|
|
def insert_code_hook(dolfile: DolFile, codehandler: CodeHandler, address: int):
|
|
dolfile.seek(address)
|
|
|
|
if dolfile.read(4) != b'\x4E\x80\x00\x20':
|
|
parser.error("Codehandler hook given is not a blr")
|
|
|
|
dolfile.seek(-4, 1)
|
|
dolfile.insert_branch(codehandler.startaddress, address, lk=0)
|
|
|
|
|
|
def sort_file_args(fileA, fileB):
|
|
if os.path.splitext(fileA)[1].lower() == '.dol':
|
|
dolFile = fileA
|
|
gctFile = fileB
|
|
elif os.path.splitext(fileB)[1].lower() == '.dol':
|
|
dolFile = fileB
|
|
gctFile = fileA
|
|
else:
|
|
parser.error('No dol file was passed\n')
|
|
return dolFile, gctFile
|
|
|
|
if __name__ == "__main__":
|
|
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')
|
|
parser.add_argument('-o', '--optimize',
|
|
help='''Optimizes the codelist by directly patching qualifying
|
|
ram writes into the dol file, and removing them from the codelist''',
|
|
action='store_true')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.alloc:
|
|
try:
|
|
_allocation = 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 = 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 = sort_file_args(args.file, args.file2)
|
|
|
|
try:
|
|
if not os.path.isdir('BUILD'):
|
|
os.mkdir('BUILD')
|
|
|
|
if not os.path.isfile(dolFile):
|
|
parser.error('File "' + dolFile + '" does not exist')
|
|
|
|
if not os.path.exists(gctFile):
|
|
parser.error('File/folder "' + gctFile + '" does not exist')
|
|
|
|
time1 = time.time()
|
|
|
|
tmpdir = ''.join(random.choice('1234567890-_abcdefghijklomnpqrstuvwxyz') for i in range(6)) + '-GeckoLoader'
|
|
|
|
if not os.path.isdir(tmpdir):
|
|
os.mkdir(tmpdir)
|
|
|
|
build(gctFile, dolFile, codehandlerFile, tmpdir, _allocation, _codehook)
|
|
|
|
shutil.rmtree(tmpdir)
|
|
if not args.quiet:
|
|
print(TGREENLIT + f'\n :: Compiled in {(time.time() - time1):0.4f} seconds!\n' + TRESET)
|
|
|
|
except FileNotFoundError as err:
|
|
parser.error(err)
|
|
sys.exit(1)
|