diff --git a/GeckoLoader.py b/GeckoLoader.py new file mode 100644 index 0000000..7f2fea7 --- /dev/null +++ b/GeckoLoader.py @@ -0,0 +1,522 @@ +#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) diff --git a/bin/codehandler-mini.bin b/bin/codehandler-mini.bin new file mode 100644 index 0000000..8f39134 Binary files /dev/null and b/bin/codehandler-mini.bin differ diff --git a/bin/codehandler.bin b/bin/codehandler.bin new file mode 100644 index 0000000..0027fca Binary files /dev/null and b/bin/codehandler.bin differ diff --git a/bin/geckoloader.bin b/bin/geckoloader.bin new file mode 100644 index 0000000..941791a Binary files /dev/null and b/bin/geckoloader.bin differ diff --git a/dolreader.py b/dolreader.py new file mode 100644 index 0000000..d8b2dbf --- /dev/null +++ b/dolreader.py @@ -0,0 +1,180 @@ +import struct +from io import BytesIO, RawIOBase + + +def read_ubyte(f): + return struct.unpack("B", f.read(1))[0] + +def read_uint32(f): + return struct.unpack(">I", f.read(4))[0] + +def write_uint32(f, val): + f.write(struct.pack(">I", val)) + +class DolFile(object): + def __init__(self, f): + self._rawdata = BytesIO(f.read()) + fileoffset = 0 + addressoffset = 0x48 + sizeoffset = 0x90 + + self._text = [] + self._data = [] + + nomoretext = False + nomoredata = False + + self._current_end = None + + # Read text and data section addresses and sizes + for i in range(18): + f.seek(fileoffset+i*4) + offset = read_uint32(f) + f.seek(addressoffset+i*4) + address = read_uint32(f) + f.seek(sizeoffset+i*4) + size = read_uint32(f) + + if i <= 6: + if offset == 0: + nomoretext = True + elif not nomoretext: + self._text.append((offset, address, size)) + # print("text{0}".format(i), hex(offset), hex(address), hex(size)) + else: + #datanum = i - 7 + if offset == 0: + nomoredata = True + elif not nomoredata: + self._data.append((offset, address, size)) + # print("data{0}".format(datanum), hex(offset), hex(address), hex(size)) + + f.seek(0xD8) + self._bssoffset = read_uint32(f) + self._bsssize = read_uint32(f) + self._init = read_uint32(f) + + self.bss = BytesIO(self._rawdata.getbuffer()[self._bssoffset:self._bssoffset+self._bsssize]) + + self._curraddr = self._text[0][1] + self.seek(self._curraddr) + f.seek(0) + + # Internal function for + def _resolve_address(self, gc_addr): + for offset, address, size in self._text: + if address <= gc_addr < address+size: + return offset, address, size + for offset, address, size in self._data: + if address <= gc_addr < address+size: + return offset, address, size + + raise RuntimeError("Unmapped address: {0}".format(hex(gc_addr))) + + # Unsupported: Reading an entire dol file + # Assumption: A read should not go beyond the current section + def read(self, size): + if self._curraddr + size >= self._current_end: + raise RuntimeError("Read goes over current section") + + self._curraddr += size + return self._rawdata.read(size) + + # Assumption: A write should not go beyond the current section + def write(self, data): + if self._curraddr + len(data) >= self._current_end: + raise RuntimeError("Write goes over current section") + + self._rawdata.write(data) + self._curraddr += len(data) + + def seek(self, where, whence=0): + if whence == 0: + offset, gc_start, gc_size = self._resolve_address(where) + self._rawdata.seek(offset + (where-gc_start)) + + self._curraddr = where + self._current_end = gc_start + gc_size + elif whence == 1: + offset, gc_start, gc_size = self._resolve_address(self._curraddr + where) + self._rawdata.seek(offset + ((self._curraddr + where)-gc_start)) + + self._curraddr += where + self._current_end = gc_start + gc_size + else: + raise RuntimeError("Unsupported whence type '{}'".format(whence)) + + def tell(self): + return self._curraddr + + def save(self, f): + f.seek(0) + f.write(self._rawdata.getbuffer()) + + def getsize(self): + oldpos = self._rawdata.tell() + self._rawdata.seek(0, 2) + size = self._rawdata.tell() + self._rawdata.seek(oldpos) + return size + + def getalignment(self, alignment): + size = self.getsize() + + if size % alignment != 0: + return alignment - (size % alignment) + else: + return 0 + + def setInitPoint(self, address): + oldpos = self._rawdata.tell() + self._rawdata.seek(0xE0) + self._rawdata.write(bytes.fromhex('{:08X}'.format(address))) + self._rawdata.seek(oldpos) + + def align(self, alignment): + oldpos = self._rawdata.tell() + self._rawdata.seek(0, 2) + self._rawdata.write(bytes.fromhex("00" * self.getalignment(alignment))) + self._rawdata.seek(oldpos) + + + +if __name__ == "__main__": + # Example usage (reading some enemy info from the Pikmin 2 demo from US demo disc 17) + + def read_string(f): + start = f.tell() + length = 0 + while f.read(1) != b"\x00": + length += 1 + if length > 100: + break + + f.seek(start) + return f.read(length) + + entries = [] + + with open("main.dol", "rb") as f: + dol = DolFile(f) + + start = 0x804ac478 # memory address to start of enemy info table. + + for i in range(100): + dol.seek(start+0x34*i, 0) + + # string offset would normally be pointing to a location in RAM and thus + # wouldn't be suitable as a file offset but because the seek function of DolFile + # takes into account the memory address at which the data sections of the dol file + # is loaded, we can use the string offset directly.. + stringoffset = read_uint32(dol) + identifier = read_ubyte(dol) + dol.seek(stringoffset, 0) + name = read_string(dol) + + entries.append((identifier,i, name, hex(stringoffset))) + + entries.sort(key=lambda x: x[0]) + for val in entries: + print(hex(val[0]), val) \ No newline at end of file diff --git a/loader.cpp b/loader.cpp new file mode 100644 index 0000000..9ae9ac4 --- /dev/null +++ b/loader.cpp @@ -0,0 +1,221 @@ +/*Credits to riidefi for hook code, cache asm, and teaching me C*/ + +#define dcbst(_val) asm volatile("dcbst 0, %0" \ + : \ + : "r"(_val)) +#define dcbf(_val) asm volatile("dcbf 0, %0" \ + : \ + : "r"(_val)) +#define icbi(_val) asm volatile("icbi 0, %0" \ + : \ + : "r"(_val)) + +#define call(addr) ((void (*)(...))addr) +#define MEM1_START 0x80000000 +#define MEM1_END 0x81800000 +#define CODEHANDLER 0x800018A8 +#define GCT_MAGIC 0x00D0C0DE + +typedef unsigned int u32; +typedef unsigned short u16; +typedef unsigned char u8; +typedef int s32; +typedef short s16; +typedef char s8; + +__attribute__((noreturn)) int main(); + +struct CodeList { + u16 mBaseASM; + u16 mUpperBase; + u16 mOffsetASM; + u16 mLowerOffset; +}; + +struct Info { + const u32 allocsize; + const u32 loaderSize; + const u32 handlerSize; + const u32 codeSize; + const u32* codehandlerHook; + const u32 wiiVIHook[4]; + const u32 gcnVIHook[8]; +}; + +struct DiscInfo { + const u8 mDiscID; + const u16 mGameCode; + const u8 mRegionCode; + const u16 mMakerCode; + const u8 mDiscNumber; + const u8 mDiscVersion; + const u8 mAudioStreaming; + const u8 mStreamBufferSize; + const u8 _00[12]; + const u32 mWiiMagic; + const u32 mGCNMagic; + const u32 _01[2]; + u32 mRAMSize; + const u32 _02[2]; + u32* mHeapPointer; + u32 mHeapMirror; + u32 mFstSize; + u32 mData[0x30D0 / 4]; + u32 mWiiHeap; +}; + +Info gpModInfo = { + 0, /*This is the code allocation*/ + 0, /*This is the size of the GeckoLoader*/ + 0, /*This is the size of the codehandler*/ + 0, /*This is the size of the GeckoLoader + the codelist*/ + 0, /*This is the codehandler hook address*/ + { 0x7CE33B78, 0x38870034, 0x38A70038, 0x38C7004C }, + { 0x7C030034, 0x38830020, 0x5485083C, 0x7C7F2A14, 0xA0030000, 0x7C7D2A14, 0x20A4003F, 0xB0030000 }, +}; + +DiscInfo* gpDiscResources = (DiscInfo*)MEM1_START; + +static inline void flushAddr(void* addr) +{ + dcbf(addr); + icbi(addr); +} + +static inline void directWrite(u32* addr, u32 value) +{ + *addr = value; + flushAddr(addr); +} + +/*This constructs a branch instruction. &TO = ((TO - FROM) & MAX_OFFSET) | BRANCH_TYPE | !!isLink*/ +static inline void directBranchEx(void* addr, void* ptr, bool lk) +{ + directWrite((u32*)(addr), ((((u32)(ptr) - (u32)(addr)) & 0x3ffffff) | 0x48000000 | !!lk)); +} + +static inline u32* findArrayInstance(u32* start, const u32 end, u32 arrayLength, const u32* hookData) +{ + u32 index = 0; + + /*Loop through the games RAM, make sure we don't find our own hook data by accident*/ + for (u32 i = 0; (u32)&start[i] < end; ++i) { + /*If the data matches, increase the index counter and continue search, + else set index to 0 and continue searching*/ + if (start[i] == hookData[index]) + ++index; + else + index = 0; + + /*If the data has matched the whole array, return the address of the match*/ + if (index >= (arrayLength) && ((u32)&start[i] < (u32)&gpModInfo || (u32)&start[i] > (u32)&gpModInfo + sizeof(Info))) + return (u32*)&start[i]; + } + return nullptr; +} + +static inline u32* findU32Instance(u32* start, u32 end, u32 hookData) +{ + for (u32 i = 0; (u32)&start[i] < end; ++i) { + if (start[i] == hookData) { + return (u32*)&start[i]; + } + } + return nullptr; +} + +/*Find VI hook for Game*/ +static inline u32* findVIHook(u32* start, const u32 end) +{ + const u32* hookData; + u32 arrayLength; + + /*If the game is built for the Wii, set the hookdata to be the Wii variant*/ + if (gpDiscResources->mWiiMagic) { + hookData = (const u32*)gpModInfo.wiiVIHook; + arrayLength = sizeof(gpModInfo.wiiVIHook) / sizeof(u32); + } else /*The game is built for the GCN, set the hookdata to be the GCN variant*/ + { + hookData = (const u32*)gpModInfo.gcnVIHook; + arrayLength = sizeof(gpModInfo.gcnVIHook) / sizeof(u32); + } + return findArrayInstance(start, end, arrayLength, hookData); +} + +/*Call this after findFunction, finds the address of the first instance +of value hookInstruction, and hooks it to the pointer hookTo*/ +static inline void hookFunction(u32* start, u32 hookInstruction, u32 hookTo, bool isLink) +{ + int i = 0; + while (start[i] != hookInstruction) { + ++i; + } + directBranchEx((u32*)(&start[i]), (void*)(hookTo), isLink); +} + +/*Reallocate the games internal memory heap based on the console +the game is for, to make space for our codes*/ +static inline void setHeap(u32 alloc) +{ + if (gpDiscResources->mWiiMagic) { + gpDiscResources->mHeapPointer = (u32*)((u32)gpDiscResources->mWiiHeap - alloc); + gpDiscResources->mWiiHeap = (u32)gpDiscResources->mHeapPointer; + } else { + gpDiscResources->mHeapPointer = (u32*)((u32)gpDiscResources->mHeapPointer - alloc); + } +} + +static inline void memCopy(u32* to, u32* from, s32 size) +{ + for (s32 i = 0; i < size; ++i) { + *to++ = *from++; + } +} + +static inline void flushCacheRange(u8* addr, s32 size) +{ + if ((u32)addr & 31) size += 32; + else size += 31; + + for (u32 i = 0; i < (size >> 5); ++i) { + flushAddr((void*)addr); + addr += 32; + } +} + +static inline bool initMods(DiscInfo* gpDiscResources) +{ + setHeap(gpModInfo.allocsize); /*Reallocate the internal heap*/ + + /*Change codelist pointer to the new address in the allocation*/ + CodeList* codelistPointer = (CodeList*)((u32)&gpModInfo + sizeof(gpModInfo) + 0xFC); + codelistPointer->mUpperBase = (((u32)gpDiscResources->mHeapPointer + gpModInfo.handlerSize) >> 16) & 0xFFFF; + codelistPointer->mLowerOffset = ((u32)gpDiscResources->mHeapPointer + gpModInfo.handlerSize) & 0xFFFF; + + /*Copy codelist to the new allocation*/ + memCopy(gpDiscResources->mHeapPointer, (u32*)((u32)&gpModInfo + sizeof(gpModInfo) + 4), (gpModInfo.handlerSize + gpModInfo.codeSize) >> 2); + + /*Update the cache, so that the instructions fully update*/ + flushAddr(&codelistPointer->mBaseASM); + + if (!gpModInfo.codehandlerHook || *gpModInfo.codehandlerHook != 0x4E800020) { + u32* functionAddr = findVIHook((u32*)MEM1_START, MEM1_END); + if (functionAddr == nullptr) + return false; + + hookFunction(functionAddr, 0x4E800020, (u32)gpDiscResources->mHeapPointer + 0xA8, false); + } else { + directBranchEx((void*)gpModInfo.codehandlerHook, (void*)((u32)gpDiscResources->mHeapPointer + 0xA8), false); + } + + flushCacheRange((u8*)gpDiscResources->mHeapPointer, gpModInfo.handlerSize + gpModInfo.codeSize); + return true; +} + +int main() +{ + if ((gpDiscResources->mWiiMagic || gpDiscResources->mGCNMagic) && initMods(gpDiscResources) == true) + call((void*)((u32)(gpDiscResources->mHeapPointer) + 0xA8))(); /*Call the codehandler if successful*/ + + call(0xDEADBEEF)(); /*Call the game start*/ +} \ No newline at end of file