diff --git a/.qt_for_python/lupdate/main_ui.qt.ts b/.qt_for_python/lupdate/main_ui.qt.ts new file mode 100644 index 0000000..5b17fa6 --- /dev/null +++ b/.qt_for_python/lupdate/main_ui.qt.ts @@ -0,0 +1,309 @@ + + + + Dialog + + + FULL + + + + + MINI + + + + + VI + + + + + GX + + + + + PAD + + + + + ACTIVE + + + + + ALL + + + + + MainWindow + + + &File + + + + + &Edit + + + + + &Help + + + + + &Open Session... + + + + + Open a session + + + + + Ctrl+O + + + + + &Close Session... + + + + + Close the current session + + + + + Ctrl+Shift+C + + + + + &Save Session + + + + + Save the current session + + + + + Ctrl+S + + + + + &Save Session As... + + + + + Save the current session to the specified location + + + + + Ctrl+Shift+S + + + + + Undo + + + + + Undo the last action + + + + + Ctrl+Z + + + + + Redo + + + + + Redo the last action + + + + + Ctrl+Shift+Z + + + + + Cut + + + + + Cuts the selected text and places it on the clipboard + + + + + Ctrl+X + + + + + Copy + + + + + Copies the selected text and places it on the clipboard + + + + + Ctrl+C + + + + + Paste + + + + + Paste the contents of the clipboard + + + + + Ctrl+V + + + + + Delete + + + + + Deletes the selected text + + + + + Select All + + + + + Select all of the text + + + + + Ctrl+A + + + + + &Preferences... + + + + + Open the application preferences dialog + + + + + About &GeckoLoader... + + + + + About &Qt... + + + + + &Check Update + + + + + Files + + + + + Open DOL + + + + + Open Codes + + + + + Open Folder + + + + + Destination + + + + + Options + + + + + Allocation + + + + + AUTO + + + + + Codehandler + + + + + Code Hook + + + + + Include Codes + + + + + Advanced Settings + + + + + RUN + + + + diff --git a/README.md b/README.md index cc0db00..421b32d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,87 @@ -# GeckoLoader -**GeckoLoader is a command line tool, providing an easy way to have near unlimited code space, allowing thousands of lines of gecko code for every Wii/GCN game.** +![GeckoLoaderLogo](https://user-images.githubusercontent.com/60854312/159154005-b666fa45-67a6-4c6b-8a61-b986676e6083.png) -**Windows** +--- + +### GeckoLoader is a command line tool, providing an easy way to have near unlimited code space, allowing thousands of lines of gecko code for every Wii/GCN game. + +## Windows ![Imgur](https://i.imgur.com/3NiQ4T4.png) -**Linux** +## Linux ![Imgur](https://i.imgur.com/6cDiMW7.png) -**Codes** +--- + +## Installation + +Run the installer and choose to install `GeckoLoader` + +## Usage + +### Codes `GeckoLoader` supports 2 methods: 1. GCT files (Raw codelist) 2. Textual Codelist (Ocarina Manager or Dolphin Format txt files) -`GeckoLoader` also supports the ability to use a folder filled with GCT files and/or Textual codelists as input for multi codelist patching +`GeckoLoader` also supports the ability to use a folder filled with GCT files and/or Textual codelists as input for multi codelist patching. -**DOL File** +### DOL -`GeckoLoader` needs a valid `dol` file to patch with your codes. Simply supply the path to your `dol` file in either the GUI or the CLI +`GeckoLoader` needs a valid `dol` file to patch with your codes. Simply supply the path to your `dol` file in either the GUI or the CLI. `GeckoLoader` also supports patching the same `dol` file multiple times until the file becomes filled with section data. -**Steps to compile GeckoLoader** - -Run the installer and choose to install `GeckoLoader` +### Compilation Then you can do either: + 1. In command prompt, input `GeckoLoader -h` for help on syntax and options 2. Run the command `GeckoLoader ` filling in the variables as needed Or: + 1. Fill out the relevant data in the GUI - 2. Click the RUN button + 2. Click the "RUN" button + +Your new patched `dol` file will be in the folder `./geckoloader-build` by default. + +## Common Issues + +### The allocation was invalid + +This means the manual allocation amount you've passed into GeckoLoader is not a hexidecimal number. + +### The codehandler hook address was beyond bounds + +This means the manual hook address you've passed into GeckoLoader is beyond valid range. Values from 0x80000000 to 0x817FFFFF (inclusive) are allowed. + +### The codehandler hook address was invalid + +This means the manual hook address you've passed into GeckoLoader is not a hexidecimal number. + +### There are no unused sections left for GeckoLoader to use + +This means you've used up all of the remaining text/data sections, and GeckoLoader can't make space for itself. Try using a fresh DOL if this has been patched many times. Otherwise you're out of luck. + +### Init address specified for GeckoLoader (x) clobbers existing dol sections + +This means the address you specified manually for GeckoLoader to initialize at is about to mutilate your game and thus, try a safer address instead. + +### Failed to find a hook address + +This means it couldn't find a known location to hook to. This can be resolved by trying different hooktypes, or manually specifying a hook address. + +### Allocated codespace was smaller than the given codelist + +This means you manually specified an allocation size that happens to be smaller than the codelist's minimum space requirements. Give it some more room to work with or let it automatically decide the allocation amount. + +## Credits + +- Wiimm - Original idea and implementation this is based from +- Riidefi - Taught me C/C++, which this uses -Your new patched `dol` file will be in the folder `./geckoloader-build` by default +**Make sure to give proper credit to myself (JoshuaMK) and this project when you utilize it in a mod! It's greatly appreciated.** diff --git a/build.py b/build.py new file mode 100644 index 0000000..6aa7bfd --- /dev/null +++ b/build.py @@ -0,0 +1,32 @@ +import geckoloader +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + longDescription = fh.read() + +with open("requirements.txt", "r", encoding="utf-8") as r: + requires = [x for x in r] + +setuptools.setup( + name='GeckoLoader', + version=geckoloader.__version__, + description='DOL Patcher for extending the codespace of Wii/GC games', + long_description=longDescription, + long_description_content_type="text/markdown", + url='https://github.com/JoshuaMKW/geckoloader', + author='JoshuaMK', + author_email='joshuamkw2002@gmail.com', + license='GNU General Public License v3.0', + packages=setuptools.find_packages(), + include_package_data=True, + install_requires=requires, + + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Operating System :: OS Independent' + ], + python_requires='>=3.8', +) \ No newline at end of file diff --git a/dolreader.py b/dolreader.py deleted file mode 100644 index 8249bee..0000000 --- a/dolreader.py +++ /dev/null @@ -1,397 +0,0 @@ -from io import BytesIO - -import tools -from fileutils import align_byte_size, read_uint32, write_uint32 - -class UnmappedAddressError(Exception): pass -class SectionCountFullError(Exception): pass -class AddressOutOfRangeError(Exception): pass - -class DolFile(object): - - class SectionType: - Text = 0 - Data = 1 - - maxTextSections = 7 - maxDataSections = 11 - offsetInfoLoc = 0 - addressInfoLoc = 0x48 - sizeInfoLoc = 0x90 - bssInfoLoc = 0xD8 - entryInfoLoc = 0xE0 - - def __init__(self, f=None): - - self.textSections = [] - self.dataSections = [] - - self.bssAddress = 0 - self.bssSize = 0 - self.entryPoint = 0x80003000 - - if f is None: return - - # Read text and data section addresses and sizes - for i in range(DolFile.maxTextSections + DolFile.maxDataSections): - f.seek(DolFile.offsetInfoLoc + (i << 2)) - offset = read_uint32(f) - f.seek(DolFile.addressInfoLoc + (i << 2)) - address = read_uint32(f) - f.seek(DolFile.sizeInfoLoc + (i << 2)) - size = read_uint32(f) - - if offset >= 0x100: - f.seek(offset) - data = BytesIO(f.read(size)) - if i < DolFile.maxTextSections: - self.textSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Text}) - else: - self.dataSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Data}) - - f.seek(DolFile.bssInfoLoc) - self.bssAddress = read_uint32(f) - self.bssSize = read_uint32(f) - - f.seek(DolFile.entryInfoLoc) - self.entryPoint = read_uint32(f) - - self._currLogicAddr = self.first_section["address"] - self.seek(self._currLogicAddr) - f.seek(0) - - def __repr__(self) -> str: - return f"repr={vars(self)}" - - def __str__(self) -> str: - return f"Nintendo DOL executable {self.__repr__()}" - - def resolve_address(self, gcAddr: int) -> tuple: - """ Returns the data of the section that houses the given address\n - UnmappedAddressError is raised when the address is unmapped """ - - for section in self.sections: - if section["address"] <= gcAddr < (section["address"] + section["size"]): - return section - - raise UnmappedAddressError(f"Unmapped address: 0x{gcAddr:X}") - - def seek_nearest_unmapped(self, gcAddr: int, buffer=0) -> int: - '''Returns the nearest unmapped address (greater) if the given address is already taken by data''' - - for section in self.sections: - if section["address"] > (gcAddr + buffer) or (section["address"] + section["size"]) < gcAddr: - continue - gcAddr = section["address"] + section["size"] - - try: - self.resolve_address(gcAddr) - except UnmappedAddressError: - break - return gcAddr - - @property - def sections(self) -> tuple: - """ Generator that yields each section's data """ - - for i in self.textSections: - yield i - for i in self.dataSections: - yield i - - @property - def last_section(self) -> tuple: - """ Returns the last section in the dol file as sorted by internal offset """ - - largestOffset = 0 - indexToTarget = 0 - targetType = DolFile.SectionType.Text - - for i, section in enumerate(self.textSections): - if section["offset"] > largestOffset: - largestOffset = section["offset"] - indexToTarget = i - targetType = DolFile.SectionType.Text - for i, section in enumerate(self.dataSections): - if section["offset"] > largestOffset: - largestOffset = section["offset"] - indexToTarget = i - targetType = DolFile.SectionType.Data - - if targetType == DolFile.SectionType.Text: - return self.textSections[indexToTarget] - else: - return self.dataSections[indexToTarget] - - @property - def first_section(self) -> tuple: - """ Returns the first section in the dol file as sorted by internal offset """ - - smallestOffset = 0xFFFFFFFF - indexToTarget = 0 - targetType = DolFile.SectionType.Text - - for i, section in enumerate(self.textSections): - if section["offset"] < smallestOffset: - smallestOffset = section["offset"] - indexToTarget = i - targetType = DolFile.SectionType.Text - for i, section in enumerate(self.dataSections): - if section["offset"] < smallestOffset: - smallestOffset = section["offset"] - indexToTarget = i - targetType = DolFile.SectionType.Data - - if targetType == DolFile.SectionType.Text: - return self.textSections[indexToTarget] - else: - return self.dataSections[indexToTarget] - - # Unsupported: Reading an entire dol file - # Assumption: A read should not go beyond the current section - def read(self, _size: int) -> bytes: - section = self.resolve_address(self._currLogicAddr) - if self._currLogicAddr + _size > (section["address"] + section["size"]): - raise UnmappedAddressError("Read goes over current section") - - self._currLogicAddr += _size - return section["data"].read(_size) - - # Assumption: A write should not go beyond the current section - def write(self, _data: bytes): - section = self.resolve_address(self._currLogicAddr) - if self._currLogicAddr + len(_data) > (section["address"] + section["size"]): - raise UnmappedAddressError("Write goes over current section") - - section["data"].write(_data) - self._currLogicAddr += len(_data) - - def seek(self, where: int, whence: int = 0): - if whence == 0: - section = self.resolve_address(where) - section["data"].seek(where - section["address"]) - - self._currLogicAddr = where - elif whence == 1: - section = self.resolve_address(self._currLogicAddr + where) - section["data"].seek((self._currLogicAddr + where) - section["address"]) - - self._currLogicAddr += where - else: - raise NotImplementedError(f"Unsupported whence type '{whence}'") - - def tell(self) -> int: - return self._currLogicAddr - - def save(self, f): - f.seek(0) - f.write(b"\x00" * self.size) - - for i, section in enumerate(self.sections): - if section["type"] == DolFile.SectionType.Data: - entry = i + (DolFile.maxTextSections - len(self.textSections)) - else: - entry = i - - f.seek(DolFile.offsetInfoLoc + (entry << 2)) - write_uint32(f, section["offset"]) #offset in file - f.seek(DolFile.addressInfoLoc + (entry << 2)) - write_uint32(f, section["address"]) #game address - f.seek(DolFile.sizeInfoLoc + (entry << 2)) - write_uint32(f, section["size"]) #size in file - - f.seek(section["offset"]) - f.write(section["data"].getbuffer()) - - f.seek(DolFile.bssInfoLoc) - write_uint32(f, self.bssAddress) - write_uint32(f, self.bssSize) - - f.seek(DolFile.entryInfoLoc) - write_uint32(f, self.entryPoint) - align_byte_size(f, 256) - - @property - def size(self) -> int: - try: - section = self.last_section - return (section["offset"] + section["size"] + 255) & -256 - except IndexError: - return 0x100 - - def get_section_size(self, index: int, section: SectionType) -> int: - """ Return the current size of the specified section\n - section: DolFile.SectionType """ - - if section == DolFile.SectionType.Text: - return self.textSections[index]["size"] - else: - return self.dataSections[index]["size"] - - - def append_text_sections(self, sectionsList: list): - """ Follows the list format: [tuple(Data, GameAddress or None), tuple(Data... """ - - for i, dataSet in enumerate(sectionsList): - if len(self.textSections) >= DolFile.maxTextSections: - raise SectionCountFullError(f"Exceeded max text section limit of {DolFile.maxTextSections}") - - finalSection = self.last_section - lastSection = self.textSections[len(self.textSections) - 1] - data, address = dataSet - - if not hasattr(data, "getbuffer"): - if hasattr(data, "read"): - data.seek(0) - data = BytesIO(data.read()) - else: - data = BytesIO(data) - - offset = finalSection["offset"] + finalSection["size"] - - if i < len(sectionsList) - 1: - size = (len(data.getbuffer()) + 31) & -32 - else: - size = (len(data.getbuffer()) + 255) & -256 - - if address is None: - address = self.seek_nearest_unmapped(lastSection["address"] + lastSection["size"], size) - - if address < 0x80000000 or address >= 0x81200000: - raise AddressOutOfRangeError(f"Address '{address:08X}' of text section {i} is beyond scope (0x80000000 <-> 0x81200000)") - - self.textSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Text}) - - def append_data_sections(self, sectionsList: list): - """ Follows the list format: [tuple(Data, GameAddress or None), tuple(Data... """ - - for i, dataSet in enumerate(sectionsList): - if len(self.dataSections) >= DolFile.maxDataSections: - raise SectionCountFullError(f"Exceeded max data section limit of {DolFile.maxDataSections}") - - finalSection = self.last_section - lastSection = self.dataSections[len(self.dataSections) - 1] - data, address = dataSet - - if not hasattr(data, "getbuffer"): - if hasattr(data, "read"): - data.seek(0) - data = BytesIO(data.read()) - else: - data = BytesIO(data) - - offset = finalSection["offset"] + finalSection["size"] - - if i < len(sectionsList) - 1: - size = (len(data.getbuffer()) + 31) & -32 - else: - size = (len(data.getbuffer()) + 255) & -256 - - if address is None: - address = self.seek_nearest_unmapped(lastSection["address"] + lastSection["size"], size) - - if address < 0x80000000 or address >= 0x81200000: - raise AddressOutOfRangeError(f"Address '{address:08X}' of data section {i} is beyond scope (0x80000000 <-> 0x81200000)") - - self.dataSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Data}) - - def insert_branch(self, to: int, _from: int, lk=0): - """ Insert a branch instruction at _from\n - to: address to branch to\n - _from: address to branch from\n - lk: 0 | 1, is branch linking? """ - - _from &= 0xFFFFFFFC - to &= 0xFFFFFFFC - self.seek(_from) - write_uint32(self, (to - _from) & 0x3FFFFFD | 0x48000000 | lk) - - def extract_branch_addr(self, bAddr: int) -> tuple: - """ Returns the destination of the given branch, - and if the branch is conditional """ - - self.seek(bAddr) - - ppc = read_uint32(self) - conditional = (ppc >> 24) & 0xFF < 0x48 - - if conditional is True: - if (ppc & 0x8000): - offset = (ppc & 0xFFFD) - 0x10000 - else: - offset = ppc & 0xFFFD - else: - if (ppc & 0x2000000): - offset = (ppc & 0x3FFFFFD) - 0x4000000 - else: - offset = ppc & 0x3FFFFFD - - return (bAddr + offset, conditional) - - def read_string(self, addr: int = None, maxlen: int = 0, encoding: str = "utf-8") -> str: - """ Reads a null terminated string from the specified address """ - - if addr != None: - self.seek(addr) - - length = 0 - string = "" - while (char := self.read(1)) != b"\x00": - try: - string += char.decode(encoding) - length += 1 - except UnicodeDecodeError: - print(f"{char} at pos {length}, (address 0x{addr + length:08X}) is not a valid utf-8 character") - return "" - if length > (maxlen-1) and maxlen != 0: - return string - - return string - - def print_info(self): - print("") - print("|-- DOL INFO --|".center(20, " ")) - print("") - - for i, (offset, addr, size, _, _) in enumerate(self.textSections): - header = f"| Text section {i} |" - info = [ "-"*len(header) + "\n" + header + "\n" + "-"*len(header), - "File Offset:".ljust(16, " ") + f"0x{offset:X}", - "Virtual addr:".ljust(16, " ") + f"0x{addr:X}", - "Size:".ljust(16, " ") + f"0x{size:X}" ] - - print("\n".join(info) + "\n") - - for i, (offset, addr, size, _, _) in enumerate(self.dataSections): - header = f"| Data section {i} |" - info = [ "-"*len(header) + "\n" + header + "\n" + "-"*len(header), - "File Offset:".ljust(16, " ") + f"0x{offset:X}", - "Virtual addr:".ljust(16, " ") + f"0x{addr:X}", - "Size:".ljust(16, " ") + f"0x{size:X}" ] - - print("\n".join(info) + "\n") - - header = "| BSS section |" - info = [ "-"*len(header) + "\n" + header + "\n" + "-"*len(header), - "Virtual addr:".ljust(16, " ") + f"0x{self.bssAddress:X}", - "Size:".ljust(16, " ") + f"0x{self.bssSize:X}", - "End:".ljust(16, " ") + f"0x{self.bssAddress+self.bssSize:X}" ] - - print("\n".join(info) + "\n") - - header = "| Miscellaneous Info |" - info = [ "-"*len(header) + "\n" + header + "\n" + "-"*len(header), - "Text sections:".ljust(16, " ") + f"0x{len(self.textSections):X}", - "Data sections:".ljust(16, " ") + f"0x{len(self.dataSections):X}", - "File length:".ljust(16, " ") + f"0x{self.size:X}" ] - - print("\n".join(info) + "\n") - -if __name__ == "__main__": - # Example usage (Reading global string "mario" from Super Mario Sunshine (NTSC-U)) - - with open("Start.dol", "rb") as f: - dol = DolFile(f) - - name = dol.read_string(addr=0x804165A0) - print(name) diff --git a/gct_d.txt b/gct_d.txt new file mode 100644 index 0000000..8f86924 --- /dev/null +++ b/gct_d.txt @@ -0,0 +1,174 @@ +[Gecko] +$On Airstrip From File Select [JoshuaMK] +02164e32 00000001 +*Set to Delfino Plaza +$Skip FMVs +042B5E8C 38600001 +042B5EF4 38600001 +* +$Light Adjuster [JoshuaMK] +C227C6AC 0000001A +7C0802A6 90010004 +9421FFC0 BF610008 +3C608041 8063E108 +83C304FC 4800000D +43000000 00000000 +7FE802A6 839E0018 +837E001C 73600008 +887F0004 4182000C +68600001 981F0004 +2C030000 41820070 +73800001 41820030 +73800400 41820018 +887D5E45 3463FFFF +4180001C 987D5E45 +48000014 C03F0000 +C05D5E0C EC420828 +D05D5E0C 73800002 +41820034 73800400 +4182001C 887D5E45 +280300FF 41820020 +38630001 987D5E45 +48000014 C03F0000 +C05D5E0C EC42082A +D05D5E0C BB610008 +38210040 80010004 +7C0803A6 00000000 +0427C054 60000000 +0427C05C 60000000 +0427C6D4 60000000 +04280154 60000000 +* +* +$Level Select +042A6788 3BC00009 +* +$Boot Area [JoshuaMK] +042A65E0 38600001 +042A65E4 38000005 +* +$LongJump [JoshuaMK] +0417A830 5400077B +04297A60 5400077B +C224954C 00000005 +3CC08041 80C6E108 +7C061800 38A0004D +40820014 89430400 +2C0A0000 41820008 +38A000F6 00000000 +C224C398 00000006 +3C808041 8084E108 +7C04F800 40820018 +809F007C 70840800 +4182000C 881F0400 +7C630378 2C030000 +60000000 00000000 +C224C3FC 00000005 +3C808041 8084E108 +7C04F800 40820014 +881F0400 7C000034 +5400D97E 7C630038 +2C030001 00000000 +C22543B8 00000017 +3FC08041 83DEE108 +7C1E1800 7C7E1B78 +408200A0 7C0802A6 +90010004 9421FFE0 +BFA10008 7C9D2378 +48000009 40A00000 +7FE802A6 806304FC +80630018 70600010 +41A2004C 807E007C +70600800 40820040 +2C1D0882 41820018 +3C1DFE00 2C000881 +4182000C 2C000880 +40820024 C03E00B0 +C01F0000 FF810040 +419C0014 3FA00200 +63BD0880 38600001 +48000008 38600000 +987E0400 7FA4EB78 +BBA10008 38210020 +80010004 7C0803A6 +7FC3F378 7C9F2378 +60000000 00000000 +C225453C 00000018 +7C0802A6 90010004 +9421FFC0 BFA10008 +D3E10018 D3C10020 +D3A10028 FFE00890 +FFC01090 FFA01890 +48000011 3EC00000 +40000000 42900000 +7FE802A6 3D408041 +814AE108 7C0AF000 +4082004C 895E0400 +2C0A0000 41820040 +C03E00B0 C01F0004 +EC210032 C01F0008 +FF810040 409D0008 +FC200090 7FC3F378 +3C008025 60005734 +7C0903A6 4E800421 +C3DE00B0 C01F0000 +EFFF0032 FC20F890 +FC40F090 C3E10018 +C3C10020 C3A10028 +BBA10008 38210040 +80010004 7C0803A6 +C002F0C4 00000000 +C22565D8 00000005 +3D608041 816BE108 +7C0BF800 40820010 +897F0400 556B3830 +7C005B78 54000631 +60000000 00000000 +*v4 +$70 shines stuff [people] +C2299614 00000023 +83ED9FA0 3BC00002 +7FE3FB78 3C800001 +608403AE 3D808029 +618C4A24 7D8903A6 +4E800421 2C030001 +41820034 3BC00009 +7FE3FB78 3C800004 +3D808029 618C4D4C +7D8903A6 4E800421 +3D808029 618C967C +7D8903A6 2C030046 +4D800420 7FE3FB78 +38800001 3CA00001 +60A50384 3D808029 +618C48E4 7D8903A6 +4E800421 7FE3FB78 +38800001 3CA00001 +60A50385 3D808029 +618C48E4 7D8903A6 +4E800421 7FE3FB78 +38800001 3CA00001 +60A50386 3D808029 +618C48E4 7D8903A6 +4E800421 7FE3FB78 +38800001 3CA00001 +60A50387 3D808029 +618C48E4 7D8903A6 +4E800421 7FE3FB78 +38800001 3CA00001 +60A5038F 3D808029 +618C48E4 7D8903A6 +4E800421 7FC3F378 +3D808029 618C9728 +7D8903A6 4E800420 +60000000 00000000 +* +* +* +* +* +[Gecko_Enabled] +$Level Select +[BreakPoints] +803433ac nb +8021dbc4 nb diff --git a/gct_o.txt b/gct_o.txt new file mode 100644 index 0000000..7f73ec6 --- /dev/null +++ b/gct_o.txt @@ -0,0 +1,122 @@ +Boot Stage +* 042A65E0 38600001 +* 042A65E4 38000005 + +On Delfino From Death [NTSC-U] +* 022997A6 00000001 +* 022997AE 00000005 +* 022997BE 00000001 +* 022997C6 00000005 + +No Light Circle +* 0427C6AC 48000560 + +Always Remove Boot Out +060049CC 00000014 +BF859999 9999999A +00000000 40800000 +40400000 00000000 +C22995BC 00000023 +9421FFE0 93A10014 +7C7D1B78 806300AC +93C10018 7C9E2378 +3C808029 9361000C +28030000 609B8BB0 +93410008 93810010 +418200BC 281E0004 +408200B4 889D0064 +2804000B 408200A8 +3C808040 6084D0A8 +83441060 809A007C +28041302 40820090 +80630118 886302E9 +28030001 40820014 +A07D004C 60630002 +B07D004C 48000070 +3C608041 389BFF4C +8383D0A8 3C640001 +4CC63182 7C6903A6 +4E800421 809A0080 +2C830000 386020D0 +7C632079 3C600C40 +4E823342 60630201 +41940008 48000008 +38640000 907A007C +3C60802B 6063B6F8 +7C6903A6 4E800421 +3C608003 6063293C +7C6903A6 7F83E378 +4E800421 7F6903A6 +7FA3EB78 7FC4F378 +4E800421 83C10018 +83A10014 83810010 +8361000C 83410008 +38210020 00000000 +C2004A64 0000002C +7C0802A6 90010004 +9421FFF0 3C808040 +6085E108 80850070 +3CC08000 C80649CC +3CC08000 8084025C +C04649D4 C0240024 +FC01002A FC001000 +4E811B82 4094003C +80A50000 80A5007C +28051302 40820078 +80A30008 90A40018 +80A30004 90A40014 +80630000 90640010 +80010014 38210010 +7C0803A6 4E800020 +3C603F80 90640024 +90640028 9064002C +906401A4 906401A8 +906401AC 38600000 +90640034 3C60801B +60630738 4CC63182 +7C6903A6 7C832378 +4E800421 80010014 +38210010 7C0803A6 +4E800020 3C608000 +C06349D8 3C608000 +C0240014 C0440028 +C90349CC C084002C +C0A401A4 FC000018 +C0C401A8 C0E401AC +3C608000 D0040024 +EC21182A C0040034 +C06349DC FC42402A +FC84402A D0240014 +FC201018 FCA5402A +D0240028 FC202018 +FCC6402A FCE7402A +D024002C FC202818 +EC00182A D02401A4 +FC203018 FC403818 +D02401A8 D04401AC +D0040034 80010014 +38210010 7C0803A6 +4E800020 00000000 +C21BD664 00000006 +9421FFF0 7C0802A6 +90010008 7FC4F378 +3C008000 60004A64 +7C0903A6 4E800421 +80010008 7C0803A6 +38210010 00000000 +041BD668 48000568 +062999DC 00000038 +41820030 4080000C +2C000005 40820028 +2C00000D 40800020 +806D9F28 8063007C +2C031302 4082000C +2C00000B 41820008 +637B0003 576007FF +C22999D8 00000002 +881A0064 2C00000A +60000000 00000000 +042413E0 60000000 +04297BD8 60000000 +04297BE8 60848200 +0429A598 4800000C \ No newline at end of file diff --git a/geckoloader/__init__.py b/geckoloader/__init__.py new file mode 100644 index 0000000..07b5c38 --- /dev/null +++ b/geckoloader/__init__.py @@ -0,0 +1,2 @@ +__version__ = "v7.1.0" +__author__ = "JoshuaMK" \ No newline at end of file diff --git a/geckoloader/__main__.py b/geckoloader/__main__.py new file mode 100644 index 0000000..d938de9 --- /dev/null +++ b/geckoloader/__main__.py @@ -0,0 +1,51 @@ +# Written by JoshuaMK 2020 + +from argparse import Namespace +import atexit +from enum import Enum +import logging +import os +import pickle as cPickle +import re +import shutil +import signal +import sys +import tempfile +from contextlib import redirect_stderr, redirect_stdout +from distutils.version import LooseVersion +from io import StringIO +from pathlib import Path +from typing import Any, Dict + +from PyQt5 import QtCore, QtGui, QtWidgets +from geckoloader.gui import GeckoLoaderGUI + +from pyfile.children_ui import PrefWindow, SettingsWindow +from dolreader.dol import DolFile +from fileutils import get_program_folder, resource_path +from kernel import CodeHandler, KernelLoader +from main_ui import MainWindow +from tools import CommandLineParser, color_text +from versioncheck import Updater +from geckoloader import __version__ +from geckoloader.cli import GeckoLoaderCli + + + + +if __name__ == "__main__": + cli = GeckoLoaderCli('GeckoLoader', __version__, + description='Dol editing tool for allocating extended codespace') + + if len(sys.argv) == 1: + cli.print_splash() + app = GeckoLoaderGUI(cli) + signal.signal(signal.SIGINT, signal.SIG_DFL) + app.run() + elif '--checkupdate' in sys.argv: + cli.check_updates() + elif '--splash' in sys.argv: + cli.print_splash() + else: + args = cli.parse_args() + cli._exec(args, TMPDIR) diff --git a/children_ui.py b/geckoloader/children_ui.py similarity index 54% rename from children_ui.py rename to geckoloader/children_ui.py index fae7d00..224b849 100644 --- a/children_ui.py +++ b/geckoloader/children_ui.py @@ -1,13 +1,18 @@ import sys -from PyQt5 import QtCore, QtGui, QtWidgets +from PySide6.QtCore import QMetaObject, QSize, Qt +from PySide6.QtGui import QFont, QIcon, QPixmap +from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDialog, + QDialogButtonBox, QGridLayout, QLabel, + QLineEdit, QSizePolicy) -from fileutils import resource_path +from geckoloader.fileutils import resource_path -class PrefWindow(QtWidgets.QDialog): +class PrefWindow(QDialog): def __init__(self): - super().__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) + super().__init__(None, Qt.WindowSystemMenuHint | + Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setup_ui() @@ -15,36 +20,38 @@ def setup_ui(self): self.setObjectName("Dialog") self.setFixedSize(300, 120) self.setModal(True) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(str(resource_path("bin/icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon = QIcon() + icon.addPixmap(QPixmap( + str(resource_path("bin/icon.ico"))), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) - #Buttonbox - self.buttonBox = QtWidgets.QDialogButtonBox(self) + # Buttonbox + self.buttonBox = QDialogButtonBox(self) self.buttonBox.setFixedSize(280, 30) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons( + QDialogButtonBox.Cancel | QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") - self.formLayoutWidget = QtWidgets.QGridLayout(self) + self.formLayoutWidget = QGridLayout(self) self.formLayoutWidget.setObjectName("formLayoutWidget") - self.comboBoxLayout = QtWidgets.QGridLayout() + self.comboBoxLayout = QGridLayout() self.comboBoxLayout.setVerticalSpacing(10) self.comboBoxLayout.setObjectName("comboBoxLayout") - #qtstyle box - self.qtstyleSelect = QtWidgets.QComboBox() + # qtstyle box + self.qtstyleSelect = QComboBox() self.qtstyleSelect.setObjectName("qtstyleSelect") self.comboBoxLayout.addWidget(self.qtstyleSelect, 1, 1, 1, 1) - #qtstyle label - self.qtstyleLabel = QtWidgets.QLabel() + # qtstyle label + self.qtstyleLabel = QLabel() self.qtstyleLabel.setObjectName("qtstyleLabel") self.comboBoxLayout.addWidget(self.qtstyleLabel, 1, 0, 1, 1) - #qtdark theme - self.qtdarkButton = QtWidgets.QCheckBox() + # qtdark theme + self.qtdarkButton = QCheckBox() self.qtdarkButton.setObjectName("formalnaming") self.qtdarkButton.setText("Dark Theme") self.comboBoxLayout.addWidget(self.qtdarkButton, 2, 0, 1, 1) @@ -56,128 +63,137 @@ def setup_ui(self): self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - QtCore.QMetaObject.connectSlotsByName(self) + QMetaObject.connectSlotsByName(self) def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("Dialog", "Preferences", None)) - self.qtstyleLabel.setText(QtWidgets.QApplication.translate("Dialog", "GUI Style:", None)) + self.setWindowTitle(QApplication.translate( + "Dialog", "Preferences", None)) + self.qtstyleLabel.setText( + QApplication.translate("Dialog", "GUI Style:", None)) -class SettingsWindow(QtWidgets.QDialog): + +class SettingsWindow(QDialog): def __init__(self): - super().__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) + super().__init__(None, Qt.WindowSystemMenuHint | + Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setup_ui() def setup_ui(self): self.setObjectName("Dialog") - + if sys.platform == "win32": self.setFixedSize(300, 240) else: self.setFixedSize(370, 240) - + self.setModal(True) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(str(resource_path("bin/icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon = QIcon() + icon.addPixmap(QPixmap( + str(resource_path("bin/icon.ico"))), QIcon.Normal, QIcon.Off) self.setWindowIcon(icon) - #Buttonbox - self.buttonBox = QtWidgets.QDialogButtonBox(self) + # Buttonbox + self.buttonBox = QDialogButtonBox(self) if sys.platform == "win32": self.buttonBox.setFixedSize(280, 30) else: self.buttonBox.setFixedSize(350, 30) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") - self.formLayoutWidget = QtWidgets.QGridLayout(self) + self.formLayoutWidget = QGridLayout(self) self.formLayoutWidget.setObjectName("formLayoutWidget") - self.comboBoxLayout = QtWidgets.QGridLayout() + self.comboBoxLayout = QGridLayout() self.comboBoxLayout.setVerticalSpacing(10) self.comboBoxLayout.setObjectName("comboBoxLayout") - #protect codes - self.protectCodes = QtWidgets.QCheckBox() + # protect codes + self.protectCodes = QCheckBox() self.protectCodes.setObjectName("protectCodes") self.protectCodes.setText("Protect Game (Prevent user codes)") self.comboBoxLayout.addWidget(self.protectCodes, 1, 0, 1, 1) - #encrypt codes - self.encryptCodes = QtWidgets.QCheckBox() + # encrypt codes + self.encryptCodes = QCheckBox() self.encryptCodes.setObjectName("encryptCodes") self.encryptCodes.setText("Encrypt codes") self.comboBoxLayout.addWidget(self.encryptCodes, 2, 0, 1, 1) - #optimize codes - self.optimizeCodes = QtWidgets.QCheckBox() + # optimize codes + self.optimizeCodes = QCheckBox() self.optimizeCodes.setObjectName("optimizeCodes") self.optimizeCodes.setText("Optimize codes") self.optimizeCodes.setChecked(True) self.comboBoxLayout.addWidget(self.optimizeCodes, 3, 0, 1, 1) - #Codehook Address Label - self.codehookLabel = QtWidgets.QLabel() + # Codehook Address Label + self.codehookLabel = QLabel() self.codehookLabel.setObjectName("codehookLabel") self.comboBoxLayout.addWidget(self.codehookLabel, 4, 0, 1, 1) - #Codehook Address Textbox - self.codehookLineEdit = QtWidgets.QLineEdit() + # Codehook Address Textbox + self.codehookLineEdit = QLineEdit() self.codehookLineEdit.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.codehookLineEdit.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.codehookLineEdit.sizePolicy().hasHeightForWidth()) self.codehookLineEdit.setSizePolicy(sizePolicy) - self.codehookLineEdit.setMinimumSize(QtCore.QSize(79, 23)) - self.codehookLineEdit.setMaximumSize(QtCore.QSize(79, 23)) - font = QtGui.QFont() + self.codehookLineEdit.setMinimumSize(QSize(79, 23)) + self.codehookLineEdit.setMaximumSize(QSize(79, 23)) + font = QFont() font.setFamily("Consolas") font.setPointSize(12) font.setWeight(42) self.codehookLineEdit.setFont(font) self.codehookLineEdit.setText("") self.codehookLineEdit.setMaxLength(8) - self.codehookLineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) + self.codehookLineEdit.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) self.codehookLineEdit.setObjectName("codehookLineEdit") self.comboBoxLayout.addWidget(self.codehookLineEdit, 4, 1, 1, 1) - #kernelHook Address Label - self.kernelHookLabel = QtWidgets.QLabel() + # kernelHook Address Label + self.kernelHookLabel = QLabel() self.kernelHookLabel.setObjectName("kernelHookLabel") self.comboBoxLayout.addWidget(self.kernelHookLabel, 5, 0, 1, 1) - #kernelHook Address Textbox - self.kernelHookLineEdit = QtWidgets.QLineEdit() + # kernelHook Address Textbox + self.kernelHookLineEdit = QLineEdit() self.kernelHookLineEdit.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.kernelHookLineEdit.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.kernelHookLineEdit.sizePolicy().hasHeightForWidth()) self.kernelHookLineEdit.setSizePolicy(sizePolicy) - self.kernelHookLineEdit.setMinimumSize(QtCore.QSize(79, 23)) - self.kernelHookLineEdit.setMaximumSize(QtCore.QSize(79, 23)) - font = QtGui.QFont() + self.kernelHookLineEdit.setMinimumSize(QSize(79, 23)) + self.kernelHookLineEdit.setMaximumSize(QSize(79, 23)) + font = QFont() font.setFamily("Consolas") font.setPointSize(12) font.setWeight(42) self.kernelHookLineEdit.setFont(font) self.kernelHookLineEdit.setText("") self.kernelHookLineEdit.setMaxLength(8) - self.kernelHookLineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) + self.kernelHookLineEdit.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) self.kernelHookLineEdit.setObjectName("kernelHookLineEdit") self.comboBoxLayout.addWidget(self.kernelHookLineEdit, 5, 1, 1, 1) - #verbosity label - self.verbosityLabel = QtWidgets.QLabel() + # verbosity label + self.verbosityLabel = QLabel() self.verbosityLabel.setObjectName("verbosityLabel") self.comboBoxLayout.addWidget(self.verbosityLabel, 6, 0, 1, 1) - #verbosity box - self.verbositySelect = QtWidgets.QComboBox() + # verbosity box + self.verbositySelect = QComboBox() self.verbositySelect.addItems(["1", "2", "3", "0"]) self.verbositySelect.setObjectName("verbositySelect") self.comboBoxLayout.addWidget(self.verbositySelect, 6, 1, 1, 1) @@ -189,7 +205,7 @@ def setup_ui(self): self.retranslateUi() self.buttonBox.accepted.connect(self.accept) - QtCore.QMetaObject.connectSlotsByName(self) + QMetaObject.connectSlotsByName(self) def set_edit_fields(self): self.protectCodes.setEnabled(True) @@ -200,7 +216,11 @@ def set_edit_fields(self): self.verbositySelect.setEnabled(True) def retranslateUi(self): - self.setWindowTitle(QtWidgets.QApplication.translate("Dialog", "Advanced Settings", None)) - self.codehookLabel.setText(QtWidgets.QApplication.translate("Dialog", "Codehook Address:", None)) - self.kernelHookLabel.setText(QtWidgets.QApplication.translate("Dialog", "Kernel Init Address:", None)) - self.verbosityLabel.setText(QtWidgets.QApplication.translate("Dialog", "Verbosity:", None)) + self.setWindowTitle(QApplication.translate( + "Dialog", "Advanced Settings", None)) + self.codehookLabel.setText(QApplication.translate( + "Dialog", "Codehook Address:", None)) + self.kernelHookLabel.setText(QApplication.translate( + "Dialog", "Kernel Init Address:", None)) + self.verbosityLabel.setText( + QApplication.translate("Dialog", "Verbosity:", None)) diff --git a/geckoloader/cli.py b/geckoloader/cli.py new file mode 100644 index 0000000..54e2a7e --- /dev/null +++ b/geckoloader/cli.py @@ -0,0 +1,280 @@ +# Written by JoshuaMK 2020 + +from argparse import Namespace +import atexit +import shutil +import tempfile +from distutils.version import LooseVersion +from pathlib import Path +from typing import Any, Dict +from dolreader.dol import DolFile +from fileutils import resource_path +from kernel import CodeHandler, KernelLoader +from tools import CommandLineParser, color_text +from versioncheck import Updater + +from geckoloader import __version__ + +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 = '' + +TMPDIR = Path(tempfile.mkdtemp(prefix="GeckoLoader-")) + +@atexit.register +def clean_tmp_resources(): + tmpfolder = TMPDIR.parent + for entry in tmpfolder.iterdir(): + if entry.name.startswith("GeckoLoader-"): + shutil.rmtree(entry, ignore_errors=True) + + +class GeckoLoaderCli(CommandLineParser): + def __init__(self, name, version: str = None, description: str = ''): + super().__init__(prog=(f"{name} {version}"), + description=description, allow_abbrev=False) + self.__version__ = version + self.__doc__ = description + + self.add_argument('dolfile', help='DOL file') + self.add_argument('codelist', help='Folder or Gecko GCT|TXT file') + self.add_argument('-a', '--alloc', + help='Define the size of the code allocation in hex, only applies when using the ARENA space', + metavar='SIZE') + self.add_argument('-i', '--init', + help='Define where GeckoLoader is initialized in hex', + metavar='ADDRESS') + self.add_argument('-tc', '--txtcodes', + help='''["ACTIVE", "ALL"] What codes get parsed when a txt file is used. + "ALL" makes all codes get parsed, + "ACTIVE" makes only activated codes get parsed. + "ACTIVE" is the default''', + default='ACTIVE', + metavar='TYPE') + self.add_argument('--handler', + help='''["MINI", "FULL"] 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. "FULL" is the default''', + default='FULL', + choices=['MINI', 'FULL'], + metavar='TYPE') + self.add_argument('--hooktype', + help='''["VI", "GX", "PAD"] The type of hook used for the RAM search. "VI" or "GX" are recommended, + although "PAD" can work just as well. "VI" is the default''', + default='VI', + choices=['VI', 'GX', 'PAD'], + metavar='HOOK') + self.add_argument('--hookaddress', + help='Choose where the codehandler hooks to in hex, overrides auto hooks', + metavar='ADDRESS') + self.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') + self.add_argument('-p', '--protect', + help='''Targets and nullifies the standard codehandler provided by loaders and Dolphin Emulator, + only applies when the ARENA is used''', + action='store_true') + self.add_argument('--dest', + help='Target path to put the modified DOL, can be a folder or file', + metavar='PATH') + self.add_argument('--checkupdate', + help='''Checks to see if a new update exists on the GitHub Repository releases page, + this option overrides all other commands.''', + action='store_true') + self.add_argument('--splash', + help='''Print the splash screen, this option overrides + all other commands excluding --checkupdate''', + action='store_true') + self.add_argument('--encrypt', + help='Encrypts the codelist on compile time, helping to slow the snoopers', + action='store_true') + self.add_argument('-q', '--quiet', + help='Print nothing to the console', + action='store_true') + self.add_argument('-v', '--verbose', + help='Print extra info to the console', + default=0, + action='count') + + def __str__(self) -> str: + return self.__doc__ + + def print_splash(self): + helpMessage = 'Try option -h for more info on this program'.center( + 64, ' ') + version = self.__version__.rjust(9, ' ') + + logo = [' ', + ' ╔═══════════════════════════════════════════════════════════╗ ', + ' ║ ║ ', + ' ║ ┌───┐┌───┐┌───┐┌┐┌─┐┌───┐┌┐ ┌───┐┌───┐┌───┐┌───┐┌───┐ ║ ', + ' ║ │┌─┐││┌──┘│┌─┐││││┌┘│┌─┐│││ │┌─┐││┌─┐│└┐┌┐││┌──┘│┌─┐│ ║ ', + ' ║ ││ └┘│└──┐││ └┘│└┘┘ ││ ││││ ││ ││││ ││ │││││└──┐│└─┘│ ║ ', + ' ║ ││┌─┐│┌──┘││ ┌┐│┌┐│ ││ ││││ ┌┐││ │││└─┘│ │││││┌──┘│┌┐┌┘ ║ ', + ' ║ │└┴─││└──┐│└─┘││││└┐│└─┘││└─┘││└─┘││┌─┐│┌┘└┘││└──┐│││└┐ ║ ', + ' ║ └───┘└───┘└───┘└┘└─┘└───┘└───┘└───┘└┘ └┘└───┘└───┘└┘└─┘ ║ ', + ' ║ ║ ', + ' ║ ┌┐┌───┐┌───┐┌┐ ┌┐┌┐ ┌┐┌───┐┌─┐┌─┐┌┐┌─┐ ║ ', + ' ║ │││┌─┐││┌─┐│││ ││││ │││┌─┐││ └┘ ││││┌┘ ║ ', + ' ║ ││││ │││└──┐│└─┘│││ ││││ │││┌┐┌┐││└┘┘ ║ ', + ' ║ ┌──┐┌┐││││ ││└──┐││┌─┐│││ │││└─┘││││││││┌┐│ ┌──┐ ║ ', + ' ║ └──┘│└┘││└─┘││└─┘│││ │││└─┘││┌─┐││││││││││└┐└──┘ ║ ', + ' ║ └──┘└───┘└───┘└┘ └┘└───┘└┘ └┘└┘└┘└┘└┘└─┘ ║ ', + f' ║ {version} ║ ', + ' ╚═══════════════════════════════════════════════════════════╝ ', + ' ', + ' GeckoLoader is a cli tool for allowing extended ', + ' gecko code space in all Wii and GC games. ', + ' ', + f'{helpMessage}', + ' '] + + for line in logo: + print(color_text( + line, textToColor={'║': TREDLIT, '╔': TRED, '╚': TRED, '╝': TRED, '╗': TRED, '═': TRED}, defaultColor=TGREENLIT)) + + def check_updates(self): + repoChecker = Updater('JoshuaMKW', 'GeckoLoader') + + tag, status = repoChecker.get_newest_version() + + if status is False: + self.error(color_text(tag + '\n', defaultColor=TREDLIT), + print_usage=False) + + print('') + + if LooseVersion(tag) > LooseVersion(self.__version__): + print(color_text( + f' :: A new update is live at {repoChecker.gitReleases.format(repoChecker.owner, repoChecker.repo)}', defaultColor=TYELLOWLIT)) + print(color_text( + f' :: Current version is "{self.__version__}", Most recent version is "{tag}"', defaultColor=TYELLOWLIT)) + elif LooseVersion(tag) < LooseVersion(self.__version__): + print(color_text(' :: No update available', defaultColor=TGREENLIT)) + print(color_text( + f' :: Current version is "{self.__version__}(dev)", Most recent version is "{tag}(release)"', defaultColor=TGREENLIT)) + else: + print(color_text(' :: No update available', defaultColor=TGREENLIT)) + print(color_text( + f' :: Current version is "{self.__version__}(release)", Most recent version is "{tag}(release)"', defaultColor=TGREENLIT)) + + print('') + + def _validate_args(self, args: Namespace) -> Dict[str, Any]: + dolFile = Path(args.dolfile).resolve() + codeList = Path(args.codelist).resolve() + + if args.dest: + dest = Path(args.dest).resolve() + if dest.suffix == "": + dest = dest / dolFile.name + else: + dest = Path.cwd() / "geckoloader-build" / dolFile.name + + if args.alloc: + try: + _allocation = int(args.alloc, 16) + except ValueError: + self.error(color_text( + 'The allocation was invalid\n', defaultColor=TREDLIT)) + else: + _allocation = None + + if args.hookaddress: + try: + _codehook = int(args.hookaddress, 16) + except ValueError: + self.error(color_text( + 'The codehandler hook address was invalid\n', defaultColor=TREDLIT)) + if 0x80000000 > _codehook >= 0x81800000: + self.error(color_text( + 'The codehandler hook address was beyond bounds\n', defaultColor=TREDLIT)) + else: + _codehook = None + + if args.handler == KernelLoader.HandlerType.MINI: + codeHandlerFile = Path('bin/codehandler-mini.bin') + elif args.handler == KernelLoader.HandlerType.FULL: + codeHandlerFile = Path('bin/codehandler.bin') + else: + self.error(color_text( + f'Codehandler type {args.handler} is invalid\n', defaultColor=TREDLIT)) + + if not dolFile.is_file(): + self.error(color_text( + f'File "{dolFile}" does not exist\n', defaultColor=TREDLIT)) + + if not codeList.exists(): + self.error(color_text( + f'File/folder "{codeList}" does not exist\n', defaultColor=TREDLIT)) + + return {"dol": dolFile, + "codepath": codeList, + "codehandler": codeHandlerFile, + "destination": dest, + "allocation": _allocation, + "hookaddress": _codehook, + "hooktype": args.hooktype, + "initaddress": None if args.init is None else int(args.init, 16), + "includeall": args.txtcodes.lower() == "all", + "optimize": args.optimize, + "protect": args.protect, + "encrypt": args.encrypt, + "verbosity": args.verbose, + "quiet": args.quiet} + + def _exec(self, args): + context = self._validate_args(args) + + try: + with context["dol"].open("rb") as dol: + dolFile = DolFile(dol) + + with resource_path(context["codehandler"]).open("rb") as handler: + codeHandler = CodeHandler(handler) + + with resource_path("bin/geckoloader.bin").open("rb") as kernelfile: + geckoKernel = KernelLoader(kernelfile, + hookType=context["hooktype"], + hookAddress=context["hookaddress"], + initAddress=context["initaddress"], + allocation=context["allocation"], + includeAllCodes=context["includeall"], + optimizeCodes=context["optimize"], + protectCodes=context["protect"], + encryptCodes=context["encrypt"], + cli=self) + geckoKernel.verbosity = context["verbosity"] + if context["quiet"] == True: + geckoKernel.silence() + else: + geckoKernel.desilence() + + if not context["destination"].parent.exists(): + context["destination"].parent.mkdir( + parents=True, exist_ok=True) + + geckoKernel.build(context["codepath"], dolFile, + codeHandler, TMPDIR, context["destination"]) + + except FileNotFoundError as e: + self.error(color_text(e, defaultColor=TREDLIT)) diff --git a/fileutils.py b/geckoloader/fileutils.py similarity index 76% rename from fileutils.py rename to geckoloader/fileutils.py index ddd29fc..b495354 100644 --- a/fileutils.py +++ b/geckoloader/fileutils.py @@ -1,23 +1,28 @@ import struct -import sys -from os import getenv from pathlib import Path -from tools import align_byte_size, get_alignment - -def resource_path(relative_path: str = "") -> Path: +def resource_path(relPath: str = "") -> Path: """ Get absolute path to resource, works for dev and for cx_freeze """ - if getattr(sys, "frozen", False): - # The application is frozen - base_path = Path(sys.executable).parent + import sys + + if hasattr(sys, "_MEIPASS"): + return getattr(sys, "_MEIPASS", Path(__file__).parent) / relPath else: - base_path = Path(__file__).parent - - return base_path / relative_path + if getattr(sys, "frozen", False): + # The application is frozen + base_path = Path(sys.executable).parent + else: + base_path = Path(__file__).parent + + return base_path / relPath + def get_program_folder(folder: str = "") -> Path: """ Get path to appdata """ + from os import getenv + import sys + if sys.platform == "win32": datapath = Path(getenv("APPDATA")) / folder elif sys.platform == "darwin": @@ -32,56 +37,75 @@ def get_program_folder(folder: str = "") -> Path: raise NotImplementedError(f"{sys.platform} OS is unsupported") return datapath + def read_sbyte(f): return struct.unpack("b", f.read(1))[0] + def write_sbyte(f, val): f.write(struct.pack("b", val)) + def read_sint16(f): return struct.unpack(">h", f.read(2))[0] + def write_sint16(f, val): f.write(struct.pack(">h", val)) + def read_sint32(f): return struct.unpack(">i", f.read(4))[0] + def write_sint32(f, val): f.write(struct.pack(">i", val)) + def read_ubyte(f): return struct.unpack("B", f.read(1))[0] + def write_ubyte(f, val): f.write(struct.pack("B", val)) + def read_uint16(f): return struct.unpack(">H", f.read(2))[0] + def write_uint16(f, val): f.write(struct.pack(">H", val)) + def read_uint32(f): return struct.unpack(">I", f.read(4))[0] + def write_uint32(f, val): f.write(struct.pack(">I", val)) + def read_float(f): return struct.unpack(">f", f.read(4))[0] + def write_float(f, val): f.write(struct.pack(">f", val)) + def read_double(f): return struct.unpack(">d", f.read(4))[0] + def write_double(f, val): f.write(struct.pack(">d", val)) + def read_bool(f, vSize=1): return struct.unpack("B", f.read(vSize))[0] > 0 + def write_bool(f, val, vSize=1): - f.write(b'\x00'*(vSize-1) + b'\x01') if val is True else f.write(b'\x00' * vSize) + f.write(b'\x00'*(vSize-1) + + b'\x01') if val is True else f.write(b'\x00' * vSize) diff --git a/GeckoLoader.py b/geckoloader/gui.py similarity index 54% rename from GeckoLoader.py rename to geckoloader/gui.py index 891d745..ff682e4 100644 --- a/GeckoLoader.py +++ b/geckoloader/gui.py @@ -1,298 +1,31 @@ -# Written by JoshuaMK 2020 -import atexit import logging import os import pickle as cPickle import re -import shutil -import signal import sys -import tempfile from contextlib import redirect_stderr, redirect_stdout -from distutils.version import LooseVersion +from enum import Enum, IntEnum from io import StringIO from pathlib import Path +from typing import Tuple -from PyQt5 import QtCore, QtGui, QtWidgets - -from children_ui import PrefWindow, SettingsWindow -from dolreader import DolFile -from fileutils import get_program_folder, resource_path -from kernel import CodeHandler, KernelLoader -from main_ui import MainWindow -from tools import CommandLineParser, color_text -from versioncheck import Updater - -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 = '' - -__version__ = "v7.1.0" - -TMPDIR = Path(tempfile.mkdtemp(prefix="GeckoLoader-")) - - -@atexit.register -def clean_tmp_resources(): - tmpfolder = TMPDIR.parent - for entry in tmpfolder.iterdir(): - if entry.name.startswith("GeckoLoader-"): - shutil.rmtree(entry, ignore_errors=True) - - -class GeckoLoaderCli(CommandLineParser): - - def __init__(self, name, version=None, description=''): - super().__init__(prog=(f"{name} {version}"), - description=description, allow_abbrev=False) - self.__version__ = version - self.__doc__ = description - - self.add_argument('dolfile', help='DOL file') - self.add_argument('codelist', help='Folder or Gecko GCT|TXT file') - self.add_argument('-a', '--alloc', - help='Define the size of the code allocation in hex, only applies when using the ARENA space', - metavar='SIZE') - self.add_argument('-i', '--init', - help='Define where GeckoLoader is initialized in hex', - metavar='ADDRESS') - self.add_argument('-tc', '--txtcodes', - help='''["ACTIVE", "ALL"] What codes get parsed when a txt file is used. - "ALL" makes all codes get parsed, - "ACTIVE" makes only activated codes get parsed. - "ACTIVE" is the default''', - default='ACTIVE', - metavar='TYPE') - self.add_argument('--handler', - help='''["MINI", "FULL"] 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. "FULL" is the default''', - default='FULL', - choices=['MINI', 'FULL'], - metavar='TYPE') - self.add_argument('--hooktype', - help='''["VI", "GX", "PAD"] The type of hook used for the RAM search. "VI" or "GX" are recommended, - although "PAD" can work just as well. "VI" is the default''', - default='VI', - choices=['VI', 'GX', 'PAD'], - metavar='HOOK') - self.add_argument('--hookaddress', - help='Choose where the codehandler hooks to in hex, overrides auto hooks', - metavar='ADDRESS') - self.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') - self.add_argument('-p', '--protect', - help='''Targets and nullifies the standard codehandler provided by loaders and Dolphin Emulator, - only applies when the ARENA is used''', - action='store_true') - self.add_argument('--dest', - help='Target path to put the modified DOL, can be a folder or file', - metavar='PATH') - self.add_argument('--checkupdate', - help='''Checks to see if a new update exists on the GitHub Repository releases page, - this option overrides all other commands.''', - action='store_true') - self.add_argument('--splash', - help='''Print the splash screen, this option overrides - all other commands excluding --checkupdate''', - action='store_true') - self.add_argument('--encrypt', - help='Encrypts the codelist on compile time, helping to slow the snoopers', - action='store_true') - self.add_argument('-q', '--quiet', - help='Print nothing to the console', - action='store_true') - self.add_argument('-v', '--verbose', - help='Print extra info to the console', - default=0, - action='count') - - def __str__(self) -> str: - return self.__doc__ - - def print_splash(self): - helpMessage = 'Try option -h for more info on this program'.center( - 64, ' ') - version = self.__version__.rjust(9, ' ') - - logo = [' ', - ' ╔═══════════════════════════════════════════════════════════╗ ', - ' ║ ║ ', - ' ║ ┌───┐┌───┐┌───┐┌┐┌─┐┌───┐┌┐ ┌───┐┌───┐┌───┐┌───┐┌───┐ ║ ', - ' ║ │┌─┐││┌──┘│┌─┐││││┌┘│┌─┐│││ │┌─┐││┌─┐│└┐┌┐││┌──┘│┌─┐│ ║ ', - ' ║ ││ └┘│└──┐││ └┘│└┘┘ ││ ││││ ││ ││││ ││ │││││└──┐│└─┘│ ║ ', - ' ║ ││┌─┐│┌──┘││ ┌┐│┌┐│ ││ ││││ ┌┐││ │││└─┘│ │││││┌──┘│┌┐┌┘ ║ ', - ' ║ │└┴─││└──┐│└─┘││││└┐│└─┘││└─┘││└─┘││┌─┐│┌┘└┘││└──┐│││└┐ ║ ', - ' ║ └───┘└───┘└───┘└┘└─┘└───┘└───┘└───┘└┘ └┘└───┘└───┘└┘└─┘ ║ ', - ' ║ ║ ', - ' ║ ┌┐┌───┐┌───┐┌┐ ┌┐┌┐ ┌┐┌───┐┌─┐┌─┐┌┐┌─┐ ║ ', - ' ║ │││┌─┐││┌─┐│││ ││││ │││┌─┐││ └┘ ││││┌┘ ║ ', - ' ║ ││││ │││└──┐│└─┘│││ ││││ │││┌┐┌┐││└┘┘ ║ ', - ' ║ ┌──┐┌┐││││ ││└──┐││┌─┐│││ │││└─┘││││││││┌┐│ ┌──┐ ║ ', - ' ║ └──┘│└┘││└─┘││└─┘│││ │││└─┘││┌─┐││││││││││└┐└──┘ ║ ', - ' ║ └──┘└───┘└───┘└┘ └┘└───┘└┘ └┘└┘└┘└┘└┘└─┘ ║ ', - f' ║ {version} ║ ', - ' ╚═══════════════════════════════════════════════════════════╝ ', - ' ', - ' GeckoLoader is a cli tool for allowing extended ', - ' gecko code space in all Wii and GC games. ', - ' ', - f'{helpMessage}', - ' '] - - for line in logo: - print(color_text( - line, [('║', TREDLIT), ('╔╚╝╗═', TRED)], TGREENLIT)) - - def check_updates(self): - repoChecker = Updater('JoshuaMKW', 'GeckoLoader') - - tag, status = repoChecker.get_newest_version() - - if status is False: - self.error(color_text(tag + '\n', defaultColor=TREDLIT), - print_usage=False) - - print('') - - if LooseVersion(tag) > LooseVersion(self.__version__): - print(color_text( - f' :: A new update is live at {repoChecker.gitReleases.format(repoChecker.owner, repoChecker.repo)}', defaultColor=TYELLOWLIT)) - print(color_text( - f' :: Current version is "{self.__version__}", Most recent version is "{tag}"', defaultColor=TYELLOWLIT)) - elif LooseVersion(tag) < LooseVersion(self.__version__): - print(color_text(' :: No update available', defaultColor=TGREENLIT)) - print(color_text( - f' :: Current version is "{self.__version__}(dev)", Most recent version is "{tag}(release)"', defaultColor=TGREENLIT)) - else: - print(color_text(' :: No update available', defaultColor=TGREENLIT)) - print(color_text( - f' :: Current version is "{self.__version__}(release)", Most recent version is "{tag}(release)"', defaultColor=TGREENLIT)) - - print('') - - def _validate_args(self, args) -> dict: - dolFile = Path(args.dolfile).resolve() - codeList = Path(args.codelist).resolve() - - if args.dest: - dest = Path(args.dest).resolve() - if dest.suffix == "": - dest = dest / args.dolfile.name - else: - dest = Path.cwd() / "geckoloader-build" / args.dolfile.name - - if args.alloc: - try: - _allocation = int(args.alloc, 16) - except ValueError: - self.error(color_text( - 'The allocation was invalid\n', defaultColor=TREDLIT)) - else: - _allocation = None +from PySide6.QtCore import QRegularExpression, Qt +from PySide6.QtGui import QIcon, QPixmap, QRegularExpressionValidator +from PySide6.QtWidgets import (QApplication, QErrorMessage, QFileDialog, + QMessageBox, QStyleFactory, QTextEdit) - if args.hookaddress: - if 0x80000000 > int(args.hookaddress, 16) >= 0x81800000: - self.error(color_text( - 'The codehandler hook address was beyond bounds\n', defaultColor=TREDLIT)) - else: - try: - _codehook = int(args.hookaddress, 16) - except ValueError: - self.error(color_text( - 'The codehandler hook address was invalid\n', defaultColor=TREDLIT)) - else: - _codehook = None +from geckoloader import __version__ +from geckoloader.children_ui import PrefWindow, SettingsWindow +from geckoloader.cli import GeckoLoaderCli +from geckoloader.fileutils import get_program_folder, resource_path +from geckoloader.gui import GeckoLoaderGUI +from geckoloader.main_ui import MainWindow - if args.handler == CodeHandler.Types.MINI: - codeHandlerFile = Path('bin/codehandler-mini.bin') - elif args.handler == CodeHandler.Types.FULL: - codeHandlerFile = Path('bin/codehandler.bin') - else: - self.error(color_text( - f'Codehandler type {args.handler} is invalid\n', defaultColor=TREDLIT)) - - if not dolFile.is_file(): - self.error(color_text( - f'File "{dolFile}" does not exist\n', defaultColor=TREDLIT)) - - if not codeList.exists(): - self.error(color_text( - f'File/folder "{codeList}" does not exist\n', defaultColor=TREDLIT)) - - return {"dol": dolFile, - "codepath": codeList, - "codehandler": codeHandlerFile, - "destination": dest, - "allocation": _allocation, - "hookaddress": _codehook, - "hooktype": args.hooktype, - "initaddress": None if args.init is None else int(args.init, 16), - "includeall": args.txtcodes.lower() == "all", - "optimize": args.optimize, - "protect": args.protect, - "encrypt": args.encrypt, - "verbosity": args.verbose, - "quiet": args.quiet} - - def _exec(self, args, tmpdir): - context = self._validate_args(args) - - try: - with context["dol"].open("rb") as dol: - dolFile = DolFile(dol) - with resource_path(context["codehandler"]).open("rb") as handler: - codeHandler = CodeHandler(handler) - codeHandler.allocation = context["allocation"] - codeHandler.hookAddress = context["hookaddress"] - codeHandler.hookType = context["hooktype"] - codeHandler.includeAll = context["includeall"] - codeHandler.optimizeList = context["optimize"] +class GeckoLoaderGUI(object): - with resource_path("bin/geckoloader.bin").open("rb") as kernelfile: - geckoKernel = KernelLoader(kernelfile, cli) - geckoKernel.initAddress = context["initaddress"] - geckoKernel.verbosity = context["verbosity"] - geckoKernel.quiet = context["quiet"] - geckoKernel.encrypt = context["encrypt"] - geckoKernel.protect = context["protect"] - - if not context["destination"].parent.exists(): - context["destination"].parent.mkdir(parents=True, exist_ok=True) - - geckoKernel.build(context["codepath"], dolFile, - codeHandler, TMPDIR, context["destination"]) - - except FileNotFoundError as e: - self.error(color_text(e, defaultColor=TREDLIT)) - - -class GUI(object): - - class Dialogs: + class Dialogs(IntEnum): LOAD_DEST = 0 LOAD_GCT = 1 LOAD_FOLDER = 2 @@ -317,11 +50,11 @@ def __init__(self, cli: GeckoLoaderCli): self.log = logging.getLogger(f"GeckoLoader {self.version}") - if not get_program_folder("GeckoLoader").exists(): - get_program_folder("GeckoLoader").mkdir() + pFolder = get_program_folder("GeckoLoader") + if not pFolder.exists(): + pFolder.mkdir() - hdlr = logging.FileHandler( - get_program_folder("GeckoLoader") / "error.log") + hdlr = logging.FileHandler(pFolder / "error.log") formatter = logging.Formatter( "\n%(levelname)s (%(asctime)s): %(message)s") hdlr.setFormatter(formatter) @@ -329,7 +62,7 @@ def __init__(self, cli: GeckoLoaderCli): def show_dialog(self, dialog_type=None): if dialog_type == "aboutqt": - QtWidgets.QMessageBox.aboutQt(self.app.activeWindow()) + QMessageBox.aboutQt(self.app.activeWindow()) elif dialog_type == "aboutGeckoLoader": desc = "".join(["GeckoLoader is a cross platform tool designed to give ", "the user the most efficient codespace usage possible.\n\n ", @@ -342,7 +75,7 @@ def show_dialog(self, dialog_type=None): "JoshuaMK \n\n", "All rights reserved."]) - QtWidgets.QMessageBox.about( + QMessageBox.about( self.app.activeWindow(), "About GeckoLoader", desc) elif dialog_type == "Preferences": self.uiprefs.show() @@ -353,12 +86,12 @@ def show_dialog(self, dialog_type=None): def version(self) -> str: return self.cli.__version__ - def _open_dol(self) -> tuple: + def _open_dol(self) -> Tuple[bool, str]: if self.dolPath is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open DOL", str(Path.home()), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open DOL", str(Path.home()), "Nintendo DOL Executable (*.dol);;All files (*)")[0]) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open DOL", str(self.dolPath.parent), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open DOL", str(self.dolPath.parent), "Nintendo DOL Executable (*.dol);;All files (*)")[0]) if fname == "" or fname is None: # Make sure we have something to open @@ -372,21 +105,21 @@ def _open_dol(self) -> tuple: else: return False, "The file does not exist!" - def _load_codes(self, isFolder: bool = False) -> tuple: + def _load_codes(self, isFolder: bool = False) -> Tuple[bool, str]: if not isFolder: if self.codePath[0] is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Codelist", str(Path.home()), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open Codelist", str(Path.home()), "Gecko Code Table (*.gct);;Gecko Codelist (*.txt);;All files (*)")[0]) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Codelist", str(self.codePath[0].parent), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open Codelist", str(self.codePath[0].parent), "Gecko Code Table (*.gct);;Gecko Codelist (*.txt);;All files (*)")[0]) else: if self.codePath[0] is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(Path.home()), - QtWidgets.QFileDialog.ShowDirsOnly)) + fname = str(QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(Path.home()), + QFileDialog.ShowDirsOnly)) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(self.codePath[0].parent), - QtWidgets.QFileDialog.ShowDirsOnly)) + fname = str(QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(self.codePath[0].parent), + QFileDialog.ShowDirsOnly)) if fname == "" or fname is None: # Make sure we have something to open return False, None @@ -404,10 +137,10 @@ def _load_codes(self, isFolder: bool = False) -> tuple: def _open_dest(self) -> tuple: if self.dolPath is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open DOL", str(Path.home()), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open DOL", str(Path.home()), "Nintendo DOL Executable (*.dol);;All files (*)")[0]) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open DOL", str(self.dolPath.parent), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open DOL", str(self.dolPath.parent), "Nintendo DOL Executable (*.dol);;All files (*)")[0]) if fname == "" or fname is None: # Make sure we have something to open @@ -420,10 +153,10 @@ def _open_dest(self) -> tuple: def _load_session(self) -> tuple: if self.sessionPath is None: - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Session", str(Path.home()), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open Session", str(Path.home()), "GeckoLoader Session (*.gprf);;All files (*)")[0]) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Session", str(self.sessionPath.parent), + fname = str(QFileDialog.getOpenFileName(self.ui, "Open Session", str(self.sessionPath.parent), "GeckoLoader Session (*.gprf);;All files (*)")[0]) if fname == "" or fname is None: # Make sure we have something to open @@ -464,10 +197,10 @@ def _load_session(self) -> tuple: def _save_session(self, saveAs=False): if saveAs or self.sessionPath is None or self.sessionPath == "": if self.sessionPath is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getSaveFileName(self.ui, "Save Session", str(Path.home()), + fname = str(QFileDialog.getSaveFileName(self.ui, "Save Session", str(Path.home()), "GeckoLoader Session (*.gprf);;All files (*)")[0]) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getSaveFileName(self.ui, "Save Session", str(self.dolPath.parent), + fname = str(QFileDialog.getSaveFileName(self.ui, "Save Session", str(self.dolPath.parent), "GeckoLoader Session (*.gprf);;All files (*)")[0]) if fname == "" or fname is None: # Make sure we have something to open @@ -505,19 +238,19 @@ def _save_session(self, saveAs=False): def file_dialog_exec(self, event: Dialogs): try: - if event == GUI.Dialogs.LOAD_DOL: + if event == GeckoLoaderGUI.Dialogs.LOAD_DOL: status, msg = self._open_dol() - elif event == GUI.Dialogs.LOAD_GCT: + elif event == GeckoLoaderGUI.Dialogs.LOAD_GCT: status, msg = self._load_codes(False) - elif event == GUI.Dialogs.LOAD_FOLDER: + elif event == GeckoLoaderGUI.Dialogs.LOAD_FOLDER: status, msg = self._load_codes(True) - elif event == GUI.Dialogs.LOAD_DEST: + elif event == GeckoLoaderGUI.Dialogs.LOAD_DEST: status, msg = self._open_dest() - elif event == GUI.Dialogs.LOAD_SESSION: + elif event == GeckoLoaderGUI.Dialogs.LOAD_SESSION: status, msg = self._load_session() - elif event == GUI.Dialogs.SAVE_SESSION: + elif event == GeckoLoaderGUI.Dialogs.SAVE_SESSION: status, msg = self._save_session() - elif event == GUI.Dialogs.SAVE_SESSION_AS: + elif event == GeckoLoaderGUI.Dialogs.SAVE_SESSION_AS: status, msg = self._save_session(True) else: return @@ -526,12 +259,12 @@ def file_dialog_exec(self, event: Dialogs): return if status is False and msg is not None: - reply = QtWidgets.QErrorMessage(self) + reply = QErrorMessage(self) reply.setWindowTitle("I/O Failure") reply.setText(msg) reply.setInformativeText("Please try again.") - reply.setIcon(QtWidgets.QMessageBox.Warning) - reply.setStandardButtons(QtWidgets.QMessageBox.Ok) + reply.setIcon(QMessageBox.Warning) + reply.setStandardButtons(QMessageBox.Ok) reply.exec_() else: self.ui.set_edit_fields() @@ -568,7 +301,7 @@ def load_prefs(self): self.log.exception(e) # Use defaults for prefs else: # Input validation - if (p.get("qtstyle") in list(QtWidgets.QStyleFactory.keys()) or + if (p.get("qtstyle") in list(QStyleFactory.keys()) or p.get("qtstyle") == "Default"): self.prefs["qtstyle"] = p.get("qtstyle") @@ -582,7 +315,7 @@ def load_prefs(self): else: setCIndex(self.uiprefs.qtstyleSelect.findText( self.prefs.get("qtstyle"), - flags=QtCore.Qt.MatchFixedString)) + flags=Qt.MatchFixedString)) self.uiprefs.qtdarkButton.setChecked( self.prefs.get("darktheme")) @@ -618,7 +351,7 @@ def load_qtstyle(self, style, first_style_load=False): if first_style_load: setCIndex = self.uiprefs.qtstyleSelect.setCurrentIndex setCIndex(self.uiprefs.qtstyleSelect.findText(style, - flags=QtCore.Qt.MatchFixedString)) + flags=Qt.MatchFixedString)) def update_theme(self): if self.uiprefs.qtdarkButton.isChecked(): @@ -642,47 +375,30 @@ def display_update(self): else: _status = True - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap( - str(resource_path(Path("bin/icon.ico")))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon = QIcon() + icon.addPixmap(QPixmap( + str(resource_path(Path("bin/icon.ico")))), QIcon.Normal, QIcon.Off) if _status is False: - reply = QtWidgets.QErrorMessage() + reply = QErrorMessage() reply.setWindowIcon(icon) reply.setWindowTitle("Response Error") reply.setText(self._remove_ansi(_errpipe.getvalue())) reply.setInformativeText( "Make sure you have an internet connection") - reply.setIcon(QtWidgets.QMessageBox.Warning) - reply.setStandardButtons(QtWidgets.QMessageBox.Ok) + reply.setIcon(QMessageBox.Warning) + reply.setStandardButtons(QMessageBox.Ok) reply.exec_() else: - reply = QtWidgets.QMessageBox() + reply = QMessageBox() reply.setWindowIcon(icon) reply.setWindowTitle("Update Info") reply.setText(self._remove_ansi(_outpipe.getvalue()).strip( "\n") + "\n\nYou can find all GeckoLoader releases at:\nhttps://github.com/JoshuaMKW/GeckoLoader/releases") - reply.setIcon(QtWidgets.QMessageBox.Information) - reply.setStandardButtons(QtWidgets.QMessageBox.Ok) + reply.setIcon(QMessageBox.Information) + reply.setStandardButtons(QMessageBox.Ok) reply.exec_() - @staticmethod - def _enforce_mask(textbox: QtWidgets.QTextEdit, mask: int, _or: int = 0): - textbox.setText(textbox.text().strip()) - if len(textbox.text()) > 0: - _depth = len(hex(mask)[2:]) - _address = int(textbox.text(), 16) << ( - (_depth - len(textbox.text())) << 2) - - aligned = hex(((_address & mask) | _or) >> ( - (_depth - len(textbox.text())) << 2))[2:].upper() - textbox.setText(aligned) - - @staticmethod - def _remove_ansi(msg: str) -> str: - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - return ansi_escape.sub('', msg) - def connect_signals(self): self.ui.actionPreferences.triggered.connect( lambda: self.show_dialog("Preferences")) @@ -694,21 +410,21 @@ def connect_signals(self): lambda: self.display_update()) self.ui.actionOpen.triggered.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_SESSION)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.LOAD_SESSION)) self.ui.actionClose.triggered.connect(lambda: self.close_session()) self.ui.actionSave_As.triggered.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.SAVE_SESSION_AS)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.SAVE_SESSION_AS)) self.ui.actionSave.triggered.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.SAVE_SESSION)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.SAVE_SESSION)) self.ui.dolButton.clicked.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_DOL)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.LOAD_DOL)) self.ui.gctFileButton.clicked.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_GCT)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.LOAD_GCT)) self.ui.gctFolderButton.clicked.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_FOLDER)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.LOAD_FOLDER)) self.ui.destButton.clicked.connect( - lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_DEST)) + lambda: self.file_dialog_exec(GeckoLoaderGUI.Dialogs.LOAD_DEST)) self.ui.dolTextBox.textChanged.connect( lambda: self.ui.set_edit_fields()) @@ -736,6 +452,39 @@ def connect_signals(self): self.uiexSettings.kernelHookLineEdit.textChanged.connect(lambda: self._enforce_mask( self.uiexSettings.kernelHookLineEdit, 0x817FFFFC, 0x80000000)) + def run(self): + if sys.platform != "win32": + datapath = Path.home() / ".GeckoLoader" + else: + datapath = Path(os.getenv("APPDATA")) / "GeckoLoader" + + if not datapath.is_dir(): + datapath.mkdir() + + self.app = QApplication(sys.argv) + self.default_qtstyle = self.app.style().objectName() + self.ui = MainWindow(self.version) + self.uiprefs = PrefWindow() + self.uiexSettings = SettingsWindow() + + self.uiprefs.qtstyleSelect.addItem("Default") + + styleKeys = list(QStyleFactory.keys()) + self.uiprefs.qtstyleSelect.addItems(styleKeys) + + self.load_prefs() + self.load_qtstyle(self.prefs.get("qtstyle"), True) + + regex = QRegularExpression("[0-9A-Fa-f]*") + validator = QRegularExpressionValidator(regex) + self.ui.allocLineEdit.setValidator(validator) + self.uiexSettings.codehookLineEdit.setValidator(validator) + self.uiexSettings.kernelHookLineEdit.setValidator(validator) + + self.connect_signals() + self.ui.show() + sys.exit(self.app.exec_()) + def _exec_api(self): if sys.platform == "win32": self.ui.responses.appendPlainText( @@ -832,7 +581,7 @@ def _exec_api(self): with redirect_stdout(_outpipe), redirect_stderr(_errpipe): try: - self.cli._exec(args, tmpdir=TMPDIR) + self.cli._exec(args) except (SystemExit, Exception): _status = False else: @@ -846,53 +595,19 @@ def _exec_api(self): _msg += line.lstrip() + "\n" self.ui.responses.appendPlainText(_msg.strip() + "\n") - def run(self): - if sys.platform != "win32": - datapath = Path.home() / ".GeckoLoader" - else: - datapath = Path(os.getenv("APPDATA")) / "GeckoLoader" - - if not datapath.is_dir(): - datapath.mkdir() - - self.app = QtWidgets.QApplication(sys.argv) - self.default_qtstyle = self.app.style().objectName() - self.ui = MainWindow(self.version) - self.uiprefs = PrefWindow() - self.uiexSettings = SettingsWindow() - - self.uiprefs.qtstyleSelect.addItem("Default") - - styleKeys = list(QtWidgets.QStyleFactory.keys()) - self.uiprefs.qtstyleSelect.addItems(styleKeys) - - self.load_prefs() - self.load_qtstyle(self.prefs.get("qtstyle"), True) - - regex = QtCore.QRegExp("[0-9A-Fa-f]*") - validator = QtGui.QRegExpValidator(regex) - self.ui.allocLineEdit.setValidator(validator) - self.uiexSettings.codehookLineEdit.setValidator(validator) - self.uiexSettings.kernelHookLineEdit.setValidator(validator) - - self.connect_signals() - self.ui.show() - sys.exit(self.app.exec_()) + @staticmethod + def _enforce_mask(textbox: QTextEdit, mask: int, _or: int = 0): + textbox.setText(textbox.text().strip()) + if len(textbox.text()) > 0: + _depth = len(hex(mask)[2:]) + _address = int(textbox.text(), 16) << ( + (_depth - len(textbox.text())) << 2) + aligned = hex(((_address & mask) | _or) >> ( + (_depth - len(textbox.text())) << 2))[2:].upper() + textbox.setText(aligned) -if __name__ == "__main__": - cli = GeckoLoaderCli('GeckoLoader', __version__, - description='Dol editing tool for allocating extended codespace') - - if len(sys.argv) == 1: - cli.print_splash() - app = GUI(cli) - signal.signal(signal.SIGINT, signal.SIG_DFL) - app.run() - elif '--checkupdate' in sys.argv: - cli.check_updates() - elif '--splash' in sys.argv: - cli.print_splash() - else: - args = cli.parse_args() - cli._exec(args, TMPDIR) + @staticmethod + def _remove_ansi(msg: str) -> str: + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + return ansi_escape.sub('', msg) diff --git a/geckoloader/kernel.py b/geckoloader/kernel.py new file mode 100644 index 0000000..5688988 --- /dev/null +++ b/geckoloader/kernel.py @@ -0,0 +1,549 @@ +import functools +import random +import re +import string +import sys +import time +from enum import Enum +from io import BytesIO, StringIO +from pathlib import Path +from typing import BinaryIO, Dict, Generator, IO, Iterable, List, Optional, Set, TextIO, Tuple, Union + +import tools +from dolreader.dol import DolFile, SectionCountFullError, UnmappedAddressError +from fileutils import (get_alignment, read_uint16, read_uint32, write_bool, + write_sint32, write_ubyte, write_uint16, write_uint32) +from geckolibs.gct import GeckoCodeTable +from geckolibs.geckocode import AsmExecute, GeckoCode + +try: + import chardet +except ImportError as IE: + print(IE) + sys.exit(1) + + +def timer(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + value = func(*args, **kwargs) + end = time.perf_counter() + print(tools.color_text( + f"\n :: Completed in {(end - start):0.4f} seconds!\n", defaultColor=tools.TGREENLIT)) + return value + return wrapper + + +def create_branch(to: int, _from: int, lk: bool = False) -> int: + """ Create a branch instruction at `_from`\n + to: address to branch to\n + _from: address to branch from\n + lk: is branch linking? """ + + _from &= 0xFFFFFFFC + to &= 0xFFFFFFFC + return (to - _from) & 0x3FFFFFD | 0x48000000 | (1 if lk else 0) + + +class CodeHandler(object): + class Types(Enum): + MINI = "MINI" + FULL = "FULL" + + WiiVIHook = b"\x7C\xE3\x3B\x78\x38\x87\x00\x34\x38\xA7\x00\x38\x38\xC7\x00\x4C" + GCNVIHook = b"\x7C\x03\x00\x34\x38\x83\x00\x20\x54\x85\x08\x3C\x7C\x7F\x2A\x14\xA0\x03\x00\x00\x7C\x7D\x2A\x14\x20\xA4\x00\x3F\xB0\x03\x00\x00" + WiiGXDrawHook = b"\x3C\xA0\xCC\x01\x38\x00\x00\x61\x3C\x80\x45\x00\x98\x05\x80\x00" + GCNGXDrawHook = b"\x38\x00\x00\x61\x3C\xA0\xCC\x01\x3C\x80\x45\x00\x98\x05\x80\x00" + WiiPADHook = b"\x3A\xB5\x00\x01\x3A\x73\x00\x0C\x2C\x15\x00\x04\x3B\x18\x00\x0C" + GCNPADHook = b"\x3A\xB5\x00\x01\x2C\x15\x00\x04\x3B\x18\x00\x0C\x3B\xFF\x00\x0C" + + def __init__(self, f: BinaryIO): + self.baseAddress = int.from_bytes(f.read(4), "big", signed=False) + self._rawData = BytesIO(f.read()) + self.gct: GeckoCodeTable = None + + # Get codelist pointer + self._rawData.seek(0xFA) + codelistUpper = self._rawData.read(2).hex() + self._rawData.seek(0xFE) + codelistLower = self._rawData.read(2).hex() + + self._rawDataPointer = int(codelistUpper[2:] + codelistLower[2:], 16) + self.handlerLength = tools.stream_size(self._rawData) + + self.allocation = None + self.hookAddress = None + self.hookType = None + self.includeAll = False + self.optimizeList = False + + self.type = KernelLoader.HandlerType.MINI if self.handlerLength < 0x900 else KernelLoader.HandlerType.FULL + + f.seek(0) + + def init_gct(self, gctPath: Path): + if gctPath.suffix.lower() == ".txt": + self.gct = GeckoCodeTable.from_text(gctPath.read_text()) + elif gctPath.suffix.lower() == ".gct": + self.gct = GeckoCodeTable.from_bytes(gctPath.read_bytes()) + elif gctPath.suffix == "": + gct = GeckoCodeTable() + for file in gctPath.iterdir(): + if not file.is_file(): + continue + + if file.suffix.lower() == ".txt": + nextGCT = GeckoCodeTable.from_text(gctPath.read_text()) + if gct.gameID == "GECK01": + gct.gameID = nextGCT.gameID + gct.gameName = nextGCT.gameName + gct += nextGCT + elif file.suffix.lower() == ".gct": + nextGCT = GeckoCodeTable.from_bytes(gctPath.read_bytes()) + gct += nextGCT + else: + print(tools.color_text( + f" :: HINT: {file} is not a .txt or .gct file", defaultColor=tools.TYELLOWLIT)) + self.gct = gct + else: + raise NotImplementedError( + f"Parsing file type `{gctPath.suffix}' as a GCT is unsupported") + + def set_variables(self, dol: DolFile): + varOffset = self.__find_variable_data(b"\x00\xDE\xDE\xDE") + if varOffset is None: + raise RuntimeError(tools.color_text( + "Variable codehandler data not found\n", defaultColor=tools.TREDLIT)) + + self.__set_hook_instruction(dol, self.hookAddress, varOffset, 0) + + self._rawData.seek(varOffset + 4) + write_uint32(self._rawData, create_branch(self.hookAddress + 4, + self.baseAddress + (varOffset + 4), False)) + + def __find_variable_data(self, variable) -> int: + self._rawData.seek(0) + + while sample := self._rawData.read(4): + if sample == variable: + return self._rawData.tell() - 4 + + return None + + def __set_hook_instruction(self, dol: DolFile, hookAddress: int, returnOffset: int, lk: bool = False): + self._rawData.seek(returnOffset) + dol.seek(hookAddress) + ppc = read_uint32(dol) + + if ((((ppc >> 24) & 0xFF) > 0x47 and ((ppc >> 24) & 0xFF) < 0x4C) or (((ppc >> 24) & 0xFF) > 0x3F and ((ppc >> 24) & 0xFF) < 0x44)): + to, conditional = dol.extract_branch_addr(hookAddress) + if conditional: + raise NotImplementedError( + "Hooking to a conditional non spr branch is unsupported") + dol.insert_branch + write_uint32( + self._rawData, + create_branch(to, self.baseAddress + returnOffset, lk) + ) + else: + write_uint32(self._rawData, ppc) + + +class KernelLoader(object): + class DataCryptor(object): + @ staticmethod + def encrypt_key(key: int) -> int: + b1 = key & 0xFF + b2 = (key >> 8) & 0xFF + b3 = (key >> 16) & 0xFF + b4 = (key >> 24) & 0xFF + b3 ^= b4 + b2 ^= b3 + b1 ^= b2 + return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 + + @ staticmethod + def decrypt_key(key: int) -> int: + b1 = (key >> 24) & 0xFF + b2 = (key >> 16) & 0xFF + b3 = (key >> 8) & 0xFF + b4 = key & 0xFF + b1 ^= b2 + b2 ^= b3 + b3 ^= b4 + return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1 + + def __init__(self, key: int): + self.key = key + + @ property + def encryptedKey(self) -> int: + return self.encrypt_key(self.key) + + def xorcrypt_data(self, data: bytes) -> bytes: + stream = BytesIO(data) + streamLength = len(stream.getbuffer()) + i = 0 + try: + while (stream.tell() < streamLength): + packet = read_uint32(stream) + stream.seek(-4, 1) + write_uint32(stream, (packet ^ self.key) & 0xFFFFFFFF) + self.key += (i << 3) & 0xFFFFFFFF + if self.key > 0xFFFFFFFF: + self.key -= 0x100000000 + i += 1 + except Exception: + pass + return stream.getvalue() + + class HandlerType(Enum): + MINI = "MINI" + FULL = "FULL" + + GeckoProtector = AsmExecute( + b" \ + \x7C\x08\x02\xA6\x94\x21\xFF\x70 \ + \x90\x01\x00\x08\xBC\x61\x00\x0C \ + \x48\x00\x00\x0D\x00\xD0\xC0\xDE \ + \x00\xD0\xDE\xAD\x7F\xE8\x02\xA6 \ + \x3B\xDF\x00\x08\x3C\x60\x80\x00 \ + \x38\x80\x11\x00\x38\xA0\x00\x00 \ + \x60\x63\x1E\xF8\x7C\x89\x03\xA6 \ + \x38\x80\x00\x00\x7D\x03\x22\x14 \ + \x54\xE9\x06\x3E\x89\x08\x00\x08 \ + \x7D\x3F\x48\xAE\x38\xE7\x00\x01 \ + \x7C\x08\x48\x40\x41\x82\x00\x0C \ + \x60\xA7\x00\x00\x48\x00\x00\x04 \ + \x54\xE8\x06\x3E\x28\x08\x00\x03 \ + \x41\x81\x00\x10\x38\x84\x00\x01 \ + \x42\x00\xFF\xCC\x48\x00\x00\x2C \ + \x38\xA0\x00\x08\x7C\x84\x1A\x14 \ + \x7C\xA9\x03\xA6\x38\x60\x00\x00 \ + \x38\x84\xFF\xFF\x54\x66\x07\xBE \ + \x7C\xDE\x30\xAE\x38\x63\x00\x01 \ + \x9C\xC4\x00\x01\x42\x00\xFF\xF0 \ + \xB8\x61\x00\x0C\x80\x01\x00\x08 \ + \x38\x21\x00\x90\x7C\x08\x03\xA6 \ + \x4E\x80\x00\x20\x00\x00\x00\x00 \ + " + ) + + def __init__(self, f: BinaryIO, hookType: CodeHandler.Hook, hookAddress: int, initAddress: int, + allocation: Optional[int] = None, includeAllCodes: bool = False, optimizeCodes: bool = False, + protectGame: bool = False, encryptCodes: bool = False, cli: Optional[tools.CommandLineParser] = None): + self.hookHandler = HookHandler(hookType) + self.hookAddress = hookAddress + self.initAddress = initAddress + self.allocation = allocation + self.includeAll = includeAllCodes + self.optimize = optimizeCodes + self.protect = protectGame + self.encrypt = encryptCodes + + self._rawData = BytesIO(f.read()) + self._initDataList = None + self._gpModDataList: Tuple[int, int] = None + self._gpDiscDataList: Tuple[int, int] = None + self._gpKeyAddrList: Tuple[int, int] = None + + self._cli = cli + self._verbosity = 0 + self._quiet = False + + @ property + def verbosity(self) -> int: + return self._verbosity + + @ verbosity.setter + def verbosity(self, level: int): + self._verbosity = max(min(level, 0), 3) + + def silence(self): + self._quiet = True + + def desilence(self): + self._quiet = False + + def error(self, msg: str, buffer: TextIO = sys.stderr): + if self._cli is not None: + self._cli.error(msg) + else: + print(msg, file=buffer) + sys.exit(1) + + def apply_reloc(self, key: bytes, value: int): + keylen = len(key) + self._rawData.seek(0) + while (offset := self._rawData.getvalue().find(key)) >= 0: + self._rawData.seek(offset) + if keylen == 1: + _bytes = (value & 0xFF).to_bytes(1, "big", signed=False) + elif keylen == 2: + _bytes = (value & 0xFFFF).to_bytes(2, "big", signed=False) + elif keylen == 4: + _bytes = (value & 0xFFFFFFFF).to_bytes(4, "big", signed=False) + self._rawData.write(_bytes) + + def do_data_relocs(self, entryAddress: int, modAddress: int, encryptKeyAddress: int, baseOffset: int = 0): + self.apply_reloc(b"GH", ((modAddress >> 16) & 0xFFFF)) + self.apply_reloc(b"GL", (modAddress & 0xFFFF) + baseOffset) + self.apply_reloc(b"IH", (entryAddress >> 16) & 0xFFFF) + self.apply_reloc(b"IL", entryAddress & 0xFFFF) + self.apply_reloc(b"GH", ((encryptKeyAddress >> 16) & 0xFFFF)) + self.apply_reloc(b"GL", (encryptKeyAddress & 0xFFFF) + baseOffset) + + def complete_data(self, codeHandler: CodeHandler, initAddress: int): + _upperAddr, _lowerAddr = ( + (self.initAddress >> 16) & 0xFFFF, self.initAddress & 0xFFFF) + _key = random.randrange(0x100000000) + self._rawData.seek(0) + + self.apply_reloc(b"HEAP", len(codeHandler) + len(codeHandler.gct)) + self.apply_reloc(b"LSIZ", len(self._rawData.getbuffer())) + self.apply_reloc(b"HSIZ", len(codeHandler)) + self.apply_reloc(b"CSIZ", len(codeHandler.gct)) + self.apply_reloc(b"HOOK", self.hookAddress) + self.apply_reloc(b"CRPT", int(self.encrypt)) + self.apply_reloc(b"CYPT", KernelLoader.DataCryptor.encrypt_key(_key)) + + gpModInfoOffset = (self._rawData.getvalue().find(b"HEAP") << 16) + gpModUpperAddr = _upperAddr + \ + 1 if ( + _lowerAddr + gpModInfoOffset) > 0x7FFF else _upperAddr # Absolute addressing + gpKeyOffset = self._rawData.getvalue().find(b"CYPT") + gpKeyUpperAddr = _upperAddr + \ + 1 if ( + _lowerAddr + gpKeyOffset) > 0x7FFF else _upperAddr # Absolute addressing + + if _lowerAddr + gpModInfoOffset > 0xFFFF: + _lowerAddr -= 0x10000 + + self.do_data_relocs(initAddress, ((gpModUpperAddr << 16) | gpModInfoOffset) & 0xFFFFFFFF, (( + gpKeyUpperAddr << 16) | gpKeyOffset) & 0xFFFFFFFF, _lowerAddr) + + if self.encrypt: + codeHandler.encrypt_codes(_key) + + def patch_arena(self, codeHandler: CodeHandler, dol: DolFile) -> tuple: + self.complete_data( + codeHandler, [(dol.entryPoint >> 16) & 0xFFFF, dol.entryPoint & 0xFFFF]) + + self._rawData.seek(0, 2) + self._rawData.write(codeHandler._rawData.getvalue() + + codeHandler.gct._rawData.getvalue()) + + self._rawData.seek(0) + _kernelData = self._rawData.getvalue() + + try: + dol.append_text_sections([(_kernelData, self.initAddress)]) + except SectionCountFullError: + try: + dol.append_data_sections([(_kernelData, self.initAddress)]) + except SectionCountFullError: + self.error(tools.color_text( + "There are no unused sections left for GeckoLoader to use!\n", defaultColor=tools.TREDLIT)) + + dol.entryPoint = self.initAddress + return True, None + + def patch_legacy(self, codeHandler: CodeHandler, dol: DolFile) -> tuple: + codeHandler._rawData.seek(0) + codeHandler.gct._rawData.seek(0) + + _handlerData = codeHandler._rawData.getvalue() + codeHandler.gct._rawData.getvalue() + + try: + dol.append_text_sections( + [(_handlerData, codeHandler.initAddress)]) + except SectionCountFullError: + try: + dol.append_data_sections( + [(_handlerData, codeHandler.initAddress)]) + except SectionCountFullError: + self.error(tools.color_text( + "There are no unused sections left for GeckoLoader to use!\n", defaultColor=tools.TREDLIT)) + + return True, None + + def protect_game(self, codeHandler: CodeHandler): + codeHandler.gct.add_child(KernelLoader.GeckoProtector) + + @ timer + def build(self, gctPath: Path, dol: DolFile, codeHandler: CodeHandler, tmpdir: Path, dump: Path): + _oldStart = dol.entryPoint + + # Initialize our codes + codeHandler.gct = self.load_gct_from(gctPath) + + if codeHandler.gct is None: + self.error(tools.color_text( + "Valid codelist not found. Please provide a .txt/.gct file, or a folder of .txt/.gct files\n", defaultColor=tools.TREDLIT)) + + if self.protect: + self.protect_game(codeHandler) + + # Get entrypoint (or BSS midpoint) for insert + + if self.initAddress: + try: + dol.resolve_address(self.initAddress) + self.error(tools.color_text( + f"Init address specified for GeckoLoader (0x{self.initAddress:X}) clobbers existing dol sections", defaultColor=tools.TREDLIT)) + except UnmappedAddressError: + pass + else: + self.initAddress = dol.seek_nearest_unmapped(dol.bssAddress, len( + self._rawData.getbuffer()) + len(codeHandler) + len(codeHandler.gct)) + self._rawData.seek(0) + + if codeHandler.optimizeList: + codeHandler.gct.optimize_codelist(dol) + + # Is codelist optimized away? + + if codeHandler.gct._rawData.getvalue() == b"\x00\xD0\xC0\xDE\x00\xD0\xC0\xDE\xF0\x00\x00\x00\x00\x00\x00\x00": + with dump.open("wb") as final: + dol.save(final) + + if not self.quiet: + if self.verbosity >= 3: + dol.print_info() + print("-"*64) + if self.verbosity >= 1: + print(tools.color_text( + "\n :: All codes have been successfully pre patched", defaultColor=tools.TGREENLIT)) + return + + hooked = determine_codehook(dol, codeHandler, False) + if hooked: + _status, _msg = self.patch_arena(codeHandler, dol) + else: + self.error(tools.color_text( + "Failed to find a hook address. Try using option --codehook to use your own address\n", defaultColor=tools.TREDLIT)) + + if _status is False: + self.error(tools.color_text( + _msg + "\n", defaultColor=tools.TREDLIT)) + elif codeHandler.allocation < codeHandler.gct.byteSize: + self.error(tools.color_text( + "Allocated codespace was smaller than the given codelist\n", defaultColor=tools.TYELLOW)) + + with dump.open("wb") as final: + dol.save(final) + + if self.quiet: + return + + if self.verbosity >= 3: + dol.print_info() + print("-"*64) + + if self.verbosity >= 2: + print("") + info = [f" :: Start of game modified to address 0x{self.initAddress:X}", + f" :: Game function `__start()' located at address 0x{_oldStart:X}", + f" :: Allocation is 0x{codeHandler.allocation:X}; codelist size is 0x{codeHandler.gct.byteSize:X}", + f" :: Codehandler hooked at 0x{codeHandler.hookAddress:X}", + f" :: Codehandler is of type `{codeHandler.type}'", + f" :: Of the {DolFile.maxTextSections} text sections in this DOL file, {len(dol.textSections)} are now being used", + f" :: Of the {DolFile.maxDataSections} text sections in this DOL file, {len(dol.dataSections)} are now being used"] + + for bit in info: + print(tools.color_text(bit, defaultColor=tools.TGREENLIT)) + + elif self.verbosity >= 1: + print("") + info = [f" :: GeckoLoader set at address 0x{self.initAddress:X}", + f" :: Legacy size is 0x{codeHandler.allocation:X}; codelist size is 0x{codeHandler.gct.byteSize:X}", + f" :: Codehandler is of type `{codeHandler.type}'"] + for bit in info: + print(tools.color_text(bit, defaultColor=tools.TGREENLIT)) + + def load_gct_from(self, gctPath: Path) -> GeckoCodeTable: + def load_data(gctFile: Path) -> GeckoCodeTable: + if gctFile.suffix.lower() == ".txt": + return GeckoCodeTable.from_text(gctFile.read_text()) + elif gctFile.suffix.lower() == ".gct": + return GeckoCodeTable.from_bytes(gctFile.read_bytes()) + else: + print(tools.color_text( + f" :: HINT: {file} is not a .txt or .gct file, and will be ignored", defaultColor=tools.TYELLOWLIT)) + + gct = GeckoCodeTable() + if gctPath.is_dir(): + for file in gctPath.iterdir(): + gct += load_data(file) + elif gctPath.is_file(): + gct += load_data(gctPath) + + return gct + + +def determine_codehook(dol: DolFile, codeHandler: CodeHandler, hook=False) -> bool: + if codeHandler.hookAddress is None: + if not assert_code_hook(dol, codeHandler): + return False + + if hook: + codeHandler.set_variables(dol) + insert_code_hook(dol, codeHandler, codeHandler.hookAddress) + + return True + + +def assert_code_hook(dol: DolFile, codeHandler: CodeHandler) -> bool: + for section in dol.textSections: + dol.seek(section["address"]) + sample = dol.read(section["size"]) + + if codeHandler.hookType == "VI": + result = sample.find(codeHandler.GCNVIHook) + elif codeHandler.hookType == "GX": + result = sample.find(codeHandler.GCNGXDrawHook) + elif codeHandler.hookType == "PAD": + result = sample.find(codeHandler.GCNPADHook) + else: + raise NotImplementedError(tools.color_text( + f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) + + if result >= 0: + dol.seek(section["address"] + result) + else: + if codeHandler.hookType == "VI": + result = sample.find(codeHandler.WiiVIHook) + elif codeHandler.hookType == "GX": + result = sample.find(codeHandler.WiiGXDrawHook) + elif codeHandler.hookType == "PAD": + result = sample.find(codeHandler.WiiPADHook) + else: + raise NotImplementedError(tools.color_text( + f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) + + if result >= 0: + dol.seek(section["address"] + result) + else: + continue + + while (sample := read_uint32(dol)) != 0x4E800020: + pass + + dol.seek(-4, 1) + codeHandler.hookAddress = dol.tell() + + return True + return False + + +def insert_code_hook(dol: DolFile, codeHandler: CodeHandler, address: int): + dol.seek(address) + ppc = read_uint32(dol) + + if ((ppc >> 24) & 0xFF) > 0x3F and ((ppc >> 24) & 0xFF) < 0x48: + raise NotImplementedError(tools.color_text( + "Hooking the codehandler to a conditional non spr branch is unsupported", defaultColor=tools.TREDLIT)) + + dol.seek(-4, 1) + dol.insert_branch(codeHandler.startAddress, address, lk=0) diff --git a/geckoloader/main_ui.py b/geckoloader/main_ui.py new file mode 100644 index 0000000..3da07e5 --- /dev/null +++ b/geckoloader/main_ui.py @@ -0,0 +1,847 @@ +from PySide6.QtGui import QIcon, QPalette, QFont, QCloseEvent, QPixmap, QColor, QFontMetricsF, QAction +from PySide6.QtCore import Qt, QSize, QRect, QMetaObject +from PySide6.QtWidgets import (QApplication, QComboBox, QWidget, + QGridLayout, QLabel, QMessageBox, QPushButton, QFrame, QPlainTextEdit, QMenuBar, QMenu, QStatusBar, QMainWindow, + QLineEdit, QSizePolicy) + +from geckoloader.fileutils import resource_path + + +class MainWindow(QMainWindow): + def __init__(self, version: str): + super().__init__() + + self._job_active = False + self.apiRevision = version + self.setup_ui() + + self.LightTheme = self.palette() + + self.DarkTheme = QPalette() + self.DarkTheme.setColor(QPalette.Window, QColor(53, 53, 53)) + self.DarkTheme.setColor(QPalette.WindowText, Qt.white) + self.DarkTheme.setColor(QPalette.Base, QColor(25, 25, 25)) + self.DarkTheme.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) + self.DarkTheme.setColor(QPalette.ToolTipBase, Qt.black) + self.DarkTheme.setColor(QPalette.ToolTipText, Qt.white) + self.DarkTheme.setColor(QPalette.Text, Qt.white) + self.DarkTheme.setColor(QPalette.Button, QColor(53, 53, 53)) + self.DarkTheme.setColor(QPalette.ButtonText, Qt.white) + self.DarkTheme.setColor(QPalette.BrightText, Qt.red) + self.DarkTheme.setColor(QPalette.Link, QColor(42, 130, 218)) + self.DarkTheme.setColor(QPalette.Highlight, QColor(42, 130, 218)) + self.DarkTheme.setColor(QPalette.HighlightedText, Qt.black) + + def set_job_activity(self, active: bool): + self._job_active = active + + def close_event(self, event: QCloseEvent): + if self._job_active: + reply = QMessageBox(self) + reply.setWindowTitle("Active job") + reply.setText("GeckoLoader is busy!") + reply.setInformativeText("Exiting is disabled") + reply.setIcon(QMessageBox.Warning) + reply.setStandardButtons(QMessageBox.Ok) + reply.setDefaultButton(QMessageBox.Ok) + reply.exec_() + event.ignore() + else: + event.accept() + + def setup_ui(self): + self.setObjectName("MainWindow") + self.setWindowModality(Qt.NonModal) + self.setEnabled(True) + self.setFixedSize(550, 680) + font = QFont() + font.setFamily("Helvetica") + font.setPointSize(10) + font.setWeight(42) + self.setFont(font) + icon = QIcon() + icon.addPixmap(QPixmap(str(resource_path("bin/icon.ico"))), + QIcon.Normal, QIcon.Off) + self.setWindowIcon(icon) + + # Top level widget + self.centerWidget = QWidget(self) + self.centerWidget.setObjectName("centerWidget") + + self.gridLayout = QGridLayout(self.centerWidget) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + + # Layout for file paths and open boxes + self.filesLayout = QGridLayout() + self.filesLayout.setHorizontalSpacing(0) + self.filesLayout.setObjectName("filesLayout") + + self.dolLayout = QGridLayout() + self.dolLayout.setHorizontalSpacing(0) + self.dolLayout.setObjectName("dolLayout") + + # Layout for folder path + self.gctLayout = QGridLayout() + self.gctLayout.setHorizontalSpacing(0) + self.gctLayout.setVerticalSpacing(5) + self.gctLayout.setObjectName("gctLayout") + + self.destLayout = QGridLayout() + self.dolLayout.setHorizontalSpacing(0) + self.dolLayout.setObjectName("dolLayout") + + # Files label + self.filesLabel = QLabel(self.centerWidget) + self.filesLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.filesLabel.sizePolicy().hasHeightForWidth()) + self.filesLabel.setSizePolicy(sizePolicy) + self.filesLabel.setMinimumSize(QSize(80, 30)) + self.filesLabel.setMaximumSize(QSize(16777215, 30)) + font = QFont("Helvetica") + font.setPointSize(21) + font.setWeight(82) + font.setBold(True) + self.filesLabel.setFont(font) + self.filesLabel.setTextFormat(Qt.PlainText) + self.filesLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.filesLabel.setObjectName("filesLabel") + + # Dol button to open file + self.dolButton = QPushButton(self.centerWidget) + self.dolButton.setMinimumSize(QSize(100, 26)) + self.dolButton.setMaximumSize(QSize(100, 26)) + font = QFont("Helvetica") + font.setPointSize(11) + self.dolButton.setFont(font) + self.dolButton.setCheckable(False) + self.dolButton.setChecked(False) + self.dolButton.setAutoDefault(True) + self.dolButton.setDefault(False) + self.dolButton.setFlat(False) + self.dolButton.setObjectName("dolButton") + self.dolLayout.addWidget(self.dolButton, 1, 0, 1, 1) + + # Dol path textbox + self.dolTextBox = QLineEdit(self.centerWidget) + self.dolTextBox.setEnabled(False) + self.dolTextBox.setMinimumSize(QSize(200, 24)) + self.dolTextBox.setMaximumSize(QSize(16777215, 24)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(10) + font.setWeight(42) + self.dolTextBox.setFont(font) + self.dolTextBox.setText("") + self.dolTextBox.setMaxLength(255) + self.dolTextBox.setFrame(True) + self.dolTextBox.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) + self.dolTextBox.setObjectName("dolTextBox") + self.dolLayout.addWidget(self.dolTextBox, 1, 1, 1, 1) + + # horizontal separater codes + self.horiSepFiles = QFrame(self.centerWidget) + self.horiSepFiles.setMinimumSize(QSize(474, 30)) + self.horiSepFiles.setContentsMargins(20, 0, 20, 0) + self.horiSepFiles.setFrameShape(QFrame.HLine) + self.horiSepFiles.setFrameShadow(QFrame.Sunken) + self.horiSepFiles.setObjectName("horiSepFiles") + + # gctFile button to open file + self.gctFileButton = QPushButton(self.centerWidget) + self.gctFileButton.setMinimumSize(QSize(100, 26)) + self.gctFileButton.setMaximumSize(QSize(100, 26)) + font = QFont("Helvetica") + font.setPointSize(10) + self.gctFileButton.setFont(font) + self.gctFileButton.setCheckable(False) + self.gctFileButton.setChecked(False) + self.gctFileButton.setAutoDefault(True) + self.gctFileButton.setDefault(False) + self.gctFileButton.setFlat(False) + self.gctFileButton.setObjectName("gctFileButton") + self.gctLayout.addWidget(self.gctFileButton, 0, 0, 1, 1) + + # gctFile path textbox + self.gctFileTextBox = QLineEdit(self.centerWidget) + self.gctFileTextBox.setEnabled(False) + self.gctFileTextBox.setMinimumSize(QSize(200, 24)) + self.gctFileTextBox.setMaximumSize(QSize(16777215, 24)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(10) + font.setWeight(42) + self.gctFileTextBox.setFont(font) + self.gctFileTextBox.setText("") + self.gctFileTextBox.setMaxLength(255) + self.gctFileTextBox.setFrame(True) + self.gctFileTextBox.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) + self.gctFileTextBox.setObjectName("gctFileTextBox") + self.gctLayout.addWidget(self.gctFileTextBox, 0, 1, 1, 1) + + # --or-- Label + self.orFolderLabel = QLabel(self.centerWidget) + self.orFolderLabel.setEnabled(False) + self.orFolderLabel.setMinimumSize(QSize(80, 8)) + self.orFolderLabel.setMaximumSize(QSize(16777215, 8)) + font = QFont("Helvetica") + font.setPointSize(8) + font.setWeight(82) + font.setBold(True) + self.orFolderLabel.setFont(font) + self.orFolderLabel.setTextFormat(Qt.PlainText) + self.orFolderLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.orFolderLabel.setObjectName("orFolderLabel") + self.gctLayout.addWidget(self.orFolderLabel, 1, 0, 1, 2) + + # gctFolder button to open file + self.gctFolderButton = QPushButton(self.centerWidget) + self.gctFolderButton.setMinimumSize(QSize(100, 26)) + self.gctFolderButton.setMaximumSize(QSize(100, 26)) + font = QFont("Helvetica") + font.setPointSize(10) + self.gctFolderButton.setFont(font) + self.gctFolderButton.setCheckable(False) + self.gctFolderButton.setChecked(False) + self.gctFolderButton.setAutoDefault(True) + self.gctFolderButton.setDefault(False) + self.gctFolderButton.setFlat(False) + self.gctFolderButton.setObjectName("gctFolderButton") + self.gctLayout.addWidget(self.gctFolderButton, 2, 0, 1, 1) + + # gctFolder path textbox + self.gctFolderTextBox = QLineEdit(self.centerWidget) + self.gctFolderTextBox.setEnabled(False) + self.gctFolderTextBox.setMinimumSize(QSize(200, 24)) + self.gctFolderTextBox.setMaximumSize(QSize(16777215, 24)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(10) + font.setWeight(42) + self.gctFolderTextBox.setFont(font) + self.gctFolderTextBox.setText("") + self.gctFolderTextBox.setMaxLength(255) + self.gctFolderTextBox.setFrame(True) + self.gctFolderTextBox.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) + self.gctFolderTextBox.setObjectName("gctFolderTextBox") + self.gctLayout.addWidget(self.gctFolderTextBox, 2, 1, 1, 1) + + # horizontal separater dest + self.horiSepDest = QFrame(self.centerWidget) + self.horiSepDest.setMinimumSize(QSize(474, 30)) + self.horiSepDest.setContentsMargins(20, 0, 20, 0) + self.horiSepDest.setFrameShape(QFrame.HLine) + self.horiSepDest.setFrameShadow(QFrame.Sunken) + self.horiSepDest.setObjectName("horiSepDest") + + # Dest button to open file + self.destButton = QPushButton(self.centerWidget) + self.destButton.setMinimumSize(QSize(100, 26)) + self.destButton.setMaximumSize(QSize(100, 26)) + font = QFont("Helvetica") + font.setPointSize(11) + self.destButton.setFont(font) + self.destButton.setCheckable(False) + self.destButton.setChecked(False) + self.destButton.setAutoDefault(True) + self.destButton.setDefault(False) + self.destButton.setFlat(False) + self.destButton.setObjectName("destButton") + self.destLayout.addWidget(self.destButton, 0, 0, 1, 1) + + # Dest path textbox + self.destTextBox = QLineEdit(self.centerWidget) + self.destTextBox.setEnabled(False) + self.destTextBox.setMinimumSize(QSize(200, 24)) + self.destTextBox.setMaximumSize(QSize(16777215, 24)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(10) + font.setWeight(42) + self.destTextBox.setFont(font) + self.destTextBox.setText("") + self.destTextBox.setMaxLength(255) + self.destTextBox.setFrame(True) + self.destTextBox.setAlignment( + Qt.AlignLeading | Qt.AlignCenter | Qt.AlignVCenter) + self.destTextBox.setObjectName("destTextBox") + self.destLayout.addWidget(self.destTextBox, 0, 1, 1, 1) + + self.filesLayout.addLayout(self.dolLayout, 0, 0, 1, 1) + self.filesLayout.addWidget(self.horiSepFiles, 1, 0, 1, 1) + self.filesLayout.addLayout(self.gctLayout, 2, 0, 1, 1) + self.filesLayout.addWidget(self.horiSepDest, 3, 0, 1, 1) + self.filesLayout.addLayout(self.destLayout, 4, 0, 1, 1) + + # Options Layout + self.optionsLayout = QGridLayout() + self.optionsLayout.setHorizontalSpacing(20) + self.optionsLayout.setObjectName("optionsLayout") + + # Options Label + self.optionsLabel = QLabel(self.centerWidget) + self.optionsLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.optionsLabel.sizePolicy().hasHeightForWidth()) + self.optionsLabel.setSizePolicy(sizePolicy) + self.optionsLabel.setMinimumSize(QSize(79, 23)) + self.optionsLabel.setMaximumSize(QSize(16777215, 23)) + font = QFont("Helvetica") + font.setPointSize(18) + font.setWeight(82) + font.setBold(True) + self.optionsLabel.setFont(font) + self.optionsLabel.setTextFormat(Qt.PlainText) + self.optionsLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.optionsLabel.setObjectName("optionsLabel") + self.optionsLayout.addWidget(self.optionsLabel, 0, 0, 1, 4) + + # Allocation Label + self.allocLabel = QLabel(self.centerWidget) + self.allocLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.allocLabel.sizePolicy().hasHeightForWidth()) + self.allocLabel.setSizePolicy(sizePolicy) + self.allocLabel.setMinimumSize(QSize(79, 23)) + self.allocLabel.setMaximumSize(QSize(16777215, 23)) + self.allocLabel.setTextFormat(Qt.PlainText) + self.allocLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.allocLabel.setObjectName("allocLabel") + self.optionsLayout.addWidget(self.allocLabel, 1, 0, 1, 1) + + # Allocation Textbox + self.allocLineEdit = QLineEdit(self.centerWidget) + self.allocLineEdit.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.allocLineEdit.sizePolicy().hasHeightForWidth()) + self.allocLineEdit.setSizePolicy(sizePolicy) + self.allocLineEdit.setMinimumSize(QSize(79, 23)) + self.allocLineEdit.setMaximumSize(QSize(79, 23)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(12) + font.setWeight(42) + self.allocLineEdit.setFont(font) + self.allocLineEdit.setText("") + self.allocLineEdit.setMaxLength(6) + self.allocLineEdit.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.allocLineEdit.setObjectName("allocLineEdit") + self.optionsLayout.addWidget(self.allocLineEdit, 2, 0, 1, 1) + + # handlerType label + self.handlerTypeLabel = QLabel(self.centerWidget) + self.handlerTypeLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.handlerTypeLabel.sizePolicy().hasHeightForWidth()) + self.handlerTypeLabel.setSizePolicy(sizePolicy) + self.handlerTypeLabel.setMinimumSize(QSize(79, 23)) + self.handlerTypeLabel.setMaximumSize(QSize(16777215, 23)) + self.handlerTypeLabel.setTextFormat(Qt.PlainText) + self.handlerTypeLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.handlerTypeLabel.setObjectName("handlerTypeLabel") + self.optionsLayout.addWidget(self.handlerTypeLabel, 1, 1, 1, 1) + + # handlerType selection + self.handlerTypeSelect = QComboBox(self.centerWidget) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.allocLabel.sizePolicy().hasHeightForWidth()) + self.handlerTypeSelect.setSizePolicy(sizePolicy) + self.handlerTypeSelect.setMinimumSize(QSize(79, 23)) + self.handlerTypeSelect.setMaximumSize(QSize(79, 23)) + self.handlerTypeSelect.setObjectName("handlerTypeSelect") + self.handlerTypeSelect.addItems(["FULL", "MINI"]) + self.optionsLayout.addWidget(self.handlerTypeSelect, 2, 1, 1, 1) + + # hookType label + self.hookTypeLabel = QLabel(self.centerWidget) + self.hookTypeLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.hookTypeLabel.sizePolicy().hasHeightForWidth()) + self.hookTypeLabel.setSizePolicy(sizePolicy) + self.hookTypeLabel.setMinimumSize(QSize(79, 23)) + self.hookTypeLabel.setMaximumSize(QSize(16777215, 23)) + self.hookTypeLabel.setTextFormat(Qt.PlainText) + self.hookTypeLabel.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.hookTypeLabel.setObjectName("hookTypeLabel") + self.optionsLayout.addWidget(self.hookTypeLabel, 1, 2, 1, 1) + + # hookType selection + self.hookTypeSelect = QComboBox(self.centerWidget) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.allocLabel.sizePolicy().hasHeightForWidth()) + self.hookTypeSelect.setSizePolicy(sizePolicy) + self.hookTypeSelect.setMinimumSize(QSize(79, 23)) + self.hookTypeSelect.setMaximumSize(QSize(79, 23)) + self.hookTypeSelect.setObjectName("hookTypeSelect") + self.hookTypeSelect.addItems(["VI", "GX", "PAD"]) + self.optionsLayout.addWidget(self.hookTypeSelect, 2, 2, 1, 1) + + # txtCodesInclude label + self.txtCodesIncludeLabel = QLabel(self.centerWidget) + self.txtCodesIncludeLabel.setEnabled(False) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.txtCodesIncludeLabel.sizePolicy().hasHeightForWidth()) + self.txtCodesIncludeLabel.setSizePolicy(sizePolicy) + self.txtCodesIncludeLabel.setMinimumSize(QSize(79, 23)) + self.txtCodesIncludeLabel.setMaximumSize(QSize(16777215, 23)) + self.txtCodesIncludeLabel.setTextFormat(Qt.PlainText) + self.txtCodesIncludeLabel.setAlignment( + Qt.AlignCenter | Qt.AlignVCenter) + self.txtCodesIncludeLabel.setObjectName("txtCodesIncludeLabel") + self.optionsLayout.addWidget(self.txtCodesIncludeLabel, 1, 3, 1, 1) + + # txtCodesInclude selection + self.txtCodesIncludeSelect = QComboBox(self.centerWidget) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.allocLabel.sizePolicy().hasHeightForWidth()) + self.txtCodesIncludeSelect.setSizePolicy(sizePolicy) + self.txtCodesIncludeSelect.setMinimumSize(QSize(79, 23)) + self.txtCodesIncludeSelect.setMaximumSize(QSize(79, 23)) + self.txtCodesIncludeSelect.setObjectName("txtCodesIncludeSelect") + self.txtCodesIncludeSelect.addItems(["ACTIVE", "ALL"]) + self.optionsLayout.addWidget(self.txtCodesIncludeSelect, 2, 3, 1, 1) + + # horizontal separater options + self.horiSepOptions = QFrame(self.centerWidget) + self.horiSepOptions.setMinimumSize(QSize(300, 30)) + self.horiSepOptions.setContentsMargins(20, 0, 20, 0) + self.horiSepOptions.setFrameShape(QFrame.HLine) + self.horiSepOptions.setFrameShadow(QFrame.Sunken) + self.horiSepOptions.setObjectName("horiSepOptions") + self.optionsLayout.addWidget(self.horiSepOptions, 3, 0, 1, 4) + + # Advanced options button + self.exOptionsButton = QPushButton(self.centerWidget) + font = QFont("Helvetica") + font.setPointSize(13) + self.exOptionsButton.setFont(font) + self.exOptionsButton.setCheckable(False) + self.exOptionsButton.setChecked(False) + self.exOptionsButton.setAutoDefault(True) + self.exOptionsButton.setDefault(False) + self.exOptionsButton.setFlat(False) + self.exOptionsButton.setDisabled(True) + self.exOptionsButton.setObjectName("exOptionsButton") + self.optionsLayout.addWidget(self.exOptionsButton, 4, 0, 1, 4) + + # horizontal separater 1 + self.horiSepA = QFrame(self.centerWidget) + self.horiSepA.setMinimumSize(QSize(470, 30)) + self.horiSepA.setFrameShape(QFrame.HLine) + self.horiSepA.setFrameShadow(QFrame.Sunken) + self.horiSepA.setObjectName("horiSepA") + + # horizontal separater 2 + self.horiSepB = QFrame(self.centerWidget) + self.horiSepB.setMinimumSize(QSize(470, 30)) + self.horiSepB.setFrameShape(QFrame.HLine) + self.horiSepB.setFrameShadow(QFrame.Sunken) + self.horiSepB.setObjectName("horiSepB") + + # response panel + self.responses = QPlainTextEdit(self.centerWidget) + self.responses.setEnabled(True) + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.responses.sizePolicy().hasHeightForWidth()) + self.responses.setSizePolicy(sizePolicy) + self.responses.setMinimumSize(QSize(474, 180)) + self.responses.setMaximumSize(QSize(16777215, 180)) + font = QFont() + font.setFamily("Consolas") + font.setPointSize(8) + font.setWeight(42) + fontMetrics = QFontMetricsF(font) + spaceWidth = fontMetrics.width(' ') + self.responses.setFont(font) + self.responses.setPlainText("") + self.responses.setTabStopDistance(spaceWidth * 4) + self.responses.setReadOnly(True) + self.responses.setObjectName("responses") + + # Compile button + self.compileButton = QPushButton(self.centerWidget) + font = QFont("Helvetica") + font.setPointSize(34) + self.compileButton.setFont(font) + self.compileButton.setCheckable(False) + self.compileButton.setChecked(False) + self.compileButton.setAutoDefault(True) + self.compileButton.setDefault(False) + self.compileButton.setFlat(False) + self.compileButton.setDisabled(True) + self.compileButton.setObjectName("compileButton") + + self.gridLayout.addWidget(self.filesLabel, 0, 0, 1, 1) + self.gridLayout.addLayout(self.filesLayout, 1, 0, 1, 1) + self.gridLayout.addWidget(self.horiSepA, 2, 0, 1, 1) + self.gridLayout.addLayout(self.optionsLayout, 3, 0, 1, 1) + self.gridLayout.addWidget(self.horiSepB, 4, 0, 1, 1) + self.gridLayout.addWidget(self.responses, 5, 0, 1, 1) + self.gridLayout.addWidget(self.compileButton, 6, 0, 1, 1) + + self.setCentralWidget(self.centerWidget) + + # Toolbar + self.menubar = QMenuBar(self) + self.menubar.setGeometry(QRect(0, 0, 470, 22)) + self.menubar.setObjectName("menubar") + + self.menuFile = QMenu(self.menubar) + font = QFont() + font.setFamily("Helvetica") + self.menuFile.setFont(font) + self.menuFile.setObjectName("menuFile") + + self.menuEdit = QMenu(self.menubar) + font = QFont() + font.setFamily("Helvetica") + self.menuEdit.setFont(font) + self.menuEdit.setObjectName("menuEdit") + + self.menuHelp = QMenu(self.menubar) + font = QFont() + font.setFamily("Helvetica") + self.menuHelp.setFont(font) + self.menuHelp.setObjectName("menuHelp") + + self.setMenuBar(self.menubar) + + self.actionOpen = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionOpen.setFont(font) + self.actionOpen.setObjectName("actionOpen") + + self.actionClose = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionClose.setFont(font) + self.actionClose.setObjectName("actionClose") + + self.actionSave = QAction(self) + self.actionSave.setEnabled(False) + font = QFont() + font.setFamily("Helvetica") + self.actionSave.setFont(font) + self.actionSave.setObjectName("actionSave") + + self.actionSave_As = QAction(self) + self.actionSave_As.setEnabled(False) + font = QFont() + font.setFamily("Helvetica") + self.actionSave_As.setFont(font) + self.actionSave_As.setObjectName("actionSave_As") + + self.actionUndo = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionUndo.setFont(font) + self.actionUndo.setMenuRole(QAction.TextHeuristicRole) + self.actionUndo.setObjectName("actionUndo") + self.actionRedo = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionRedo.setFont(font) + self.actionRedo.setObjectName("actionRedo") + self.actionCut = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionCut.setFont(font) + self.actionCut.setObjectName("actionCut") + self.actionCopy = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionCopy.setFont(font) + self.actionCopy.setObjectName("actionCopy") + self.actionPaste = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionPaste.setFont(font) + self.actionPaste.setObjectName("actionPaste") + self.actionDelete = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionDelete.setFont(font) + self.actionDelete.setObjectName("actionDelete") + self.actionSelect_All = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionSelect_All.setFont(font) + self.actionSelect_All.setObjectName("actionSelect_All") + self.actionPreferences = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionPreferences.setFont(font) + self.actionPreferences.setMenuRole(QAction.PreferencesRole) + self.actionPreferences.setObjectName("actionPreferences") + + self.actionAbout_GeckoLoader = QAction(self) + font = QFont() + font.setFamily("Helvetica") + self.actionAbout_GeckoLoader.setFont(font) + self.actionAbout_GeckoLoader.setMenuRole(QAction.AboutRole) + self.actionAbout_GeckoLoader.setObjectName("actionAbout_GeckoLoader") + + self.actionAbout_Qt = QAction(self) + self.actionAbout_Qt.setStatusTip("") + font = QFont() + font.setFamily("Helvetica") + self.actionAbout_Qt.setFont(font) + self.actionAbout_Qt.setMenuRole(QAction.AboutQtRole) + self.actionAbout_Qt.setObjectName("actionAbout_Qt") + + self.actionCheck_Update = QAction(self) + self.actionCheck_Update.setStatusTip("") + font = QFont() + font.setFamily("Helvetica") + self.actionCheck_Update.setFont(font) + self.actionCheck_Update.setObjectName("actionCheck_Update") + + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionClose) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionSave) + self.menuFile.addAction(self.actionSave_As) + + self.menuEdit.addAction(self.actionPreferences) + + self.menuHelp.addAction(self.actionAbout_GeckoLoader) + self.menuHelp.addAction(self.actionAbout_Qt) + self.menuHelp.addAction(self.actionCheck_Update) + + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuEdit.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) + + # Statusbar + self.statusbar = QStatusBar(self) + self.statusbar.setObjectName("statusbar") + self.setStatusBar(self.statusbar) + + self.retranslate_ui() + self.set_edit_fields() + + QMetaObject.connectSlotsByName(self) + + def set_edit_fields(self): + self.filesLabel.setEnabled(True) + self.dolTextBox.setEnabled(True) + self.destTextBox.setEnabled(True) + self.optionsLabel.setEnabled(True) + self.allocLabel.setEnabled(True) + self.allocLineEdit.setEnabled(True) + self.handlerTypeLabel.setEnabled(True) + self.handlerTypeSelect.setEnabled(True) + self.hookTypeLabel.setEnabled(True) + self.hookTypeSelect.setEnabled(True) + self.txtCodesIncludeLabel.setEnabled(True) + self.txtCodesIncludeSelect.setEnabled(True) + self.exOptionsButton.setEnabled(True) + self.actionSave.setEnabled(True) + self.actionSave_As.setEnabled(True) + + self._lstrip_textboxes() + + if self.gctFileTextBox.text() != "": + self.gctFileTextBox.setEnabled(True) + self.gctFolderTextBox.setDisabled(True) + elif self.gctFolderTextBox.text() != "": + self.gctFileTextBox.setDisabled(True) + self.gctFolderTextBox.setEnabled(True) + else: + self.gctFileTextBox.setEnabled(True) + self.gctFolderTextBox.setEnabled(True) + + if self.dolTextBox.text().lower().endswith(".dol") and len(self.dolTextBox.text()) > 4: + self.compileButton.setEnabled( + self.gctFileTextBox.text() != "" or self.gctFolderTextBox.text() != "") + else: + self.compileButton.setDisabled(True) + + def retranslate_ui(self): + self.setWindowTitle(QApplication.translate( + "MainWindow", f"GeckoLoader {self.apiRevision} - untitled", None)) + self.menuFile.setTitle( + QApplication.translate("MainWindow", "&File", None)) + self.menuEdit.setTitle( + QApplication.translate("MainWindow", "&Edit", None)) + self.menuHelp.setTitle( + QApplication.translate("MainWindow", "&Help", None)) + self.actionOpen.setText(QApplication.translate( + "MainWindow", "&Open Session...", None)) + self.actionOpen.setStatusTip(QApplication.translate( + "MainWindow", "Open a session", None)) + self.actionOpen.setShortcut( + QApplication.translate("MainWindow", "Ctrl+O", None)) + self.actionClose.setText(QApplication.translate( + "MainWindow", "&Close Session...", None)) + self.actionClose.setStatusTip(QApplication.translate( + "MainWindow", "Close the current session", None)) + self.actionClose.setShortcut(QApplication.translate( + "MainWindow", "Ctrl+Shift+C", None)) + self.actionSave.setText(QApplication.translate( + "MainWindow", "&Save Session", None)) + self.actionSave.setStatusTip(QApplication.translate( + "MainWindow", "Save the current session", None)) + self.actionSave.setShortcut( + QApplication.translate("MainWindow", "Ctrl+S", None)) + self.actionSave_As.setText(QApplication.translate( + "MainWindow", "&Save Session As...", None)) + self.actionSave_As.setStatusTip(QApplication.translate( + "MainWindow", "Save the current session to the specified location", None)) + self.actionSave_As.setShortcut( + QApplication.translate("MainWindow", "Ctrl+Shift+S", None)) + self.actionUndo.setText( + QApplication.translate("MainWindow", "Undo", None)) + self.actionUndo.setStatusTip(QApplication.translate( + "MainWindow", "Undo the last action", None)) + self.actionUndo.setShortcut( + QApplication.translate("MainWindow", "Ctrl+Z", None)) + self.actionRedo.setText( + QApplication.translate("MainWindow", "Redo", None)) + self.actionRedo.setStatusTip(QApplication.translate( + "MainWindow", "Redo the last action", None)) + self.actionRedo.setShortcut(QApplication.translate( + "MainWindow", "Ctrl+Shift+Z", None)) + self.actionCut.setText( + QApplication.translate("MainWindow", "Cut", None)) + self.actionCut.setStatusTip(QApplication.translate( + "MainWindow", "Cuts the selected text and places it on the clipboard", None)) + self.actionCut.setShortcut( + QApplication.translate("MainWindow", "Ctrl+X", None)) + self.actionCopy.setText( + QApplication.translate("MainWindow", "Copy", None)) + self.actionCopy.setStatusTip(QApplication.translate( + "MainWindow", "Copies the selected text and places it on the clipboard", None)) + self.actionCopy.setShortcut( + QApplication.translate("MainWindow", "Ctrl+C", None)) + self.actionPaste.setText( + QApplication.translate("MainWindow", "Paste", None)) + self.actionPaste.setStatusTip(QApplication.translate( + "MainWindow", "Paste the contents of the clipboard", None)) + self.actionPaste.setShortcut( + QApplication.translate("MainWindow", "Ctrl+V", None)) + self.actionDelete.setText( + QApplication.translate("MainWindow", "Delete", None)) + self.actionDelete.setStatusTip(QApplication.translate( + "MainWindow", "Deletes the selected text", None)) + self.actionSelect_All.setText( + QApplication.translate("MainWindow", "Select All", None)) + self.actionSelect_All.setStatusTip(QApplication.translate( + "MainWindow", "Select all of the text", None)) + self.actionSelect_All.setShortcut( + QApplication.translate("MainWindow", "Ctrl+A", None)) + self.actionPreferences.setText(QApplication.translate( + "MainWindow", "&Preferences...", None)) + self.actionPreferences.setStatusTip(QApplication.translate( + "MainWindow", "Open the application preferences dialog", None)) + self.actionAbout_GeckoLoader.setText(QApplication.translate( + "MainWindow", "About &GeckoLoader...", None)) + self.actionAbout_Qt.setText(QApplication.translate( + "MainWindow", "About &Qt...", None)) + self.actionCheck_Update.setText( + QApplication.translate("MainWindow", "&Check Update", None)) + + self.filesLabel.setText( + QApplication.translate("MainWindow", "Files", None)) + + self.dolButton.setText(QApplication.translate( + "MainWindow", "Open DOL", None)) + self.gctFileButton.setText(QApplication.translate( + "MainWindow", "Open Codes", None)) + self.orFolderLabel.setText(QApplication.translate( + "MainWindow", "-"*40 + "OR" + "-"*40, None)) + self.gctFolderButton.setText( + QApplication.translate("MainWindow", "Open Folder", None)) + self.destButton.setText(QApplication.translate( + "MainWindow", "Destination", None)) + + self.optionsLabel.setText( + QApplication.translate("MainWindow", "Options", None)) + + self.allocLabel.setText(QApplication.translate( + "MainWindow", "Allocation", None)) + self.allocLineEdit.setPlaceholderText( + QApplication.translate("MainWindow", "AUTO", None)) + + self.handlerTypeLabel.setText( + QApplication.translate("MainWindow", "Codehandler", None)) + self.handlerTypeSelect.setItemText( + 0, QApplication.translate("Dialog", "FULL", None)) + self.handlerTypeSelect.setItemText( + 1, QApplication.translate("Dialog", "MINI", None)) + + self.hookTypeLabel.setText( + QApplication.translate("MainWindow", "Code Hook", None)) + self.hookTypeSelect.setItemText( + 0, QApplication.translate("Dialog", "VI", None)) + self.hookTypeSelect.setItemText( + 1, QApplication.translate("Dialog", "GX", None)) + self.hookTypeSelect.setItemText( + 2, QApplication.translate("Dialog", "PAD", None)) + + self.txtCodesIncludeLabel.setText( + QApplication.translate("MainWindow", "Include Codes", None)) + self.txtCodesIncludeSelect.setItemText( + 0, QApplication.translate("Dialog", "ACTIVE", None)) + self.txtCodesIncludeSelect.setItemText( + 1, QApplication.translate("Dialog", "ALL", None)) + + self.exOptionsButton.setText(QApplication.translate( + "MainWindow", "Advanced Settings", None)) + + self.compileButton.setText( + QApplication.translate("MainWindow", "RUN", None)) + + def _lstrip_textboxes(self): + attributes = [item for item in vars(self) if not item.startswith('__')] + + for item in attributes: + item = getattr(self, item) + if isinstance(item, QLineEdit): + strlength = len(item.text()) + cursorPos = item.cursorPosition() + item.setText(item.text().lstrip()) + item.setCursorPosition( + cursorPos - (strlength - len(item.text()))) + elif isinstance(item, QPlainTextEdit): + sliderPos = item.verticalScrollBar().sliderPosition() + item.setPlainText(item.toPlainText().lstrip()) + item.verticalScrollBar().setSliderPosition(sliderPos) diff --git a/tools.py b/geckoloader/tools.py similarity index 84% rename from tools.py rename to geckoloader/tools.py index ba4f3b1..acd16c0 100644 --- a/tools.py +++ b/geckoloader/tools.py @@ -1,8 +1,7 @@ -import struct import sys -import os from io import IOBase from argparse import ArgumentParser +from typing import Dict, Optional try: import colorama @@ -54,6 +53,7 @@ def align_byte_size(obj, alignment: int, fillchar="00"): else: raise NotImplementedError(f"Aligning the size of class {type(obj)} is unsupported") +""" def color_text(text: str, textToColor: list=[("", None)], defaultColor: str=None) -> str: currentColor = None formattedText = "" @@ -92,9 +92,28 @@ def color_text(text: str, textToColor: list=[("", None)], defaultColor: str=None formattedText += char return formattedText + TRESET +""" + +def color_text(text: str, textToColor: Optional[Dict[str, str]] = None, defaultColor: Optional[str] = "") -> str: + currentColor = None + formattedText = "" + + if textToColor is None: + return f"{defaultColor}{text}{TRESET}" + + for char in text: + if char in textToColor: + newColor = textToColor[char] + if currentColor != newColor: + formattedText += TRESET + formattedText += newColor + currentColor = newColor + formattedText += char + + return formattedText + class CommandLineParser(ArgumentParser): - def error(self, message: str, prefix: str=None, print_usage=True, exit=True): if print_usage: self.print_usage(sys.stderr) diff --git a/versioncheck.py b/geckoloader/versioncheck.py similarity index 100% rename from versioncheck.py rename to geckoloader/versioncheck.py diff --git a/kernel.py b/kernel.py deleted file mode 100644 index 9e0f8e7..0000000 --- a/kernel.py +++ /dev/null @@ -1,665 +0,0 @@ -import functools -import random -import re -import sys -import time -from io import BytesIO -from pathlib import Path - -import tools -from dolreader import DolFile, SectionCountFullError, UnmappedAddressError -from fileutils import (get_alignment, read_uint16, read_uint32, write_bool, - write_sint32, write_ubyte, write_uint16, write_uint32) - -try: - import chardet -except ImportError as IE: - print(IE) - sys.exit(1) - -def timer(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - start = time.perf_counter() - value = func(*args, **kwargs) - end = time.perf_counter() - print(tools.color_text(f"\n :: Completed in {(end - start):0.4f} seconds!\n", defaultColor=tools.TGREENLIT)) - return value - return wrapper - -class InvalidGeckoCodeError(Exception): pass - -class GCT(object): - - def __init__(self, f): - self.codeList = BytesIO(f.read()) - self.rawLineCount = tools.stream_size(self.codeList) >> 3 - self.lineCount = self.rawLineCount - 2 - f.seek(0) - - @property - def size(self): - return len(self.codeList.getbuffer()) - - @staticmethod - def determine_codelength(codetype, info: bytes) -> int: - if codetype.startswith(b"\x06"): - bytelength = int.from_bytes(info, byteorder="big", signed=False) - padding = get_alignment(bytelength, 8) - return 0x8 + bytelength + padding - - elif (codetype.startswith(b"\x08") or codetype.startswith(b"\x09") - or codetype.startswith(b"\x18") or codetype.startswith(b"\x18")): - return 0x16 - - elif (codetype.startswith(b"\xC0") or codetype.startswith(b"\xC2") or codetype.startswith(b"\xC4") - or codetype.startswith(b"\xC3") or codetype.startswith(b"\xC5") or codetype.startswith(b"\xD2") - or codetype.startswith(b"\xD4") or codetype.startswith(b"\xD3") or codetype.startswith(b"\xD5")): - return 0x8 + (int.from_bytes(info, byteorder="big", signed=False) << 3) - - elif (codetype.startswith(b"\xF2") or codetype.startswith(b"\xF3") - or codetype.startswith(b"\xF4") or codetype.startswith(b"\xF5")): - return 0x8 + (int.from_bytes(info[:2], byteorder="big", signed=False) << 3) - - elif codetype.startswith(b"\xF6"): - return 0x8 + (int.from_bytes(info[:4], byteorder="big", signed=False) << 3) - - else: - return 0x8 - - def optimize_codelist(self, dolFile: DolFile): - codelist = b"\x00\xD0\xC0\xDE"*2 - skipcodes = 0 - - self.codeList.seek(8) - while codetype := self.codeList.read(4): - info = self.codeList.read(4) - address = 0x80000000 | (int.from_bytes(codetype, byteorder="big", signed=False) & 0x1FFFFFF) - try: - if skipcodes <= 0: - if (codetype.startswith(b"\x00") or codetype.startswith(b"\x01") - or codetype.startswith(b"\x10") or codetype.startswith(b"\x11")): - dolFile.seek(address) - - counter = int.from_bytes(info[:-2], byteorder="big", signed=False) - value = info[2:] - - while counter + 1 > 0: - dolFile.write(value[1:]) - counter -= 1 - continue - - elif (codetype.startswith(b"\x02") or codetype.startswith(b"\x03") - or codetype.startswith(b"\x12") or codetype.startswith(b"\x13")): - dolFile.seek(address) - - counter = int.from_bytes(info[:-2], byteorder="big", signed=False) - value = info[2:] - - while counter + 1 > 0: - dolFile.write(value) - counter -= 1 - continue - - elif (codetype.startswith(b"\x04") or codetype.startswith(b"\x05") - or codetype.startswith(b"\x14") or codetype.startswith(b"\x15")): - dolFile.seek(address) - dolFile.write(info) - continue - - elif (codetype.startswith(b"\x06") or codetype.startswith(b"\x07") - or codetype.startswith(b"\x16") or codetype.startswith(b"\x17")): - dolFile.seek(address) - - arraylength = int.from_bytes(info, byteorder="big", signed=False) - padding = get_alignment(arraylength, 8) - - dolFile.write(self.codeList.read(arraylength)) - - self.codeList.seek(padding, 1) - continue - - elif (codetype.startswith(b"\x08") or codetype.startswith(b"\x09") - or codetype.startswith(b"\x18") or codetype.startswith(b"\x19")): - dolFile.seek(address) - - value = int.from_bytes(info, byteorder="big", signed=False) - data = read_uint16(self.codeList) - size = (data & 0x3000) >> 12 - counter = data & 0xFFF - address_increment = read_uint16(self.codeList) - value_increment = read_uint32(self.codeList) - - while counter + 1 > 0: - if size == 0: - write_ubyte(dolFile, value & 0xFF) - dolFile.seek(-1, 1) - elif size == 1: - write_uint16(dolFile, value & 0xFFFF) - dolFile.seek(-2, 1) - elif size == 2: - write_uint32(dolFile, value) - dolFile.seek(-4, 1) - else: - raise ValueError("Size type {} does not match 08 codetype specs".format(size)) - - dolFile.seek(address_increment, 1) - value += value_increment - counter -= 1 - if value > 0xFFFFFFFF: - value -= 0x100000000 - continue - - elif (codetype.startswith(b"\xC6") or codetype.startswith(b"\xC7") - or codetype.startswith(b"\xC6") or codetype.startswith(b"\xC7")): - dolFile.insert_branch(int.from_bytes(info, byteorder="big", signed=False), address, lk=address&1) - continue - - if codetype.hex().startswith("2") or codetype.hex().startswith("3"): - skipcodes += 1 - - elif codetype.startswith(b"\xE0"): - skipcodes -= 1 - - elif codetype.startswith(b"\xF0"): - codelist += b"\xF0\x00\x00\x00\x00\x00\x00\x00" - break - - self.codeList.seek(-8, 1) - codelist += self.codeList.read(GCT.determine_codelength(codetype, info)) - - except (RuntimeError, UnmappedAddressError): - self.codeList.seek(-8, 1) - codelist += self.codeList.read(GCT.determine_codelength(codetype, info)) - - self.codeList = BytesIO(codelist) - -class CodeHandler(object): - - class Types: - MINI = "MINI" - FULL = "FULL" - - def __init__(self, f): - self._rawData = BytesIO(f.read()) - - """Get codelist pointer""" - self._rawData.seek(0xFA) - codelistUpper = self._rawData.read(2).hex() - self._rawData.seek(0xFE) - codelistLower = self._rawData.read(2).hex() - - self.codeListPointer = int(codelistUpper[2:] + codelistLower[2:], 16) - self.handlerLength = tools.stream_size(self._rawData) - self.initAddress = 0x80001800 - self.startAddress = 0x800018A8 - - self.wiiVIHook = b"\x7C\xE3\x3B\x78\x38\x87\x00\x34\x38\xA7\x00\x38\x38\xC7\x00\x4C" - self.gcnVIHook = b"\x7C\x03\x00\x34\x38\x83\x00\x20\x54\x85\x08\x3C\x7C\x7F\x2A\x14\xA0\x03\x00\x00\x7C\x7D\x2A\x14\x20\xA4\x00\x3F\xB0\x03\x00\x00" - self.wiiGXDrawHook = b"\x3C\xA0\xCC\x01\x38\x00\x00\x61\x3C\x80\x45\x00\x98\x05\x80\x00" - self.gcnGXDrawHook = b"\x38\x00\x00\x61\x3C\xA0\xCC\x01\x3C\x80\x45\x00\x98\x05\x80\x00" - self.wiiPADHook = b"\x3A\xB5\x00\x01\x3A\x73\x00\x0C\x2C\x15\x00\x04\x3B\x18\x00\x0C" - self.gcnPADHook = b"\x3A\xB5\x00\x01\x2C\x15\x00\x04\x3B\x18\x00\x0C\x3B\xFF\x00\x0C" - - self.allocation = None - self.hookAddress = None - self.hookType = None - self.geckoCodes = None - self.includeAll = False - self.optimizeList = False - - if self.handlerLength < 0x900: - self.type = CodeHandler.Types.MINI - else: - self.type = CodeHandler.Types.FULL - - f.seek(0) - - def init_gct(self, gctFile: Path, tmpdir: Path=None): - if tmpdir is not None: - _tmpGct = tmpdir / "gct.bin" - else: - _tmpGct = Path("gct.bin") - - if gctFile.suffix.lower() == ".txt": - with _tmpGct.open("wb+") as temp: - temp.write(bytes.fromhex("00D0C0DE"*2 + self.parse_input(gctFile) + "F000000000000000")) - temp.seek(0) - self.geckoCodes = GCT(temp) - elif gctFile.suffix.lower() == ".gct": - with gctFile.open("rb") as gct: - self.geckoCodes = GCT(gct) - elif gctFile.suffix == "": - with _tmpGct.open("wb+") as temp: - temp.write(b"\x00\xD0\xC0\xDE"*2) - - for file in gctFile.iterdir(): - if file.is_file(): - if file.suffix.lower() == ".txt": - temp.write(bytes.fromhex(self.parse_input(file))) - elif file.suffix.lower() == ".gct": - with file.open("rb") as gct: - temp.write(gct.read()[8:-8]) - else: - print(tools.color_text(f" :: HINT: {file} is not a .txt or .gct file", defaultColor=tools.TYELLOWLIT)) - - temp.write(b"\xF0\x00\x00\x00\x00\x00\x00\x00") - temp.seek(0) - self.geckoCodes = GCT(temp) - else: - raise NotImplementedError(f"Parsing file type `{gctFile.suffix}' as a GCT is unsupported") - - def parse_input(self, geckoText: Path) -> str: - with geckoText.open("rb") as gecko: - result = chardet.detect(gecko.read()) - encodeType = result["encoding"] - - with geckoText.open("r", encoding=encodeType) as gecko: - geckoCodes = "" - state = None - - for line in gecko.readlines(): - if line in ("", "\n"): - continue - - if state is None: - if line.startswith("$") or line.startswith("["): - state = "Dolphin" - else: - state = "OcarinaM" - - try: - if state == "OcarinaM": - if self.includeAll: - geckoLine = re.findall(r"[A-F0-9]{8}[\t\f ][A-F0-9]{8}", line, re.IGNORECASE)[0] - else: - geckoLine = re.findall(r"(?:\*\s*)([A-F0-9]{8}[\t\f ][A-F0-9]{8})", line, re.IGNORECASE)[0] - else: - geckoLine = re.findall(r"(? int: - b1 = key & 0xFF - b2 = (key >> 8) & 0xFF - b3 = (key >> 16) & 0xFF - b4 = (key >> 24) & 0xFF - b3 ^= b4 - b2 ^= b3 - b1 ^= b2 - return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 - - def encrypt_codes(self, key: int): - self.geckoCodes.codeList.seek(0) - i = 0 - while True: - try: - packet = read_uint32(self.geckoCodes.codeList) - self.geckoCodes.codeList.seek(-4, 1) - write_uint32(self.geckoCodes.codeList, (packet^key) & 0xFFFFFFFF) - key += (i << 3) & 0xFFFFFFFF - if key > 0xFFFFFFFF: - key -= 0x100000000 - i += 1 - except: - break - - def find_variable_data(self, variable) -> int: - self._rawData.seek(0) - - while sample := self._rawData.read(4): - if sample == variable: - return self._rawData.tell() - 4 - - return None - - def set_hook_instruction(self, dolFile: DolFile, address: int, varOffset: int, lk=0): - self._rawData.seek(varOffset) - dolFile.seek(address) - ppc = read_uint32(dolFile) - - if ((((ppc >> 24) & 0xFF) > 0x47 and ((ppc >> 24) & 0xFF) < 0x4C) or (((ppc >> 24) & 0xFF) > 0x3F and ((ppc >> 24) & 0xFF) < 0x44)): - to, conditional = dolFile.extract_branch_addr(address) - if conditional: - raise NotImplementedError("Hooking to a conditional non spr branch is unsupported") - write_uint32(self._rawData, (to - (self.initAddress + varOffset)) & 0x3FFFFFD | 0x48000000 | lk) - else: - write_uint32(self._rawData, ppc) - - def set_variables(self, dolFile: DolFile): - varOffset = self.find_variable_data(b"\x00\xDE\xDE\xDE") - if varOffset is None: - raise RuntimeError(tools.color_text("Variable codehandler data not found\n", defaultColor=tools.TREDLIT)) - - self.set_hook_instruction(dolFile, self.hookAddress, varOffset, 0) - - self._rawData.seek(varOffset + 4) - write_uint32(self._rawData, ((self.hookAddress + 4) - (self.initAddress + (varOffset + 4))) & 0x3FFFFFD | 0x48000000 | 0) - -class KernelLoader(object): - - def __init__(self, f, cli: tools.CommandLineParser=None): - self._rawData = BytesIO(f.read()) - self._initDataList = None - self._gpModDataList = None - self._gpDiscDataList = None - self._gpKeyAddrList = None - self._cli = cli - self.initAddress = None - self.protect = False - self.verbosity = 0 - self.quiet = False - self.encrypt = False - - def error(self, msg: str): - if self._cli is not None: - self._cli.error(msg) - else: - print(msg) - sys.exit(1) - - def set_variables(self, entryPoint: list, baseOffset: int=0): - self._rawData.seek(0) - - if self._gpModDataList is None: - return - - while sample := self._rawData.read(2): - if sample == b"GH": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, self._gpModDataList[0]) - elif sample == b"GL": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, baseOffset + self._gpModDataList[1]) - elif sample == b"IH": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, entryPoint[0]) - elif sample == b"IL": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, entryPoint[1]) - elif sample == b"KH": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, self._gpKeyAddrList[0]) - elif sample == b"KL": - self._rawData.seek(-2, 1) - write_uint16(self._rawData, baseOffset + self._gpKeyAddrList[1]) - - def complete_data(self, codeHandler: CodeHandler, initpoint: list): - _upperAddr, _lowerAddr = ((self.initAddress >> 16) & 0xFFFF, self.initAddress & 0xFFFF) - _key = random.randrange(0x100000000) - self._rawData.seek(0) - - while sample := self._rawData.read(4): - if sample == b"HEAP": #Found keyword "HEAP". Goes with the resize of the heap - self._rawData.seek(-4, 1) - - gpModInfoOffset = self._rawData.tell() - gpModUpperAddr = _upperAddr + 1 if (_lowerAddr + gpModInfoOffset) > 0x7FFF else _upperAddr #Absolute addressing - - if codeHandler.allocation == None: - codeHandler.allocation = (codeHandler.handlerLength + codeHandler.geckoCodes.size + 7) & -8 - - write_uint32(self._rawData, codeHandler.allocation) - - elif sample == b"LSIZ": #Found keyword "LSIZ". Goes with the size of the loader - self._rawData.seek(-4, 1) - write_uint32(self._rawData, len(self._rawData.getbuffer())) - - elif sample == b"HSIZ": #Found keyword "HSIZ". Goes with the size of the codehandler - self._rawData.seek(-4, 1) - write_sint32(self._rawData, codeHandler.handlerLength) - - elif sample == b"CSIZ": #Found keyword "CSIZ". Goes with the size of the codes - self._rawData.seek(-4, 1) - write_sint32(self._rawData, codeHandler.geckoCodes.size) - - elif sample == b"HOOK": #Found keyword "HOOK". Goes with the codehandler hook - self._rawData.seek(-4, 1) - write_uint32(self._rawData, codeHandler.hookAddress) - - elif sample == b"CRPT": #Found keyword "CRPT". Boolean of the encryption - self._rawData.seek(-4, 1) - write_bool(self._rawData, self.encrypt, 4) - - elif sample == b"CYPT": #Found keyword "CYPT". Encryption Key - self._rawData.seek(-4, 1) - - gpKeyOffset = self._rawData.tell() - gpKeyUpperAddr = _upperAddr + 1 if (_lowerAddr + gpKeyOffset) > 0x7FFF else _upperAddr #Absolute addressing - - write_uint32(self._rawData, CodeHandler.encrypt_key(_key)) - - if _lowerAddr + gpModInfoOffset > 0xFFFF: - _lowerAddr -= 0x10000 - - self._gpModDataList = (gpModUpperAddr, gpModInfoOffset) - self._gpKeyAddrList = (gpKeyUpperAddr, gpKeyOffset) - - self.set_variables(initpoint, _lowerAddr) - - if self.encrypt: - codeHandler.encrypt_codes(_key) - - def patch_arena(self, codeHandler: CodeHandler, dolFile: DolFile) -> tuple: - self.complete_data(codeHandler, [(dolFile.entryPoint >> 16) & 0xFFFF, dolFile.entryPoint & 0xFFFF]) - - self._rawData.seek(0, 2) - self._rawData.write(codeHandler._rawData.getvalue() + codeHandler.geckoCodes.codeList.getvalue()) - - self._rawData.seek(0) - _kernelData = self._rawData.getvalue() - - try: - dolFile.append_text_sections([(_kernelData, self.initAddress)]) - except SectionCountFullError: - try: - dolFile.append_data_sections([(_kernelData, self.initAddress)]) - except SectionCountFullError: - self.error(tools.color_text("There are no unused sections left for GeckoLoader to use!\n", defaultColor=tools.TREDLIT)) - - dolFile.entryPoint = self.initAddress - return True, None - - def patch_legacy(self, codeHandler: CodeHandler, dolFile: DolFile) -> tuple: - codeHandler._rawData.seek(0) - codeHandler.geckoCodes.codeList.seek(0) - - _handlerData = codeHandler._rawData.getvalue() + codeHandler.geckoCodes.codeList.getvalue() - - try: - dolFile.append_text_sections([(_handlerData, codeHandler.initAddress)]) - except SectionCountFullError: - try: - dolFile.append_data_sections([(_handlerData, codeHandler.initAddress)]) - except SectionCountFullError: - self.error(tools.color_text("There are no unused sections left for GeckoLoader to use!\n", defaultColor=tools.TREDLIT)) - - return True, None - - def protect_game(self, codeHandler: CodeHandler): - _oldpos = codeHandler.geckoCodes.codeList.tell() - - protectdata = (b"\xC0\x00\x00\x00\x00\x00\x00\x17", - b"\x7C\x08\x02\xA6\x94\x21\xFF\x70", - b"\x90\x01\x00\x08\xBC\x61\x00\x0C", - b"\x48\x00\x00\x0D\x00\xD0\xC0\xDE", - b"\x00\xD0\xDE\xAD\x7F\xE8\x02\xA6", - b"\x3B\xDF\x00\x08\x3C\x60\x80\x00", - b"\x38\x80\x11\x00\x38\xA0\x00\x00", - b"\x60\x63\x1E\xF8\x7C\x89\x03\xA6", - b"\x38\x80\x00\x00\x7D\x03\x22\x14", - b"\x54\xE9\x06\x3E\x89\x08\x00\x08", - b"\x7D\x3F\x48\xAE\x38\xE7\x00\x01", - b"\x7C\x08\x48\x40\x41\x82\x00\x0C", - b"\x60\xA7\x00\x00\x48\x00\x00\x04", - b"\x54\xE8\x06\x3E\x28\x08\x00\x03", - b"\x41\x81\x00\x10\x38\x84\x00\x01", - b"\x42\x00\xFF\xCC\x48\x00\x00\x2C", - b"\x38\xA0\x00\x08\x7C\x84\x1A\x14", - b"\x7C\xA9\x03\xA6\x38\x60\x00\x00", - b"\x38\x84\xFF\xFF\x54\x66\x07\xBE", - b"\x7C\xDE\x30\xAE\x38\x63\x00\x01", - b"\x9C\xC4\x00\x01\x42\x00\xFF\xF0", - b"\xB8\x61\x00\x0C\x80\x01\x00\x08", - b"\x38\x21\x00\x90\x7C\x08\x03\xA6", - b"\x4E\x80\x00\x20\x00\x00\x00\x00") - - codeHandler.geckoCodes.codeList.seek(-8, 2) - - for line in protectdata: - codeHandler.geckoCodes.codeList.write(line) - - codeHandler.geckoCodes.codeList.write(b"\xF0\x00\x00\x00\x00\x00\x00\x00") - codeHandler.geckoCodes.codeList.seek(_oldpos) - - @timer - def build(self, gctFile: Path, dolFile: DolFile, codeHandler: CodeHandler, tmpdir: Path, dump: Path): - _oldStart = dolFile.entryPoint - - """Initialize our codes""" - - codeHandler.init_gct(gctFile, tmpdir) - - if codeHandler.geckoCodes is None: - self.error(tools.color_text("Valid codelist not found. Please provide a .txt/.gct file, or a folder of .txt/.gct files\n", defaultColor=tools.TREDLIT)) - - if self.protect: - self.protect_game(codeHandler) - - """Get entrypoint (or BSS midpoint) for insert""" - - if self.initAddress: - try: - dolFile.resolve_address(self.initAddress) - self.error(tools.color_text(f"Init address specified for GeckoLoader (0x{self.initAddress:X}) clobbers existing dol sections", defaultColor=tools.TREDLIT)) - except UnmappedAddressError: - pass - else: - self.initAddress = dolFile.seek_nearest_unmapped(dolFile.bssAddress, len(self._rawData.getbuffer()) + codeHandler.handlerLength + codeHandler.geckoCodes.size) - self._rawData.seek(0) - - if codeHandler.optimizeList: - codeHandler.geckoCodes.optimize_codelist(dolFile) - - """Is codelist optimized away?""" - - if codeHandler.geckoCodes.codeList.getvalue() == b"\x00\xD0\xC0\xDE\x00\xD0\xC0\xDE\xF0\x00\x00\x00\x00\x00\x00\x00": - with dump.open("wb") as final: - dolFile.save(final) - - if not self.quiet: - if self.verbosity >= 3: - dolFile.print_info() - print("-"*64) - if self.verbosity >= 1: - print(tools.color_text("\n :: All codes have been successfully pre patched", defaultColor=tools.TGREENLIT)) - return - - hooked = determine_codehook(dolFile, codeHandler, False) - if hooked: - _status, _msg = self.patch_arena(codeHandler, dolFile) - else: - self.error(tools.color_text("Failed to find a hook address. Try using option --codehook to use your own address\n", defaultColor=tools.TREDLIT)) - - if _status is False: - self.error(tools.color_text(_msg + "\n", defaultColor=tools.TREDLIT)) - elif codeHandler.allocation < codeHandler.geckoCodes.size: - self.error(tools.color_text("Allocated codespace was smaller than the given codelist\n", defaultColor=tools.TYELLOW)) - - with dump.open("wb") as final: - dolFile.save(final) - - if self.quiet: - return - - if self.verbosity >= 3: - dolFile.print_info() - print("-"*64) - - if self.verbosity >= 2: - print("") - info = [f" :: Start of game modified to address 0x{self.initAddress:X}", - f" :: Game function `__start()' located at address 0x{_oldStart:X}", - f" :: Allocation is 0x{codeHandler.allocation:X}; codelist size is 0x{codeHandler.geckoCodes.size:X}", - f" :: Codehandler hooked at 0x{codeHandler.hookAddress:X}", - f" :: Codehandler is of type `{codeHandler.type}'", - f" :: Of the {DolFile.maxTextSections} text sections in this DOL file, {len(dolFile.textSections)} are now being used", - f" :: Of the {DolFile.maxDataSections} text sections in this DOL file, {len(dolFile.dataSections)} are now being used"] - - for bit in info: - print(tools.color_text(bit, defaultColor=tools.TGREENLIT)) - - elif self.verbosity >= 1: - print("") - info = [f" :: GeckoLoader set at address 0x{self.initAddress:X}", - f" :: Legacy size is 0x{codeHandler.allocation:X}; codelist size is 0x{codeHandler.geckoCodes.size:X}", - f" :: Codehandler is of type `{codeHandler.type}'"] - for bit in info: - print(tools.color_text(bit, defaultColor=tools.TGREENLIT)) - -def determine_codehook(dolFile: DolFile, codeHandler: CodeHandler, hook=False) -> bool: - if codeHandler.hookAddress is None: - if not assert_code_hook(dolFile, codeHandler): - return False - - if hook: - codeHandler.set_variables(dolFile) - insert_code_hook(dolFile, codeHandler, codeHandler.hookAddress) - - return True - - -def assert_code_hook(dolFile: DolFile, codeHandler: CodeHandler) -> bool: - for section in dolFile.textSections: - dolFile.seek(section["address"]) - sample = dolFile.read(section["size"]) - - if codeHandler.hookType == "VI": - result = sample.find(codeHandler.gcnVIHook) - elif codeHandler.hookType == "GX": - result = sample.find(codeHandler.gcnGXDrawHook) - elif codeHandler.hookType == "PAD": - result = sample.find(codeHandler.gcnPADHook) - else: - raise NotImplementedError(tools.color_text(f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) - - if result >= 0: - dolFile.seek(section["address"] + result) - else: - if codeHandler.hookType == "VI": - result = sample.find(codeHandler.wiiVIHook) - elif codeHandler.hookType == "GX": - result = sample.find(codeHandler.wiiGXDrawHook) - elif codeHandler.hookType == "PAD": - result = sample.find(codeHandler.wiiPADHook) - else: - raise NotImplementedError(tools.color_text(f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) - - if result >= 0: - dolFile.seek(section["address"] + result) - else: - continue - - while (sample := read_uint32(dolFile)) != 0x4E800020: - pass - - dolFile.seek(-4, 1) - codeHandler.hookAddress = dolFile.tell() - - return True - return False - -def insert_code_hook(dolFile: DolFile, codeHandler: CodeHandler, address: int): - dolFile.seek(address) - ppc = read_uint32(dolFile) - - if ((ppc >> 24) & 0xFF) > 0x3F and ((ppc >> 24) & 0xFF) < 0x48: - raise NotImplementedError(tools.color_text("Hooking the codehandler to a conditional non spr branch is unsupported", defaultColor=tools.TREDLIT)) - - dolFile.seek(-4, 1) - dolFile.insert_branch(codeHandler.startAddress, address, lk=0) diff --git a/loader.cpp b/loader.cpp index 7885817..3a94845 100644 --- a/loader.cpp +++ b/loader.cpp @@ -1,21 +1,6 @@ -//#include -//#include - -//using std::array; -//using std::memcpy; -//using std::memcmp; -//using std::memset; -//using std::string; - -#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 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 CODEHANDLER 0x800018A8 @@ -34,18 +19,14 @@ using f64 = double; __attribute__((noreturn)) int main(); -struct CodeList -{ - +struct CodeList { u16 mBaseASM; u16 mUpperBase; u16 mOffsetASM; u16 mLowerOffset; }; -struct Info -{ - +struct Info { const u32 allocsize; const u32 loaderSize; const u32 handlerSize; @@ -54,135 +35,104 @@ struct Info const u32 crypted; }; -class DiscHeader -{ - +class DiscHeader { public: - enum class TVMODE - { - NTSC, - PAL, - DEBUG, - DEBUGPAL, - MPAL, - PAL60 - }; - - struct MetaData - { - - const u8 mDiscID; //0x0000 - const u16 mGameCode; //0x0001 - const u8 mRegionCode; //0x0003 - const u16 mMakerCode; //0x0004 - const u8 mDiscNumber; //0x0006 - const u8 mDiscVersion; //0x0007 - const u8 mAudioStreaming; //0x0008 - const u8 mStreamBufferSize; //0x0009 - const u8 _00[12]; //0x000A - const u32 mWiiMagic; //0x0018 - const u32 mGCNMagic; //0x001C - const u32 mNinBootCode; //0x0020 - const u32 mAppVersion; //0x0024 - const u32 mPhysicalRAMSize; //0x0028 - const u32 mBoardModel; //0x002C - u8 *mOSArenaLo; //0x0030 - u8 *mOSArenaHi; //0x0034 - u32 *mFstStart; //0x0038 - u32 mFstSize; //0x003C - u32 mDebuggerPresent; //0x0040 - const u32 mDebuggerExceptionMask; //0x0044 - void *mExceptionHookDest; //0x0048 - const u32 mExceptionReturn; //0x004C - u32 _01[0x10 / 4]; //0x0050 - u32 mDebuggerHook[0x24 / 4]; //0x0060 - u32 _02[0x3C / 4]; //0x0084 - u32 mCurrentOSContext; //0x00C0 - u32 mPreviousOSMask; //0x00C4 - u32 mCurrentOSMask; //0x00C8 - DiscHeader::TVMODE mTVMode; //0x00CC - u32 mARAMSize; //0x00D0 - void *mCurOSContextLogical; //0x00D4 - void *mDefaultOSThreadLogical; //0x00D8 - u32 *mThreadQueueHead; //0x00DC - u32 *mThreadQueueTail; //0x00E0 - u32 *mCurrentOSThread; //0x00E4 - u32 mDebuggerSize; //0x00E8 - u32 *mDebuggerMonitorLoc; //0x00EC - u32 mSimulatedMemSize; //0x00F0 - u8 *mBi2HeaderLoc; //0x00F4 - u32 mBusClockSpeed; //0x00F8 - u32 mCPUClockSpeed; //0x00FC - u32 _04[0x3010 / 4]; //0x0100 - u8 *mWiiHeap; //0x3110 + enum class TVMODE { NTSC, PAL, DEBUG, DEBUGPAL, MPAL, PAL60 }; + + struct MetaData { + + const u8 mDiscID; // 0x0000 + const u16 mGameCode; // 0x0001 + const u8 mRegionCode; // 0x0003 + const u16 mMakerCode; // 0x0004 + const u8 mDiscNumber; // 0x0006 + const u8 mDiscVersion; // 0x0007 + const u8 mAudioStreaming; // 0x0008 + const u8 mStreamBufferSize; // 0x0009 + const u8 _00[12]; // 0x000A + const u32 mWiiMagic; // 0x0018 + const u32 mGCNMagic; // 0x001C + const u32 mNinBootCode; // 0x0020 + const u32 mAppVersion; // 0x0024 + const u32 mPhysicalRAMSize; // 0x0028 + const u32 mBoardModel; // 0x002C + u8 *mOSArenaLo; // 0x0030 + u8 *mOSArenaHi; // 0x0034 + u32 *mFstStart; // 0x0038 + u32 mFstSize; // 0x003C + u32 mDebuggerPresent; // 0x0040 + const u32 mDebuggerExceptionMask; // 0x0044 + void *mExceptionHookDest; // 0x0048 + const u32 mExceptionReturn; // 0x004C + u32 _01[0x10 / 4]; // 0x0050 + u32 mDebuggerHook[0x24 / 4]; // 0x0060 + u32 _02[0x3C / 4]; // 0x0084 + u32 mCurrentOSContext; // 0x00C0 + u32 mPreviousOSMask; // 0x00C4 + u32 mCurrentOSMask; // 0x00C8 + DiscHeader::TVMODE mTVMode; // 0x00CC + u32 mARAMSize; // 0x00D0 + void *mCurOSContextLogical; // 0x00D4 + void *mDefaultOSThreadLogical; // 0x00D8 + u32 *mThreadQueueHead; // 0x00DC + u32 *mThreadQueueTail; // 0x00E0 + u32 *mCurrentOSThread; // 0x00E4 + u32 mDebuggerSize; // 0x00E8 + u32 *mDebuggerMonitorLoc; // 0x00EC + u32 mSimulatedMemSize; // 0x00F0 + u8 *mBi2HeaderLoc; // 0x00F4 + u32 mBusClockSpeed; // 0x00F8 + u32 mCPUClockSpeed; // 0x00FC + u32 _04[0x3010 / 4]; // 0x0100 + u8 *mWiiHeap; // 0x3110 }; static MetaData sMetaData; - enum class CONSOLETYPE - { - Gamecube, - Wii, - Unknown - }; + enum class CONSOLETYPE { Gamecube, Wii, Unknown }; - inline u32 getGameID() { return ((u32)sMetaData.mDiscID << 24) | ((u32)sMetaData.mGameCode << 8) | ((u32)sMetaData.mRegionCode); } + inline u32 getGameID() { + return ((u32)sMetaData.mDiscID << 24) | ((u32)sMetaData.mGameCode << 8) | + ((u32)sMetaData.mRegionCode); + } inline u16 getMakerID() { return sMetaData.mMakerCode; } inline u8 getDiscNumber() { return sMetaData.mDiscNumber; } inline u8 getDiscVersion() { return sMetaData.mDiscVersion; } - CONSOLETYPE detectHomeConsole() - { - if (sMetaData.mGCNMagic) - { + CONSOLETYPE detectHomeConsole() { + if (sMetaData.mGCNMagic) { return CONSOLETYPE::Gamecube; - } - else if (sMetaData.mWiiMagic) - { + } else if (sMetaData.mWiiMagic) { return CONSOLETYPE::Wii; } return CONSOLETYPE::Unknown; } - inline void setHeap(u32 alloc) - { - if (sMetaData.mBi2HeaderLoc < sMetaData.mOSArenaHi) - { + inline void setHeap(u32 alloc) { + if (sMetaData.mBi2HeaderLoc < sMetaData.mOSArenaHi) { sMetaData.mOSArenaHi = sMetaData.mBi2HeaderLoc - alloc; - if (this->detectHomeConsole() == DiscHeader::CONSOLETYPE::Wii) - { + if (this->detectHomeConsole() == DiscHeader::CONSOLETYPE::Wii) { sMetaData.mWiiHeap = sMetaData.mBi2HeaderLoc - alloc; } - } - else - { - if (this->detectHomeConsole() == DiscHeader::CONSOLETYPE::Wii) - { + } else { + if (this->detectHomeConsole() == DiscHeader::CONSOLETYPE::Wii) { sMetaData.mOSArenaHi = sMetaData.mWiiHeap - alloc; sMetaData.mWiiHeap -= alloc; - } - else - { + } else { sMetaData.mOSArenaHi -= alloc; } - } } }; static DiscHeader sDisc; Info gpModInfo = { - 0x48454150, - 0x4C53495A, - 0x4853495A, - 0x4353495A, - (const u32 *)0x484F4F4B, + 0x48454150, 0x4C53495A, 0x4853495A, 0x4353495A, (const u32 *)0x484F4F4B, 0x43525054, }; -inline u32 extractBranchAddr(u32 *bAddr) -{ +inline u32 extractBranchAddr(u32 *bAddr) { s32 offset; if (*bAddr & 0x2000000) offset = (*bAddr & 0x3FFFFFD) - 0x4000000; @@ -191,232 +141,216 @@ inline u32 extractBranchAddr(u32 *bAddr) return (u32)bAddr + offset; } -namespace Memory -{ +namespace Memory { - static void memcpy(u8 *to, u8 *from, s32 size) - { - for (s32 i = 0; i < size; ++i) - { - *to++ = *from++; - } +static void memcpy(u8 *to, u8 *from, s32 size) { + for (s32 i = 0; i < size; ++i) { + *to++ = *from++; } +} - namespace Cache - { +namespace Cache { - static inline void flushAddr(void *addr) - { - dcbf(addr); - icbi(addr); - } +static inline void flushAddr(void *addr) { + dcbf(addr); + icbi(addr); +} - static void flushRange(u8 *addr, s32 size) - { - size += 31 + (((u32)addr & 31) > 0); +static void flushRange(u8 *addr, s32 size) { + size += 31 + (((u32)addr & 31) > 0); - for (u32 i = 0; i < (size >> 5); ++i) - { - flushAddr((void *)(addr + (i << 5))); - } - } + for (u32 i = 0; i < (size >> 5); ++i) { + flushAddr((void *)(addr + (i << 5))); + } +} - static void storeAddr(void *addr) - { - dcbst(addr); - icbi(addr); - } +static void storeAddr(void *addr) { + dcbst(addr); + icbi(addr); +} - static void storeRange(u8 *addr, s32 size) - { - size += 31 + (((u32)addr & 31) > 0); +static void storeRange(u8 *addr, s32 size) { + size += 31 + (((u32)addr & 31) > 0); - for (u32 i = 0; i < (size >> 5); ++i) - { - storeAddr((void *)(addr + (i << 5))); - } - } + for (u32 i = 0; i < (size >> 5); ++i) { + storeAddr((void *)(addr + (i << 5))); + } +} - } // namespace Cache +} // namespace Cache - namespace Direct - { +namespace Instr { - template - static inline void write(T *addr, T value) - { - *addr = value; - Cache::flushAddr(addr); - } +template static inline void write(T *addr, T value) { + *addr = value; + Cache::flushAddr(addr); +} - /*This constructs a branch instruction. &TO = ((TO - FROM) & MAX_OFFSET) | BRANCH_TYPE | !!isLink*/ - static inline void branch(void *addr, void *to, bool lk) - { - Direct::write((u32 *)(addr), ((((u32)(to) - (u32)(addr)) & 0x3ffffff) | 0x48000000 | lk)); - } +/*This constructs a branch instruction. &TO = ((TO - FROM) & MAX_OFFSET) | + * BRANCH_TYPE | !!isLink*/ +static inline void branch(void *addr, void *to, bool lk) { + Instr::write((u32 *)(addr), ((((u32)(to) - (u32)(addr)) & 0x3ffffff) | + 0x48000000 | lk)); +} - } // namespace Direct +} // namespace Direct - namespace Search - { +namespace Search { - static u32 *array(u32 *start, u32 *end, u32 arrayLength, const u32 *hookData) - { - u32 index = 0; +static u32 *array(u32 *start, 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; &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; + /*Loop through the games RAM, make sure we don't find our own hook data by + * accident*/ + for (u32 i = 0; &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) && (&start[i] < (u32 *)&gpModInfo || &start[i] > (u32 *)&gpModInfo + sizeof(Info))) - return &start[i]; - } - return nullptr; - } + /*If the data has matched the whole array, return the address of the match*/ + if (index >= (arrayLength) && + (&start[i] < (u32 *)&gpModInfo || + &start[i] > (u32 *)&gpModInfo + sizeof(Info))) + return &start[i]; + } + return nullptr; +} - template - static T *single(T *start, T *end, T match) - { - for (u32 i = 0; &start[i] < end; ++i) - { - if (start[i] == match) - { - return &start[i]; - } - } - return nullptr; +template static T *single(T *start, T *end, T match) { + for (u32 i = 0; &start[i] < end; ++i) { + if (start[i] == match) { + return &start[i]; } + } + return nullptr; +} - /*Call this after viHook, finds the address of the first instance - of targetVal, and hooks it to the pointer hookTo*/ - static inline void hookFunction(u32 *start, u32 targetVal, void *hookTo, bool lk) - { - Direct::branch(Search::single(start, start + 0x500, targetVal), hookTo, lk); - } +/*Call this after viHook, finds the address of the first instance +of targetVal, and hooks it to the pointer hookTo*/ +static inline void hookFunction(u32 *start, u32 targetVal, void *hookTo, + bool lk) { + Instr::branch(Search::single(start, start + 0x500, targetVal), hookTo, + lk); +} - } // namespace Search +} // namespace Search - class Crypt - { +class Crypt { - private: - u32 key; +private: + u32 key; - u32 getKey() - { - u32 b1 = (this->key >> 24) & 0xFF; - u32 b2 = (this->key >> 16) & 0xFF; - u32 b3 = (this->key >> 8) & 0xFF; - u32 b4 = this->key & 0xFF; - b1 ^= b2; - b2 ^= b3; - b3 ^= b4; - return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1; - } + u32 getKey() { + u32 b1 = (this->key >> 24) & 0xFF; + u32 b2 = (this->key >> 16) & 0xFF; + u32 b3 = (this->key >> 8) & 0xFF; + u32 b4 = this->key & 0xFF; + b1 ^= b2; + b2 ^= b3; + b3 ^= b4; + return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1; + } - void setKey(u32 key) - { - u32 b1 = key & 0xFF; - u32 b2 = (key >> 8) & 0xFF; - u32 b3 = (key >> 16) & 0xFF; - u32 b4 = (key >> 24) & 0xFF; - b3 ^= b4; - b2 ^= b3; - b1 ^= b2; - this->key = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; - } + void setKey(u32 key) { + u32 b1 = key & 0xFF; + u32 b2 = (key >> 8) & 0xFF; + u32 b3 = (key >> 16) & 0xFF; + u32 b4 = (key >> 24) & 0xFF; + b3 ^= b4; + b2 ^= b3; + b1 ^= b2; + this->key = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; + } - public: - Crypt(u32 key) - { - this->key = key; - } +public: + Crypt(u32 key) { this->key = key; } - inline void xorCrypt(u32 *dest, u32 *buffer, u32 size) - { - auto key = this->getKey(); + inline void xorCrypt(u32 *dest, u32 *buffer, u32 size) { + auto key = this->getKey(); - for (u32 i = 0; i < size; ++i) - { - dest[i] = buffer[i] ^ key; - key += i << 3; - } + for (u32 i = 0; i < size; ++i) { + dest[i] = buffer[i] ^ key; + key += i << 3; } - }; + } +}; - enum class Space : u32 - { - Start = 0x80000000, - End = 0x81800000, - Size = 0x1800000 - }; +enum class Space : u32 { + Start = 0x80000000, + End = 0x81800000, + Size = 0x1800000 +}; } // namespace Memory Memory::Crypt gpCryptor = {0x43595054}; -static void initMods() -{ +static void initMods() { sDisc.setHeap(gpModInfo.allocsize); /*Reallocate the internal heap*/ /*Change codelist pointer to the new address in the allocation*/ - CodeList *codelistPointer = (CodeList *)((u32)&gpModInfo + sizeof(Info) + 0xFC); - codelistPointer->mUpperBase = (((u32)sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize) >> 16) & 0xFFFF; - codelistPointer->mLowerOffset = ((u32)sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize) & 0xFFFF; + CodeList *codelistPointer = + (CodeList *)((u32)&gpModInfo + sizeof(Info) + 0xFC); + codelistPointer->mUpperBase = + (((u32)sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize) >> 16) & + 0xFFFF; + codelistPointer->mLowerOffset = + ((u32)sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize) & 0xFFFF; /*Copy codelist to the new allocation*/ - if (gpModInfo.crypted) - { - Memory::memcpy(sDisc.sMetaData.mOSArenaHi, (u8 *)&gpModInfo + sizeof(Info) + 4, gpModInfo.handlerSize); - gpCryptor.xorCrypt((u32 *)(sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize), (u32 *)((u8 *)&gpModInfo + sizeof(Info) + gpModInfo.handlerSize + 4), gpModInfo.codeSize >> 2); - } - else - { - Memory::memcpy(sDisc.sMetaData.mOSArenaHi, (u8 *)&gpModInfo + sizeof(Info) + 4, gpModInfo.handlerSize + gpModInfo.codeSize); + if (gpModInfo.crypted) { + Memory::memcpy(sDisc.sMetaData.mOSArenaHi, + (u8 *)&gpModInfo + sizeof(Info) + 4, gpModInfo.handlerSize); + gpCryptor.xorCrypt( + (u32 *)(sDisc.sMetaData.mOSArenaHi + gpModInfo.handlerSize), + (u32 *)((u8 *)&gpModInfo + sizeof(Info) + gpModInfo.handlerSize + 4), + gpModInfo.codeSize >> 2); + } else { + Memory::memcpy(sDisc.sMetaData.mOSArenaHi, + (u8 *)&gpModInfo + sizeof(Info) + 4, + gpModInfo.handlerSize + gpModInfo.codeSize); } /*Get codehandler hook resources*/ - auto fillInField = Memory::Search::single((u32 *)sDisc.sMetaData.mOSArenaHi, (u32 *)(sDisc.sMetaData.mOSArenaHi + 0x600), 0x00DEDEDE); + auto fillInField = Memory::Search::single( + (u32 *)sDisc.sMetaData.mOSArenaHi, + (u32 *)(sDisc.sMetaData.mOSArenaHi + 0x600), 0x00DEDEDE); auto returnAddress = extractBranchAddr((u32 *)gpModInfo.codehandlerHook); auto ppc = *gpModInfo.codehandlerHook; /*Write hook branch*/ - Memory::Direct::branch((void *)gpModInfo.codehandlerHook, (void *)((u32)sDisc.sMetaData.mOSArenaHi + 0xA8), false); //entryhook + Memory::Instr::branch((void *)gpModInfo.codehandlerHook, + (void *)((u32)sDisc.sMetaData.mOSArenaHi + 0xA8), + false); // entryhook /*Temporary nop*/ *fillInField = 0x60000000; /*Flush the cache so that the instructions update*/ - Memory::Cache::flushRange((u8 *)sDisc.sMetaData.mOSArenaHi, gpModInfo.handlerSize + gpModInfo.codeSize); + Memory::Cache::flushRange((u8 *)sDisc.sMetaData.mOSArenaHi, + gpModInfo.handlerSize + gpModInfo.codeSize); /*Call the codehandler*/ call((void *)((u32)(sDisc.sMetaData.mOSArenaHi) + 0xA8))(); /*Write original instruction or translate offset data if a branch*/ - if (((ppc >> 24) & 0xFF) > 0x47 && ((ppc >> 24) & 0xFF) < 0x4C) - { - Memory::Direct::branch((void *)fillInField, (void *)returnAddress, ppc & 1); - } - else - { - Memory::Direct::write(fillInField, ppc); + if (((ppc >> 24) & 0xFF) > 0x47 && ((ppc >> 24) & 0xFF) < 0x4C) { + Memory::Instr::branch((void *)fillInField, (void *)returnAddress, ppc & 1); + } else { + Memory::Instr::write(fillInField, ppc); } /*Write branch back to the hook address + 4*/ - Memory::Direct::branch((void *)&fillInField[1], (void *)(&gpModInfo.codehandlerHook[1]), false); //return + Memory::Instr::branch((void *)&fillInField[1], + (void *)(&gpModInfo.codehandlerHook[1]), + false); // return } -int main() -{ - if (sDisc.detectHomeConsole() != DiscHeader::CONSOLETYPE::Unknown) - { +int main() { + if (sDisc.detectHomeConsole() != DiscHeader::CONSOLETYPE::Unknown) { initMods(); } __start(); diff --git a/main_ui.py b/main_ui.py deleted file mode 100644 index d8d6150..0000000 --- a/main_ui.py +++ /dev/null @@ -1,762 +0,0 @@ -from PyQt5 import QtCore, QtGui, QtWidgets - -from children_ui import PrefWindow -from fileutils import resource_path - -class MainWindow(QtWidgets.QMainWindow): - def __init__(self, version: str): - super().__init__() - - self._job_active = False - self.apiRevision = version - self.setup_ui() - - self.LightTheme = self.palette() - - self.DarkTheme = QtGui.QPalette() - self.DarkTheme.setColor(QtGui.QPalette.Window, QtGui.QColor(53, 53, 53)) - self.DarkTheme.setColor(QtGui.QPalette.WindowText, QtCore.Qt.white) - self.DarkTheme.setColor(QtGui.QPalette.Base, QtGui.QColor(25, 25, 25)) - self.DarkTheme.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(53, 53, 53)) - self.DarkTheme.setColor(QtGui.QPalette.ToolTipBase, QtCore.Qt.black) - self.DarkTheme.setColor(QtGui.QPalette.ToolTipText, QtCore.Qt.white) - self.DarkTheme.setColor(QtGui.QPalette.Text, QtCore.Qt.white) - self.DarkTheme.setColor(QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) - self.DarkTheme.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.white) - self.DarkTheme.setColor(QtGui.QPalette.BrightText, QtCore.Qt.red) - self.DarkTheme.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218)) - self.DarkTheme.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218)) - self.DarkTheme.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.black) - - def set_job_activity(self, active: bool): - self._job_active = active - - def close_event(self, event: QtGui.QCloseEvent): - if self._job_active: - reply = QtWidgets.QMessageBox(self) - reply.setWindowTitle("Active job") - reply.setText("GeckoLoader is busy!") - reply.setInformativeText("Exiting is disabled") - reply.setIcon(QtWidgets.QMessageBox.Warning) - reply.setStandardButtons(QtWidgets.QMessageBox.Ok) - reply.setDefaultButton(QtWidgets.QMessageBox.Ok) - reply.exec_() - event.ignore() - else: - event.accept() - - def setup_ui(self): - self.setObjectName("MainWindow") - self.setWindowModality(QtCore.Qt.NonModal) - self.setEnabled(True) - self.setFixedSize(550, 680) - font = QtGui.QFont() - font.setFamily("Helvetica") - font.setPointSize(10) - font.setWeight(42) - self.setFont(font) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(str(resource_path("bin/icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.setWindowIcon(icon) - - #Top level widget - self.centerWidget = QtWidgets.QWidget(self) - self.centerWidget.setObjectName("centerWidget") - - self.gridLayout = QtWidgets.QGridLayout(self.centerWidget) - self.gridLayout.setVerticalSpacing(0) - self.gridLayout.setObjectName("gridLayout") - - #Layout for file paths and open boxes - self.filesLayout = QtWidgets.QGridLayout() - self.filesLayout.setHorizontalSpacing(0) - self.filesLayout.setObjectName("filesLayout") - - self.dolLayout = QtWidgets.QGridLayout() - self.dolLayout.setHorizontalSpacing(0) - self.dolLayout.setObjectName("dolLayout") - - #Layout for folder path - self.gctLayout = QtWidgets.QGridLayout() - self.gctLayout.setHorizontalSpacing(0) - self.gctLayout.setVerticalSpacing(5) - self.gctLayout.setObjectName("gctLayout") - - self.destLayout = QtWidgets.QGridLayout() - self.dolLayout.setHorizontalSpacing(0) - self.dolLayout.setObjectName("dolLayout") - - #Files label - self.filesLabel = QtWidgets.QLabel(self.centerWidget) - self.filesLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.filesLabel.sizePolicy().hasHeightForWidth()) - self.filesLabel.setSizePolicy(sizePolicy) - self.filesLabel.setMinimumSize(QtCore.QSize(80, 30)) - self.filesLabel.setMaximumSize(QtCore.QSize(16777215, 30)) - font = QtGui.QFont("Helvetica") - font.setPointSize(21) - font.setWeight(82) - font.setBold(True) - self.filesLabel.setFont(font) - self.filesLabel.setTextFormat(QtCore.Qt.PlainText) - self.filesLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.filesLabel.setObjectName("filesLabel") - - #Dol button to open file - self.dolButton = QtWidgets.QPushButton(self.centerWidget) - self.dolButton.setMinimumSize(QtCore.QSize(100, 26)) - self.dolButton.setMaximumSize(QtCore.QSize(100, 26)) - font = QtGui.QFont("Helvetica") - font.setPointSize(11) - self.dolButton.setFont(font) - self.dolButton.setCheckable(False) - self.dolButton.setChecked(False) - self.dolButton.setAutoDefault(True) - self.dolButton.setDefault(False) - self.dolButton.setFlat(False) - self.dolButton.setObjectName("dolButton") - self.dolLayout.addWidget(self.dolButton, 1, 0, 1, 1) - - #Dol path textbox - self.dolTextBox = QtWidgets.QLineEdit(self.centerWidget) - self.dolTextBox.setEnabled(False) - self.dolTextBox.setMinimumSize(QtCore.QSize(200, 24)) - self.dolTextBox.setMaximumSize(QtCore.QSize(16777215, 24)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(10) - font.setWeight(42) - self.dolTextBox.setFont(font) - self.dolTextBox.setText("") - self.dolTextBox.setMaxLength(255) - self.dolTextBox.setFrame(True) - self.dolTextBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.dolTextBox.setObjectName("dolTextBox") - self.dolLayout.addWidget(self.dolTextBox, 1, 1, 1, 1) - - #horizontal separater codes - self.horiSepFiles = QtWidgets.QFrame(self.centerWidget) - self.horiSepFiles.setMinimumSize(QtCore.QSize(474, 30)) - self.horiSepFiles.setContentsMargins(20, 0, 20, 0) - self.horiSepFiles.setFrameShape(QtWidgets.QFrame.HLine) - self.horiSepFiles.setFrameShadow(QtWidgets.QFrame.Sunken) - self.horiSepFiles.setObjectName("horiSepFiles") - - #gctFile button to open file - self.gctFileButton = QtWidgets.QPushButton(self.centerWidget) - self.gctFileButton.setMinimumSize(QtCore.QSize(100, 26)) - self.gctFileButton.setMaximumSize(QtCore.QSize(100, 26)) - font = QtGui.QFont("Helvetica") - font.setPointSize(10) - self.gctFileButton.setFont(font) - self.gctFileButton.setCheckable(False) - self.gctFileButton.setChecked(False) - self.gctFileButton.setAutoDefault(True) - self.gctFileButton.setDefault(False) - self.gctFileButton.setFlat(False) - self.gctFileButton.setObjectName("gctFileButton") - self.gctLayout.addWidget(self.gctFileButton, 0, 0, 1, 1) - - #gctFile path textbox - self.gctFileTextBox = QtWidgets.QLineEdit(self.centerWidget) - self.gctFileTextBox.setEnabled(False) - self.gctFileTextBox.setMinimumSize(QtCore.QSize(200, 24)) - self.gctFileTextBox.setMaximumSize(QtCore.QSize(16777215, 24)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(10) - font.setWeight(42) - self.gctFileTextBox.setFont(font) - self.gctFileTextBox.setText("") - self.gctFileTextBox.setMaxLength(255) - self.gctFileTextBox.setFrame(True) - self.gctFileTextBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.gctFileTextBox.setObjectName("gctFileTextBox") - self.gctLayout.addWidget(self.gctFileTextBox, 0, 1, 1, 1) - - #--or-- Label - self.orFolderLabel = QtWidgets.QLabel(self.centerWidget) - self.orFolderLabel.setEnabled(False) - self.orFolderLabel.setMinimumSize(QtCore.QSize(80, 8)) - self.orFolderLabel.setMaximumSize(QtCore.QSize(16777215, 8)) - font = QtGui.QFont("Helvetica") - font.setPointSize(8) - font.setWeight(82) - font.setBold(True) - self.orFolderLabel.setFont(font) - self.orFolderLabel.setTextFormat(QtCore.Qt.PlainText) - self.orFolderLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.orFolderLabel.setObjectName("orFolderLabel") - self.gctLayout.addWidget(self.orFolderLabel, 1, 0, 1, 2) - - #gctFolder button to open file - self.gctFolderButton = QtWidgets.QPushButton(self.centerWidget) - self.gctFolderButton.setMinimumSize(QtCore.QSize(100, 26)) - self.gctFolderButton.setMaximumSize(QtCore.QSize(100, 26)) - font = QtGui.QFont("Helvetica") - font.setPointSize(10) - self.gctFolderButton.setFont(font) - self.gctFolderButton.setCheckable(False) - self.gctFolderButton.setChecked(False) - self.gctFolderButton.setAutoDefault(True) - self.gctFolderButton.setDefault(False) - self.gctFolderButton.setFlat(False) - self.gctFolderButton.setObjectName("gctFolderButton") - self.gctLayout.addWidget(self.gctFolderButton, 2, 0, 1, 1) - - #gctFolder path textbox - self.gctFolderTextBox = QtWidgets.QLineEdit(self.centerWidget) - self.gctFolderTextBox.setEnabled(False) - self.gctFolderTextBox.setMinimumSize(QtCore.QSize(200, 24)) - self.gctFolderTextBox.setMaximumSize(QtCore.QSize(16777215, 24)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(10) - font.setWeight(42) - self.gctFolderTextBox.setFont(font) - self.gctFolderTextBox.setText("") - self.gctFolderTextBox.setMaxLength(255) - self.gctFolderTextBox.setFrame(True) - self.gctFolderTextBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.gctFolderTextBox.setObjectName("gctFolderTextBox") - self.gctLayout.addWidget(self.gctFolderTextBox, 2, 1, 1, 1) - - #horizontal separater dest - self.horiSepDest = QtWidgets.QFrame(self.centerWidget) - self.horiSepDest.setMinimumSize(QtCore.QSize(474, 30)) - self.horiSepDest.setContentsMargins(20, 0, 20, 0) - self.horiSepDest.setFrameShape(QtWidgets.QFrame.HLine) - self.horiSepDest.setFrameShadow(QtWidgets.QFrame.Sunken) - self.horiSepDest.setObjectName("horiSepDest") - - #Dest button to open file - self.destButton = QtWidgets.QPushButton(self.centerWidget) - self.destButton.setMinimumSize(QtCore.QSize(100, 26)) - self.destButton.setMaximumSize(QtCore.QSize(100, 26)) - font = QtGui.QFont("Helvetica") - font.setPointSize(11) - self.destButton.setFont(font) - self.destButton.setCheckable(False) - self.destButton.setChecked(False) - self.destButton.setAutoDefault(True) - self.destButton.setDefault(False) - self.destButton.setFlat(False) - self.destButton.setObjectName("destButton") - self.destLayout.addWidget(self.destButton, 0, 0, 1, 1) - - #Dest path textbox - self.destTextBox = QtWidgets.QLineEdit(self.centerWidget) - self.destTextBox.setEnabled(False) - self.destTextBox.setMinimumSize(QtCore.QSize(200, 24)) - self.destTextBox.setMaximumSize(QtCore.QSize(16777215, 24)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(10) - font.setWeight(42) - self.destTextBox.setFont(font) - self.destTextBox.setText("") - self.destTextBox.setMaxLength(255) - self.destTextBox.setFrame(True) - self.destTextBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.destTextBox.setObjectName("destTextBox") - self.destLayout.addWidget(self.destTextBox, 0, 1, 1, 1) - - self.filesLayout.addLayout(self.dolLayout, 0, 0, 1, 1) - self.filesLayout.addWidget(self.horiSepFiles, 1, 0, 1, 1) - self.filesLayout.addLayout(self.gctLayout, 2, 0, 1, 1) - self.filesLayout.addWidget(self.horiSepDest, 3, 0, 1, 1) - self.filesLayout.addLayout(self.destLayout, 4, 0, 1, 1) - - #Options Layout - self.optionsLayout = QtWidgets.QGridLayout() - self.optionsLayout.setHorizontalSpacing(20) - self.optionsLayout.setObjectName("optionsLayout") - - #Options Label - self.optionsLabel = QtWidgets.QLabel(self.centerWidget) - self.optionsLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.optionsLabel.sizePolicy().hasHeightForWidth()) - self.optionsLabel.setSizePolicy(sizePolicy) - self.optionsLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.optionsLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - font = QtGui.QFont("Helvetica") - font.setPointSize(18) - font.setWeight(82) - font.setBold(True) - self.optionsLabel.setFont(font) - self.optionsLabel.setTextFormat(QtCore.Qt.PlainText) - self.optionsLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.optionsLabel.setObjectName("optionsLabel") - self.optionsLayout.addWidget(self.optionsLabel, 0, 0, 1, 4) - - #Allocation Label - self.allocLabel = QtWidgets.QLabel(self.centerWidget) - self.allocLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.allocLabel.sizePolicy().hasHeightForWidth()) - self.allocLabel.setSizePolicy(sizePolicy) - self.allocLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.allocLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.allocLabel.setTextFormat(QtCore.Qt.PlainText) - self.allocLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.allocLabel.setObjectName("allocLabel") - self.optionsLayout.addWidget(self.allocLabel, 1, 0, 1, 1) - - #Allocation Textbox - self.allocLineEdit = QtWidgets.QLineEdit(self.centerWidget) - self.allocLineEdit.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.allocLineEdit.sizePolicy().hasHeightForWidth()) - self.allocLineEdit.setSizePolicy(sizePolicy) - self.allocLineEdit.setMinimumSize(QtCore.QSize(79, 23)) - self.allocLineEdit.setMaximumSize(QtCore.QSize(79, 23)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(12) - font.setWeight(42) - self.allocLineEdit.setFont(font) - self.allocLineEdit.setText("") - self.allocLineEdit.setMaxLength(6) - self.allocLineEdit.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.allocLineEdit.setObjectName("allocLineEdit") - self.optionsLayout.addWidget(self.allocLineEdit, 2, 0, 1, 1) - - #handlerType label - self.handlerTypeLabel = QtWidgets.QLabel(self.centerWidget) - self.handlerTypeLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.handlerTypeLabel.sizePolicy().hasHeightForWidth()) - self.handlerTypeLabel.setSizePolicy(sizePolicy) - self.handlerTypeLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.handlerTypeLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.handlerTypeLabel.setTextFormat(QtCore.Qt.PlainText) - self.handlerTypeLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.handlerTypeLabel.setObjectName("handlerTypeLabel") - self.optionsLayout.addWidget(self.handlerTypeLabel, 1, 1, 1, 1) - - #handlerType selection - self.handlerTypeSelect = QtWidgets.QComboBox(self.centerWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.allocLabel.sizePolicy().hasHeightForWidth()) - self.handlerTypeSelect.setSizePolicy(sizePolicy) - self.handlerTypeSelect.setMinimumSize(QtCore.QSize(79, 23)) - self.handlerTypeSelect.setMaximumSize(QtCore.QSize(79, 23)) - self.handlerTypeSelect.setObjectName("handlerTypeSelect") - self.handlerTypeSelect.addItems(["FULL", "MINI"]) - self.optionsLayout.addWidget(self.handlerTypeSelect, 2, 1, 1, 1) - - #hookType label - self.hookTypeLabel = QtWidgets.QLabel(self.centerWidget) - self.hookTypeLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hookTypeLabel.sizePolicy().hasHeightForWidth()) - self.hookTypeLabel.setSizePolicy(sizePolicy) - self.hookTypeLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.hookTypeLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.hookTypeLabel.setTextFormat(QtCore.Qt.PlainText) - self.hookTypeLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.hookTypeLabel.setObjectName("hookTypeLabel") - self.optionsLayout.addWidget(self.hookTypeLabel, 1, 2, 1, 1) - - #hookType selection - self.hookTypeSelect = QtWidgets.QComboBox(self.centerWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.allocLabel.sizePolicy().hasHeightForWidth()) - self.hookTypeSelect.setSizePolicy(sizePolicy) - self.hookTypeSelect.setMinimumSize(QtCore.QSize(79, 23)) - self.hookTypeSelect.setMaximumSize(QtCore.QSize(79, 23)) - self.hookTypeSelect.setObjectName("hookTypeSelect") - self.hookTypeSelect.addItems(["VI", "GX", "PAD"]) - self.optionsLayout.addWidget(self.hookTypeSelect, 2, 2, 1, 1) - - #txtCodesInclude label - self.txtCodesIncludeLabel = QtWidgets.QLabel(self.centerWidget) - self.txtCodesIncludeLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.txtCodesIncludeLabel.sizePolicy().hasHeightForWidth()) - self.txtCodesIncludeLabel.setSizePolicy(sizePolicy) - self.txtCodesIncludeLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.txtCodesIncludeLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.txtCodesIncludeLabel.setTextFormat(QtCore.Qt.PlainText) - self.txtCodesIncludeLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.txtCodesIncludeLabel.setObjectName("txtCodesIncludeLabel") - self.optionsLayout.addWidget(self.txtCodesIncludeLabel, 1, 3, 1, 1) - - #txtCodesInclude selection - self.txtCodesIncludeSelect = QtWidgets.QComboBox(self.centerWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.allocLabel.sizePolicy().hasHeightForWidth()) - self.txtCodesIncludeSelect.setSizePolicy(sizePolicy) - self.txtCodesIncludeSelect.setMinimumSize(QtCore.QSize(79, 23)) - self.txtCodesIncludeSelect.setMaximumSize(QtCore.QSize(79, 23)) - self.txtCodesIncludeSelect.setObjectName("txtCodesIncludeSelect") - self.txtCodesIncludeSelect.addItems(["ACTIVE", "ALL"]) - self.optionsLayout.addWidget(self.txtCodesIncludeSelect, 2, 3, 1, 1) - - #horizontal separater options - self.horiSepOptions = QtWidgets.QFrame(self.centerWidget) - self.horiSepOptions.setMinimumSize(QtCore.QSize(300, 30)) - self.horiSepOptions.setContentsMargins(20, 0, 20, 0) - self.horiSepOptions.setFrameShape(QtWidgets.QFrame.HLine) - self.horiSepOptions.setFrameShadow(QtWidgets.QFrame.Sunken) - self.horiSepOptions.setObjectName("horiSepOptions") - self.optionsLayout.addWidget(self.horiSepOptions, 3, 0, 1, 4) - - #Advanced options button - self.exOptionsButton = QtWidgets.QPushButton(self.centerWidget) - font = QtGui.QFont("Helvetica") - font.setPointSize(13) - self.exOptionsButton.setFont(font) - self.exOptionsButton.setCheckable(False) - self.exOptionsButton.setChecked(False) - self.exOptionsButton.setAutoDefault(True) - self.exOptionsButton.setDefault(False) - self.exOptionsButton.setFlat(False) - self.exOptionsButton.setDisabled(True) - self.exOptionsButton.setObjectName("exOptionsButton") - self.optionsLayout.addWidget(self.exOptionsButton, 4, 0, 1, 4) - - #horizontal separater 1 - self.horiSepA = QtWidgets.QFrame(self.centerWidget) - self.horiSepA.setMinimumSize(QtCore.QSize(470, 30)) - self.horiSepA.setFrameShape(QtWidgets.QFrame.HLine) - self.horiSepA.setFrameShadow(QtWidgets.QFrame.Sunken) - self.horiSepA.setObjectName("horiSepA") - - #horizontal separater 2 - self.horiSepB = QtWidgets.QFrame(self.centerWidget) - self.horiSepB.setMinimumSize(QtCore.QSize(470, 30)) - self.horiSepB.setFrameShape(QtWidgets.QFrame.HLine) - self.horiSepB.setFrameShadow(QtWidgets.QFrame.Sunken) - self.horiSepB.setObjectName("horiSepB") - - #response panel - self.responses = QtWidgets.QPlainTextEdit(self.centerWidget) - self.responses.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.responses.sizePolicy().hasHeightForWidth()) - self.responses.setSizePolicy(sizePolicy) - self.responses.setMinimumSize(QtCore.QSize(474, 180)) - self.responses.setMaximumSize(QtCore.QSize(16777215, 180)) - font = QtGui.QFont() - font.setFamily("Consolas") - font.setPointSize(8) - font.setWeight(42) - fontMetrics = QtGui.QFontMetricsF(font) - spaceWidth = fontMetrics.width(' ') - self.responses.setFont(font) - self.responses.setPlainText("") - self.responses.setTabStopDistance(spaceWidth * 4) - self.responses.setReadOnly(True) - self.responses.setObjectName("responses") - - #Compile button - self.compileButton = QtWidgets.QPushButton(self.centerWidget) - font = QtGui.QFont("Helvetica") - font.setPointSize(34) - self.compileButton.setFont(font) - self.compileButton.setCheckable(False) - self.compileButton.setChecked(False) - self.compileButton.setAutoDefault(True) - self.compileButton.setDefault(False) - self.compileButton.setFlat(False) - self.compileButton.setDisabled(True) - self.compileButton.setObjectName("compileButton") - - self.gridLayout.addWidget(self.filesLabel, 0, 0, 1, 1) - self.gridLayout.addLayout(self.filesLayout, 1, 0, 1, 1) - self.gridLayout.addWidget(self.horiSepA, 2, 0, 1, 1) - self.gridLayout.addLayout(self.optionsLayout, 3, 0, 1, 1) - self.gridLayout.addWidget(self.horiSepB, 4, 0, 1, 1) - self.gridLayout.addWidget(self.responses, 5, 0, 1, 1) - self.gridLayout.addWidget(self.compileButton, 6, 0, 1, 1) - - self.setCentralWidget(self.centerWidget) - - #Toolbar - self.menubar = QtWidgets.QMenuBar(self) - self.menubar.setGeometry(QtCore.QRect(0, 0, 470, 22)) - self.menubar.setObjectName("menubar") - - self.menuFile = QtWidgets.QMenu(self.menubar) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.menuFile.setFont(font) - self.menuFile.setObjectName("menuFile") - - self.menuEdit = QtWidgets.QMenu(self.menubar) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.menuEdit.setFont(font) - self.menuEdit.setObjectName("menuEdit") - - self.menuHelp = QtWidgets.QMenu(self.menubar) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.menuHelp.setFont(font) - self.menuHelp.setObjectName("menuHelp") - - self.setMenuBar(self.menubar) - - self.actionOpen = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionOpen.setFont(font) - self.actionOpen.setObjectName("actionOpen") - - self.actionClose = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionClose.setFont(font) - self.actionClose.setObjectName("actionClose") - - self.actionSave = QtWidgets.QAction(self) - self.actionSave.setEnabled(False) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionSave.setFont(font) - self.actionSave.setObjectName("actionSave") - - self.actionSave_As = QtWidgets.QAction(self) - self.actionSave_As.setEnabled(False) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionSave_As.setFont(font) - self.actionSave_As.setObjectName("actionSave_As") - - self.actionUndo = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionUndo.setFont(font) - self.actionUndo.setMenuRole(QtWidgets.QAction.TextHeuristicRole) - self.actionUndo.setObjectName("actionUndo") - self.actionRedo = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionRedo.setFont(font) - self.actionRedo.setObjectName("actionRedo") - self.actionCut = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionCut.setFont(font) - self.actionCut.setObjectName("actionCut") - self.actionCopy = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionCopy.setFont(font) - self.actionCopy.setObjectName("actionCopy") - self.actionPaste = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionPaste.setFont(font) - self.actionPaste.setObjectName("actionPaste") - self.actionDelete = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionDelete.setFont(font) - self.actionDelete.setObjectName("actionDelete") - self.actionSelect_All = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionSelect_All.setFont(font) - self.actionSelect_All.setObjectName("actionSelect_All") - self.actionPreferences = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionPreferences.setFont(font) - self.actionPreferences.setMenuRole(QtWidgets.QAction.PreferencesRole) - self.actionPreferences.setObjectName("actionPreferences") - - self.actionAbout_GeckoLoader = QtWidgets.QAction(self) - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionAbout_GeckoLoader.setFont(font) - self.actionAbout_GeckoLoader.setMenuRole(QtWidgets.QAction.AboutRole) - self.actionAbout_GeckoLoader.setObjectName("actionAbout_GeckoLoader") - - self.actionAbout_Qt = QtWidgets.QAction(self) - self.actionAbout_Qt.setStatusTip("") - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionAbout_Qt.setFont(font) - self.actionAbout_Qt.setMenuRole(QtWidgets.QAction.AboutQtRole) - self.actionAbout_Qt.setObjectName("actionAbout_Qt") - - self.actionCheck_Update = QtWidgets.QAction(self) - self.actionCheck_Update.setStatusTip("") - font = QtGui.QFont() - font.setFamily("Helvetica") - self.actionCheck_Update.setFont(font) - self.actionCheck_Update.setObjectName("actionCheck_Update") - - self.menuFile.addAction(self.actionOpen) - self.menuFile.addAction(self.actionClose) - self.menuFile.addSeparator() - self.menuFile.addAction(self.actionSave) - self.menuFile.addAction(self.actionSave_As) - - self.menuEdit.addAction(self.actionPreferences) - - self.menuHelp.addAction(self.actionAbout_GeckoLoader) - self.menuHelp.addAction(self.actionAbout_Qt) - self.menuHelp.addAction(self.actionCheck_Update) - - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuEdit.menuAction()) - self.menubar.addAction(self.menuHelp.menuAction()) - - #Statusbar - self.statusbar = QtWidgets.QStatusBar(self) - self.statusbar.setObjectName("statusbar") - self.setStatusBar(self.statusbar) - - self.retranslate_ui() - self.set_edit_fields() - - QtCore.QMetaObject.connectSlotsByName(self) - - def _lstrip_textboxes(self): - attributes = [item for item in vars(self) if not item.startswith('__')] - - for item in attributes: - item = getattr(self, item) - if isinstance(item, QtWidgets.QLineEdit): - strlength = len(item.text()) - cursorPos = item.cursorPosition() - item.setText(item.text().lstrip()) - item.setCursorPosition(cursorPos - (strlength - len(item.text()))) - elif isinstance(item, QtWidgets.QPlainTextEdit): - sliderPos = item.verticalScrollBar().sliderPosition() - item.setPlainText(item.toPlainText().lstrip()) - item.verticalScrollBar().setSliderPosition(sliderPos) - - def set_edit_fields(self): - self.filesLabel.setEnabled(True) - self.dolTextBox.setEnabled(True) - self.destTextBox.setEnabled(True) - self.optionsLabel.setEnabled(True) - self.allocLabel.setEnabled(True) - self.allocLineEdit.setEnabled(True) - self.handlerTypeLabel.setEnabled(True) - self.handlerTypeSelect.setEnabled(True) - self.hookTypeLabel.setEnabled(True) - self.hookTypeSelect.setEnabled(True) - self.txtCodesIncludeLabel.setEnabled(True) - self.txtCodesIncludeSelect.setEnabled(True) - self.exOptionsButton.setEnabled(True) - self.actionSave.setEnabled(True) - self.actionSave_As.setEnabled(True) - - self._lstrip_textboxes() - - if self.gctFileTextBox.text() != "": - self.gctFileTextBox.setEnabled(True) - self.gctFolderTextBox.setDisabled(True) - elif self.gctFolderTextBox.text() != "": - self.gctFileTextBox.setDisabled(True) - self.gctFolderTextBox.setEnabled(True) - else: - self.gctFileTextBox.setEnabled(True) - self.gctFolderTextBox.setEnabled(True) - - if self.dolTextBox.text().lower().endswith(".dol") and len(self.dolTextBox.text()) > 4: - self.compileButton.setEnabled(self.gctFileTextBox.text() != "" or self.gctFolderTextBox.text() != "") - else: - self.compileButton.setDisabled(True) - - def retranslate_ui(self): - self.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", f"GeckoLoader {self.apiRevision} - untitled", None)) - self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "&File", None)) - self.menuEdit.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Edit", None)) - self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "&Help", None)) - self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "&Open Session...", None)) - self.actionOpen.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Open a session", None)) - self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None)) - self.actionClose.setText(QtWidgets.QApplication.translate("MainWindow", "&Close Session...", None)) - self.actionClose.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Close the current session", None)) - self.actionClose.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Shift+C", None)) - self.actionSave.setText(QtWidgets.QApplication.translate("MainWindow", "&Save Session", None)) - self.actionSave.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Save the current session", None)) - self.actionSave.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+S", None)) - self.actionSave_As.setText(QtWidgets.QApplication.translate("MainWindow", "&Save Session As...", None)) - self.actionSave_As.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Save the current session to the specified location", None)) - self.actionSave_As.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Shift+S", None)) - self.actionUndo.setText(QtWidgets.QApplication.translate("MainWindow", "Undo", None)) - self.actionUndo.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Undo the last action", None)) - self.actionUndo.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Z", None)) - self.actionRedo.setText(QtWidgets.QApplication.translate("MainWindow", "Redo", None)) - self.actionRedo.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Redo the last action", None)) - self.actionRedo.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Shift+Z", None)) - self.actionCut.setText(QtWidgets.QApplication.translate("MainWindow", "Cut", None)) - self.actionCut.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Cuts the selected text and places it on the clipboard", None)) - self.actionCut.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+X", None)) - self.actionCopy.setText(QtWidgets.QApplication.translate("MainWindow", "Copy", None)) - self.actionCopy.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Copies the selected text and places it on the clipboard", None)) - self.actionCopy.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+C", None)) - self.actionPaste.setText(QtWidgets.QApplication.translate("MainWindow", "Paste", None)) - self.actionPaste.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Paste the contents of the clipboard", None)) - self.actionPaste.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+V", None)) - self.actionDelete.setText(QtWidgets.QApplication.translate("MainWindow", "Delete", None)) - self.actionDelete.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Deletes the selected text", None)) - self.actionSelect_All.setText(QtWidgets.QApplication.translate("MainWindow", "Select All", None)) - self.actionSelect_All.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Select all of the text", None)) - self.actionSelect_All.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+A", None)) - self.actionPreferences.setText(QtWidgets.QApplication.translate("MainWindow", "&Preferences...", None)) - self.actionPreferences.setStatusTip(QtWidgets.QApplication.translate("MainWindow", "Open the application preferences dialog", None)) - self.actionAbout_GeckoLoader.setText(QtWidgets.QApplication.translate("MainWindow", "About &GeckoLoader...", None)) - self.actionAbout_Qt.setText(QtWidgets.QApplication.translate("MainWindow", "About &Qt...", None)) - self.actionCheck_Update.setText(QtWidgets.QApplication.translate("MainWindow", "&Check Update", None)) - - self.filesLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Files", None)) - - self.dolButton.setText(QtWidgets.QApplication.translate("MainWindow", "Open DOL", None)) - self.gctFileButton.setText(QtWidgets.QApplication.translate("MainWindow", "Open Codes", None)) - self.orFolderLabel.setText(QtWidgets.QApplication.translate("MainWindow", "-"*40 + "OR" + "-"*40, None)) - self.gctFolderButton.setText(QtWidgets.QApplication.translate("MainWindow", "Open Folder", None)) - self.destButton.setText(QtWidgets.QApplication.translate("MainWindow", "Destination", None)) - - self.optionsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Options", None)) - - self.allocLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Allocation", None)) - self.allocLineEdit.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "AUTO", None)) - - self.handlerTypeLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Codehandler", None)) - self.handlerTypeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "FULL", None)) - self.handlerTypeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "MINI", None)) - - self.hookTypeLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Code Hook", None)) - self.hookTypeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "VI", None)) - self.hookTypeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "GX", None)) - self.hookTypeSelect.setItemText(2, QtWidgets.QApplication.translate("Dialog", "PAD", None)) - - self.txtCodesIncludeLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Include Codes", None)) - self.txtCodesIncludeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "ACTIVE", None)) - self.txtCodesIncludeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "ALL", None)) - - self.exOptionsButton.setText(QtWidgets.QApplication.translate("MainWindow", "Advanced Settings", None)) - - self.compileButton.setText(QtWidgets.QApplication.translate("MainWindow", "RUN", None)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f5f9aed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +bs4 +colorama +dolreader +geckolibs +PySide6 \ No newline at end of file diff --git a/setup.py b/setup.py index 33c9fa1..f0341ba 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,32 @@ -import os -import sys -from cx_Freeze import setup, Executable +import geckoloader +import setuptools -include_files = [ "bin/" ] -excludes = [ "tkinter" ] +with open("README.md", "r", encoding="utf-8") as fh: + longDescription = fh.read() -options = { - "build_exe": { - "optimize": 4, - "excludes": excludes, - "include_files": include_files - } -} +with open("requirements.txt", "r", encoding="utf-8") as r: + requires = [x for x in r] -executables = [ - Executable("GeckoLoader.py"), -] +setuptools.setup( + name='geckoloader', + version=geckoloader.__version__, + description='Simple python library for extracting and rebuilding ISOs', + long_description=longDescription, + long_description_content_type="text/markdown", + url='https://github.com/JoshuaMKW/GeckoLoader', + author='JoshuaMK', + author_email='joshuamkw2002@gmail.com', + license='GNU General Public License v3.0', + packages=setuptools.find_packages(), + include_package_data=True, + install_requires=requires, -setup(name = "GeckoLoader", - version = "7.1.0", - description = "DOL Patcher for extending the codespace of Wii/GC games", - executables = [Executable("GeckoLoader.py", icon=os.path.join("bin", "icon.ico"))], - author = "JoshuaMK", - author_email = "joshuamkw2002@gmail.com", - options = options - ) \ No newline at end of file + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Operating System :: OS Independent' + ], + python_requires='>=3.8', +) \ No newline at end of file