diff --git a/main.py b/main.py index 3e8e6b6..6395dd9 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,76 @@ #Written by JoshuaMK 2020 -import sys, os, time +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 build(gct, dol, size): - with open(resource_path('sme-code.bin'), 'rb') as code, open(dol, 'rb+') as dol, open(gct, 'rb') as gecko, open(resource_path('codehandler.bin'), 'rb') as handler, open('tmp.bin', 'wb+') as tmp, open(os.path.join('BUILD', os.path.split(r'{}'.format(dol))[1][:-2]), 'wb+') as final: +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('sme-code.bin'), 'rb') as code, open(dolFile, 'rb+') as dol, open(gctFile, 'rb') as gecko, open(resource_path('codehandler.bin'), 'rb') as handler, open('tmp.bin', 'wb+') as tmp, open(os.path.join('BUILD', dolFile), 'wb+') as final: '''Initialize the new DOL file''' final.write(dol.read()) @@ -48,52 +110,43 @@ def build(gct, dol, size): '''Get code initialize address''' final.seek(int('E0', 16)) - _init = final.read(4) + _init = [final.read(2), final.read(2)] '''Patch the values for the addresses and such''' - hooked = False heaped = False sized = False fsized = False - initUpperAddr = bytes.fromhex(upperAddr) - geckoUpperAddr = bytes.fromhex(upperAddr) gUpperAddr = bytes.fromhex(upperAddr) + + if isText == True: + geckoCheats = geckoParser(gctFile, args.txtcodes) - while hooked == False or heaped == False or sized == False or fsized == False: + while heaped == False or sized == False or fsized == False: try: sample = tmp.read(4) - if sample == HOOK: #Found keyword "HOOK". Goes with the entry to the game - if not hooked: - tmp.seek(-4, 1) - - initInfo = tmp.tell() - if int(lowerAddr, 16) + initInfo > int('7FFF', 16): #Absolute addressing - initUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1)) - if int(lowerAddr, 16) + (initInfo + 4) > int('7FFF', 16): #Absolute addressing - geckoUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1)) - - tmp.write(_init) - hooked = True - elif sample == HEAP: #Found keyword "HEAP". Goes with the resize of the heap + if sample == HEAP: #Found keyword "HEAP". Goes with the resize of the heap if not heaped: tmp.seek(-4, 1) - gInfo = tmp.tell() if int(lowerAddr, 16) + gInfo > int('7FFF', 16): #Absolute addressing gUpperAddr = bytes.fromhex('{:04X}'.format(int(upperAddr, 16) + 1)) - if size == '0' or size == '': - tmp.write(get_size(gecko)) + if isText == False: + size = get_size(gecko).hex().upper() + else: + size = geckoCheats[1] else: 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) @@ -102,8 +155,7 @@ def build(gct, dol, size): tmp.write(get_size(code, gecko.tell())) fsized = True except TypeError as err: - print('Fatal error (' + err + '), build failed to complete') - time.sleep(3) + parser.error(err) sys.exit(1) '''Patch all load/store offsets to data''' @@ -117,41 +169,37 @@ def build(gct, dol, size): elif sample == GL: tmp.seek(-2, 1) tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + gInfo))) - elif sample == CH: - tmp.seek(-2, 1) - tmp.write(geckoUpperAddr) - elif sample == CL: - tmp.seek(-2, 1) - tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + (initInfo + 4)))) elif sample == IH: tmp.seek(-2, 1) - tmp.write(initUpperAddr) + tmp.write(_init[0]) elif sample == IL: tmp.seek(-2, 1) - tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + initInfo))) - elif sample == JH: - tmp.seek(-2, 1) - tmp.write(geckoUpperAddr) - elif sample == JL: - tmp.seek(-2, 1) - tmp.write(bytes.fromhex('{:04X}'.format(int(lowerAddr, 16) + (initInfo + 8)))) + tmp.write(_init[1]) sample = tmp.read(2) tmp.seek(0) gecko.seek(0) + dol_handler_offset = get_size(final) final.write(handler.read()) time.sleep(0.01) dol_sme_offset = get_size(final) + final.write(tmp.read()) time.sleep(0.01) - final.write(gecko.read()) + + if isText == False: + final.write(gecko.read()) + else: + final.write(geckoCheats[0]) final.seek(0, 0) + status = False i = 0 + while i < 6: - size = int(final.read(4).hex(), 16) - if size == 0: + textOffset = int(final.read(4).hex(), 16) + if textOffset == 0: status = True offset = i * 4 final.seek(-4, 1) @@ -173,8 +221,45 @@ def build(gct, dol, size): break else: i += 1 + if status == False: - print('Not enough sections to patch the DOL file! Potentially due to previous mods?') + parser.error(TREDLIT + '\n :: ERROR: Not enough text sections to patch the DOL file! Potentially due to previous mods?\n' + TRESET) + + 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) + + 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 6 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 def get_size(file, offset=0): """ Return a file's size in bytes """ @@ -183,114 +268,62 @@ def get_size(file, offset=0): if __name__ == "__main__": - if len(sys.argv) == 1: - while True: - gct = input('Name of the input GCT file? ') - if os.path.splitext(gct)[1] == '': - gct = gct + '.gct' - if os.path.splitext(gct)[1] != '.gct' and os.path.splitext(gct)[1] != '.GCT': - print('Invalid input!') - continue - if os.path.exists(gct): - gct = os.path.abspath(gct) - break - else: - print('File not found! Please try again.') - while True: - dol = input('Name of the input dol file? ') - if os.path.splitext(dol)[1] == '': - dol = dol + '.dol' - if os.path.splitext(dol)[1] != '.dol': - print('Invalid input!') - continue - if os.path.exists(dol): - dol = os.path.abspath(dol) - break - else: - print('File not found! Please try again.') - elif len(sys.argv) == 2: - if sys.argv[1].endswith('.gct') or sys.argv[1].endswith('.GCT'): - gct = sys.argv[1] - while True: - dol = input('Name of the input dol file? ') - if os.path.splitext(dol)[1] == '': - dol = dol + '.dol' - if os.path.splitext(dol)[1] != '.dol': - print('Invalid input!') - continue - if os.path.exists(dol): - dol = os.path.abspath(dol) - break - else: - print('File not found! Please try again.') - elif sys.argv[1].endswith('.dol'): - dol = sys.argv[1] - while True: - gct = input('Name of the input GCT file? ') - if os.path.splitext(gct)[1] == '': - gct = gct + '.gct' - if os.path.splitext(gct)[1] != '.gct' and os.path.splitext(gct)[1] != '.GCT': - print('Invalid input!') - continue - if os.path.exists(gct): - gct = os.path.abspath(gct) - break - else: - print('File not found! Please try again.') - else: - print('The given file is invalid! Please provide a valid dol file, a valid GCT file, or both.') - time.sleep(1) - sys.exit(1) - else: - if sys.argv[1].endswith('.gct') or sys.argv[1].endswith('.GCT'): - gct = sys.argv[1] - if sys.argv[2].endswith('.dol'): - dol = sys.argv[2] - else: - while True: - dol = input('Name of the input DOL file? ') - if os.path.splitext(dol)[1] == '': - dol = dol + '.dol' - if os.path.splitext(dol)[1] != '.dol': - print('Invalid input!') - continue - if os.path.exists(dol): - dol = os.path.abspath(dol) - break - else: - print('File not found! Please try again.') - elif sys.argv[1].endswith('.dol'): - dol = sys.argv[1] - if sys.argv[2].endswith('.gct') or sys.argv[2].endswith('.GCT'): - gct = sys.argv[2] - else: - while True: - gct = input('Name of the input GCT file? ') - if os.path.splitext(gct)[1] == '': - gct = gct + '.gct' - if os.path.splitext(gct)[1] != '.gct' and os.path.splitext(gct)[1] != '.GCT': - print('Invalid input!') - continue - if os.path.exists(gct): - gct = os.path.abspath(gct) - break - else: - print('File not found! Please try again.') - else: - print('The given files are invalid! Please provide a valid DOL file, a valid GCT file, or both.') - sys.exit(1) - - while True: - size = input('Define code allocation in hex. (Type 0 or press Enter on empty input for auto size): ') + colorama.init() + + TRESET = '\033[0m' + TBOLD = '\033[1m' + + TGREEN = '\033[32m' + TGREENLIT = '\033[92m' + TYELLOW = '\033[33m' + TYELLOWLIT = '\033[93m' + TRED = '\033[31m' + TREDLIT = '\033[91m' + + 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) - break - except Exception: - if size == '': - break - else: - print('Invalid input! {} is not hexadecimal!'.format(size)) + except: + parser.error(TREDLIT + '\n :: ERROR: The allocation was invalid\n' + TRESET) + 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(TREDLIT + '\n :: ERROR: No dol file was passed\n' + TRESET) + + 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(TREDLIT + '\n :: ERROR: Neither a gct or gecko text file was passed\n' + TRESET) + time1 = time.time() HEAP = bytes.fromhex('48454150') @@ -310,12 +343,11 @@ if __name__ == "__main__": try: if not os.path.isdir('BUILD'): os.mkdir('BUILD') - build(gct, dol, size) + build(gctFile, dolFile, size, isText) os.remove('tmp.bin') - print('Compiled in {:0.4f} seconds!'.format(time.time() - time1)) - time.sleep(4) + if not args.quiet: + print(TGREENLIT + '\n :: Compiled in {:0.4f} seconds!\n'.format(time.time() - time1) + TRESET) - except Exception as err: - print(err) - time.sleep(4) + except FileNotFoundError as err: + parser.error(err) sys.exit(1)