diff --git a/GeckoLoader.py b/GeckoLoader.py index 5e68cab..e2c498f 100644 --- a/GeckoLoader.py +++ b/GeckoLoader.py @@ -1,6 +1,5 @@ #Written by JoshuaMK 2020 -import argparse import atexit import logging import os @@ -13,6 +12,7 @@ import tempfile from contextlib import redirect_stdout, redirect_stderr from distutils.version import LooseVersion from io import StringIO +from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets @@ -45,15 +45,15 @@ except ImportError: TRED = '' TREDLIT = '' -__version__ = "v7.0.0" +__version__ = "v7.1.0" -TMPDIR = tempfile.mkdtemp("GeckoLoader-") +TMPDIR = Path(tempfile.mkdtemp(prefix="GeckoLoader-")) @atexit.register def clean_tmp_resources(): - tmpfolder = os.path.dirname(TMPDIR) - for entry in os.listdir(tmpfolder): - if entry.startswith("GeckoLoader-"): + tmpfolder = TMPDIR.parent + for entry in tmpfolder.iterdir(): + if entry.name.startswith("GeckoLoader-"): shutil.rmtree(entry, ignore_errors=True) class GeckoLoaderCli(CommandLineParser): @@ -71,13 +71,6 @@ class GeckoLoaderCli(CommandLineParser): self.add_argument('-i', '--init', help='Define where GeckoLoader is initialized in hex', metavar='ADDRESS') - self.add_argument('-m', '--movecodes', - help='''["AUTO", "LEGACY", "ARENA"] Choose if GeckoLoader moves the codes to OSArenaHi, - or the legacy space. Default is "AUTO", - which auto decides where to insert the codes''', - default='AUTO', - choices=['AUTO', 'LEGACY', 'ARENA'], - metavar='TYPE') self.add_argument('-tc', '--txtcodes', help='''["ACTIVE", "ALL"] What codes get parsed when a txt file is used. "ALL" makes all codes get parsed, @@ -173,11 +166,11 @@ class GeckoLoaderCli(CommandLineParser): tag, status = repoChecker.get_newest_version() - print('') - 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)) @@ -211,9 +204,9 @@ class GeckoLoaderCli(CommandLineParser): _codehook = None if args.handler == CodeHandler.Types.MINI: - codeHandlerFile = 'codehandler-mini.bin' + codeHandlerFile = Path('bin/codehandler-mini.bin') elif args.handler == CodeHandler.Types.FULL: - codeHandlerFile = 'codehandler.bin' + codeHandlerFile = Path('bin/codehandler.bin') else: self.error(color_text(f'Codehandler type {args.handler} is invalid\n', defaultColor=TREDLIT)) @@ -226,23 +219,13 @@ class GeckoLoaderCli(CommandLineParser): return _allocation, _codehook, codeHandlerFile def _exec(self, args, tmpdir): - if not os.path.isabs(args.dolfile): - args.dolfile = os.path.abspath(args.dolfile) - - if not os.path.isabs(args.codelist): - args.codelist = os.path.abspath(args.codelist) - - if args.dest: - if not os.path.isabs(args.dest): - args.dest = os.path.abspath(args.dest) - _allocation, _codehook, codeHandlerFile = self._validate_args(args) try: - with open(os.path.normpath(args.dolfile), 'rb') as dol: + with open(args.dolfile, "rb") as dol: dolFile = DolFile(dol) - with open(resource_path(os.path.join('bin', os.path.normpath(codeHandlerFile))), 'rb') as handler: + with resource_path(str(codeHandlerFile)).open("rb") as handler: codeHandler = CodeHandler(handler) codeHandler.allocation = _allocation codeHandler.hookAddress = _codehook @@ -250,36 +233,28 @@ class GeckoLoaderCli(CommandLineParser): codeHandler.includeAll = args.txtcodes.lower() == 'all' codeHandler.optimizeList = args.optimize - with open(resource_path(os.path.join('bin', 'geckoloader.bin')), 'rb') as kernelfile: + with resource_path("bin/geckoloader.bin").open("rb") as kernelfile: geckoKernel = KernelLoader(kernelfile, cli) if args.init is not None: geckoKernel.initAddress = int(args.init, 16) - geckoKernel.patchJob = args.movecodes geckoKernel.verbosity = args.verbose geckoKernel.quiet = args.quiet geckoKernel.encrypt = args.encrypt geckoKernel.protect = args.protect if args.dest: - if not os.path.isabs(args.dest): - if os.path.splitext(args.dest)[1] == "": - dest = os.path.normpath(os.path.join(os.getcwd(), args.dest.lstrip('.').lstrip('\\').lstrip('/'), os.path.basename(args.dolfile))) - else: - dest = os.path.normpath(os.path.join(os.getcwd(), args.dest.lstrip('.').lstrip('\\').lstrip('/'))) - else: - if os.path.splitext(args.dest)[1] == "": - dest = os.path.normpath(os.path.join(args.dest.lstrip('.').lstrip('\\').lstrip('/'), os.path.basename(args.dolfile))) - else: - dest = os.path.normpath(os.path.join(args.dest.lstrip('.').lstrip('\\').lstrip('/'))) + dest = Path(args.dest).resolve() + if dest.suffix == "": + dest = dest / args.dolfile.name else: - dest = os.path.normpath(os.path.join(os.getcwd(), "geckoloader-build", os.path.basename(args.dolfile))) + dest = Path.cwd() / "geckoloader-build" / args.dolfile.name - if not os.path.exists(dest) and os.path.dirname(dest) not in ('', '/'): - os.makedirs(os.path.dirname(dest), exist_ok=True) - - geckoKernel.build(args.codelist, dolFile, codeHandler, TMPDIR, dest) + if not dest.parent.exists(): + dest.parent.mkdir(parents=True, exist_ok=True) + + geckoKernel.build(Path(args.codelist), dolFile, codeHandler, TMPDIR, dest) except FileNotFoundError as e: self.error(color_text(e, defaultColor=TREDLIT)) @@ -309,12 +284,12 @@ class GUI(object): self.style_log = [] self.compileCount = 0 - self.log = logging.getLogger(f"GeckoLoader {self.cli.__version__}") + self.log = logging.getLogger(f"GeckoLoader {self.version}") - if not os.path.exists(get_program_folder("GeckoLoader")): - os.mkdir(get_program_folder("GeckoLoader")) + if not get_program_folder("GeckoLoader").exists(): + get_program_folder("GeckoLoader").mkdir() - hdlr = logging.FileHandler(os.path.join(get_program_folder("GeckoLoader"), "error.log")) + hdlr = logging.FileHandler(get_program_folder("GeckoLoader") / "error.log") formatter = logging.Formatter("\n%(levelname)s (%(asctime)s): %(message)s") hdlr.setFormatter(formatter) self.log.addHandler(hdlr) @@ -329,7 +304,7 @@ class GUI(object): "pre-patching codes, dynamic codehandler hooks, codespace ", "extension through memory reallocation, multiple patching ", "of a single DOL, and more.\n\n", - f"Current running version: {self.cli.__version__}\n\n" + f"Current running version: {self.version}\n\n" "Copyright (c) 2020\n\n", "JoshuaMK \n\n", "All rights reserved." ]) @@ -340,21 +315,25 @@ class GUI(object): else: self.uiexSettings.show() + @property + def version(self) -> str: + return self.cli.__version__ + def _open_dol(self) -> tuple: if self.dolPath is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open DOL", os.path.expanduser("~"), + fname = str(QtWidgets.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", os.path.split(self.dolPath)[0], + fname = str(QtWidgets.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 return False, None else: - self.dolPath = os.path.normpath(fname) + self.dolPath = Path(fname).resolve() - if os.path.isfile(self.dolPath): - self.ui.dolTextBox.setText(self.dolPath) + if self.dolPath.is_file(): + self.ui.dolTextBox.setText(str(self.dolPath)) return True, None else: return False, "The file does not exist!" @@ -362,63 +341,63 @@ class GUI(object): def _load_codes(self, isFolder: bool=False) -> tuple: if not isFolder: if self.codePath[0] is None: # Just start in the home directory - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Codelist", os.path.expanduser("~"), + fname = str(QtWidgets.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", os.path.split(self.codePath[0])[0], + fname = str(QtWidgets.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", os.path.expanduser("~"), - QtWidgets.QFileDialog.ShowDirsOnly)) + fname = str(QtWidgets.QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(Path.home()), + QtWidgets.QFileDialog.ShowDirsOnly)) else: # Start in the last directory used by the user - fname = str(QtWidgets.QFileDialog.getExistingDirectory(self.ui, "Open Codelist", os.path.split(self.codePath[0])[0], + fname = str(QtWidgets.QFileDialog.getExistingDirectory(self.ui, "Open Codelist", str(self.codePath[0].parent), QtWidgets.QFileDialog.ShowDirsOnly)) if fname == "" or fname is None: # Make sure we have something to open return False, None else: - self.codePath = [os.path.normpath(fname), isFolder] + self.codePath = [Path(fname).resolve(), isFolder] if not isFolder: - self.ui.gctFileTextBox.setText(self.codePath[0]) + self.ui.gctFileTextBox.setText(str(self.codePath[0])) self.ui.gctFolderTextBox.setText("") else: self.ui.gctFileTextBox.setText("") - self.ui.gctFolderTextBox.setText(self.codePath[0]) + self.ui.gctFolderTextBox.setText(str(self.codePath[0])) return True, None 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", os.path.expanduser("~"), + fname = str(QtWidgets.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", os.path.split(self.dolPath)[0], + fname = str(QtWidgets.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 return False, None else: - self.destPath = os.path.normpath(fname) - self.ui.destTextBox.setText(self.destPath) + self.destPath = Path(fname).resolve() + self.ui.destTextBox.setText(str(self.destPath)) return True, None def _load_session(self) -> tuple: if self.sessionPath is None: - fname = str(QtWidgets.QFileDialog.getOpenFileName(self.ui, "Open Session", os.path.expanduser("~"), + fname = str(QtWidgets.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", os.path.split(self.sessionPath)[0], + fname = str(QtWidgets.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 return False, None else: - self.sessionPath = os.path.normpath(fname) + self.sessionPath = Path(fname).resolve() - with open(self.sessionPath, "rb") as session: + with self.sessionPath.open("rb") as session: p = cPickle.load(session) self.ui.dolTextBox.setText(p["dolPath"]) @@ -435,11 +414,10 @@ class GUI(object): self.ui.destTextBox.setText(p["destPath"]) self.ui.allocLineEdit.setText(p["alloc"]) - self.ui.patchTypeSelect.setCurrentIndex(p["patchIndex"]) self.ui.handlerTypeSelect.setCurrentIndex(p["handlerIndex"]) self.ui.hookTypeSelect.setCurrentIndex(p["hookIndex"]) self.ui.txtCodesIncludeSelect.setCurrentIndex(p["txtIndex"]) - self.ui.optimizeSelect.setCurrentIndex(p["optimizeIndex"]) + self.uiexSettings.optimizeCodes.setChecked(p["optimize"]) self.uiexSettings.protectCodes.setChecked(p["protect"]) self.uiexSettings.encryptCodes.setChecked(p["encrypt"]) self.uiexSettings.codehookLineEdit.setText(p["hookAddress"]) @@ -451,19 +429,19 @@ class GUI(object): 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", os.path.expanduser("~"), + fname = str(QtWidgets.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", os.path.split(self.dolPath)[0], + fname = str(QtWidgets.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 return False, None else: - self.sessionPath = os.path.normpath(fname) + self.sessionPath = Path(fname).resolve() try: - with open(self.sessionPath, "wb") as session: + with self.sessionPath.open("wb") as session: p = {} p["dolPath"] = self.ui.dolTextBox.text().strip() @@ -471,11 +449,10 @@ class GUI(object): p["gctFolderPath"] = self.ui.gctFolderTextBox.text().strip() p["destPath"] = self.ui.destTextBox.text().strip() p["alloc"] = self.ui.allocLineEdit.text().strip() - p["patchIndex"] = self.ui.patchTypeSelect.currentIndex() p["handlerIndex"] = self.ui.handlerTypeSelect.currentIndex() p["hookIndex"] = self.ui.hookTypeSelect.currentIndex() p["txtIndex"] = self.ui.txtCodesIncludeSelect.currentIndex() - p["optimizeIndex"] = self.ui.optimizeSelect.currentIndex() + p["optimize"] = self.uiexSettings.optimizeCodes.isChecked() p["protect"] = self.uiexSettings.protectCodes.isChecked() p["encrypt"] = self.uiexSettings.encryptCodes.isChecked() p["hookAddress"] = self.uiexSettings.codehookLineEdit.text().strip() @@ -533,7 +510,6 @@ class GUI(object): self.ui.gctFolderTextBox.setText("") self.ui.destTextBox.setText("") self.ui.allocLineEdit.setText("") - self.ui.patchTypeSelect.setCurrentIndex(0) self.ui.handlerTypeSelect.setCurrentIndex(0) self.ui.hookTypeSelect.setCurrentIndex(0) self.ui.txtCodesIncludeSelect.setCurrentIndex(0) @@ -545,13 +521,12 @@ class GUI(object): self.uiexSettings.kernelHookLineEdit.setText("") self.uiexSettings.verbositySelect.setCurrentIndex(0) self.ui.set_edit_fields() - #Reset all ui elements as needed def load_prefs(self): datapath = get_program_folder("GeckoLoader") try: - with open(os.path.join(datapath, ".GeckoLoader.conf"), "rb") as f: + with (datapath / ".GeckoLoader.conf").open("rb") as f: try: p = cPickle.load(f) except cPickle.UnpicklingError as e: @@ -587,13 +562,13 @@ class GUI(object): self.prefs["darktheme"] = self.uiprefs.qtdarkButton.isChecked() try: - with open(os.path.join(datapath, ".GeckoLoader.conf"), "wb") as f: + with (datapath / ".GeckoLoader.conf").open("wb") as f: cPickle.dump(self.prefs, f) except IOError as e: self.log.exception(e) def load_qtstyle(self, style, first_style_load=False): - self.style_log.append( [self.app.style, self.uiprefs.qtstyleSelect.currentText()] ) + self.style_log.append([self.app.style, self.uiprefs.qtstyleSelect.currentText()]) if len(self.style_log) > 2: self.style_log.pop(0) @@ -631,7 +606,7 @@ class GUI(object): _status = True icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(resource_path(os.path.join("bin", "icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(resource_path(Path("bin/icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) if _status is False: reply = QtWidgets.QErrorMessage() @@ -707,33 +682,32 @@ class GUI(object): self.compileCount += 1 if self.ui.dolTextBox.isEnabled and self.ui.dolTextBox.text().strip() != "": - dol = os.path.normpath(self.ui.dolTextBox.text().strip()) + dol = self.ui.dolTextBox.text().strip() else: self.ui.responses.appendPlainText("DOL is missing, please add the path to your codes in the respective textbox" + "\n\n") return if self.ui.gctFileTextBox.isEnabled and self.ui.gctFileTextBox.text().strip() != "": - gct = os.path.normpath(self.ui.gctFileTextBox.text().strip()) + gct = self.ui.gctFileTextBox.text().strip() elif self.ui.gctFolderTextBox.isEnabled and self.ui.gctFolderTextBox.text().strip() != "": - gct = os.path.normpath(self.ui.gctFolderTextBox.text().strip()) + gct = self.ui.gctFolderTextBox.text().strip() else: self.ui.responses.appendPlainText("GCT is missing, please add the path to your codes in the respective textbox" + "\n\n") return alloc = self.ui.allocLineEdit.text().strip() - patchJob = self.ui.patchTypeSelect.currentText().strip() hookType = self.ui.hookTypeSelect.currentText().strip() hookAddress = self.uiexSettings.codehookLineEdit.text().strip() initAddress = self.uiexSettings.kernelHookLineEdit.text().strip() txtInclude = self.ui.txtCodesIncludeSelect.currentText().strip() codeHandlerType = self.ui.handlerTypeSelect.currentText().strip() - optimize = self.ui.optimizeSelect.currentText().strip() == "TRUE" + optimize = self.uiexSettings.optimizeCodes.isChecked() protect = self.uiexSettings.protectCodes.isChecked() encrypt = self.uiexSettings.encryptCodes.isChecked() verbosity = int(self.uiexSettings.verbositySelect.currentText().strip()) dest = self.ui.destTextBox.text().strip() - argslist = [ dol, gct, "-m", patchJob, "-t", txtInclude, "--handler", codeHandlerType, "--hooktype", hookType ] + argslist = [ dol, gct, "-t", txtInclude, "--handler", codeHandlerType, "--hooktype", hookType ] if alloc != "": argslist.append("-a") @@ -802,23 +776,23 @@ class GUI(object): def run(self): if sys.platform != "win32": - datapath = os.path.join(os.getenv("HOME"), ".GeckoLoader") + datapath = Path.home() / ".GeckoLoader" else: - datapath = os.path.join(os.getenv("APPDATA"), "GeckoLoader") + datapath = Path(os.getenv("APPDATA")) / "GeckoLoader" - if not os.path.isdir(datapath): - os.mkdir(datapath) + if not datapath.is_dir(): + datapath.mkdir() self.app = QtWidgets.QApplication(sys.argv) self.default_qtstyle = self.app.style().objectName() - self.ui = MainWindow(self.cli.__version__) + self.ui = MainWindow(self.version) self.uiprefs = PrefWindow() self.uiexSettings = SettingsWindow() self.uiprefs.qtstyleSelect.addItem("Default") - for i in range(0, len(list(QtWidgets.QStyleFactory.keys()))): - self.uiprefs.qtstyleSelect.addItem(list(QtWidgets.QStyleFactory.keys())[i]) + styleKeys = list(QtWidgets.QStyleFactory.keys()) + self.uiprefs.qtstyleSelect.addItems(styleKeys) self.load_prefs() self.load_qtstyle(self.prefs.get("qtstyle"), True) @@ -841,14 +815,10 @@ if __name__ == "__main__": app = GUI(cli) signal.signal(signal.SIGINT, signal.SIG_DFL) app.run() - sys.exit(1) #Should never reach here - elif '--checkupdate' in sys.argv: cli.check_updates() - sys.exit(0) elif '--splash' in sys.argv: cli.print_splash() - sys.exit(0) - - args = cli.parse_args() - cli._exec(args, TMPDIR) + else: + args = cli.parse_args() + cli._exec(args, TMPDIR) diff --git a/children_ui.py b/children_ui.py index f03b45d..ba943b8 100644 --- a/children_ui.py +++ b/children_ui.py @@ -17,7 +17,7 @@ class PrefWindow(QtWidgets.QDialog): self.setFixedSize(300, 120) self.setModal(True) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(resource_path(os.path.join("bin", "icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(str(resource_path(os.path.join("bin", "icon.ico")))), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) #Buttonbox @@ -73,13 +73,13 @@ class SettingsWindow(QtWidgets.QDialog): self.setObjectName("Dialog") if sys.platform == "win32": - self.setFixedSize(300, 210) + self.setFixedSize(300, 240) else: - self.setFixedSize(370, 210) + self.setFixedSize(370, 240) self.setModal(True) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(resource_path(os.path.join("bin", "icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(str(resource_path(os.path.join("bin", "icon.ico")))), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) #Buttonbox @@ -113,10 +113,17 @@ class SettingsWindow(QtWidgets.QDialog): self.encryptCodes.setText("Encrypt codes") self.comboBoxLayout.addWidget(self.encryptCodes, 2, 0, 1, 1) + #optimize codes + self.optimizeCodes = QtWidgets.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() self.codehookLabel.setObjectName("codehookLabel") - self.comboBoxLayout.addWidget(self.codehookLabel, 3, 0, 1, 1) + self.comboBoxLayout.addWidget(self.codehookLabel, 4, 0, 1, 1) #Codehook Address Textbox self.codehookLineEdit = QtWidgets.QLineEdit() @@ -137,12 +144,12 @@ class SettingsWindow(QtWidgets.QDialog): self.codehookLineEdit.setMaxLength(8) self.codehookLineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) self.codehookLineEdit.setObjectName("codehookLineEdit") - self.comboBoxLayout.addWidget(self.codehookLineEdit, 3, 1, 1, 1) + self.comboBoxLayout.addWidget(self.codehookLineEdit, 4, 1, 1, 1) #kernelHook Address Label self.kernelHookLabel = QtWidgets.QLabel() self.kernelHookLabel.setObjectName("kernelHookLabel") - self.comboBoxLayout.addWidget(self.kernelHookLabel, 4, 0, 1, 1) + self.comboBoxLayout.addWidget(self.kernelHookLabel, 5, 0, 1, 1) #kernelHook Address Textbox self.kernelHookLineEdit = QtWidgets.QLineEdit() @@ -163,19 +170,18 @@ class SettingsWindow(QtWidgets.QDialog): self.kernelHookLineEdit.setMaxLength(8) self.kernelHookLineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) self.kernelHookLineEdit.setObjectName("kernelHookLineEdit") - self.comboBoxLayout.addWidget(self.kernelHookLineEdit, 4, 1, 1, 1) + self.comboBoxLayout.addWidget(self.kernelHookLineEdit, 5, 1, 1, 1) #verbosity label self.verbosityLabel = QtWidgets.QLabel() self.verbosityLabel.setObjectName("verbosityLabel") - self.comboBoxLayout.addWidget(self.verbosityLabel, 5, 0, 1, 1) + self.comboBoxLayout.addWidget(self.verbosityLabel, 6, 0, 1, 1) #verbosity box self.verbositySelect = QtWidgets.QComboBox() self.verbositySelect.addItems(["1", "2", "3", "0"]) self.verbositySelect.setObjectName("verbositySelect") - self.comboBoxLayout.addWidget(self.verbositySelect, 5, 1, 1, 1) - + self.comboBoxLayout.addWidget(self.verbositySelect, 6, 1, 1, 1) self.formLayoutWidget.addLayout(self.comboBoxLayout, 0, 0, 1, 1) self.formLayoutWidget.addWidget(self.buttonBox, 1, 0, 1, 1) @@ -189,6 +195,7 @@ class SettingsWindow(QtWidgets.QDialog): def set_edit_fields(self): self.protectCodes.setEnabled(True) self.encryptCodes.setEnabled(True) + self.optimizeCodes.setEnabled(True) self.codehookLineEdit.setEnabled(True) self.kernelHookLineEdit.setEnabled(True) self.verbositySelect.setEnabled(True) diff --git a/dolreader.py b/dolreader.py index 427e5ae..7b7ef5c 100644 --- a/dolreader.py +++ b/dolreader.py @@ -45,9 +45,9 @@ class DolFile(object): f.seek(offset) data = BytesIO(f.read(size)) if i < DolFile.maxTextSections: - self.textSections.append([offset, address, size, data, DolFile.SectionType.Text]) + self.textSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Text}) else: - self.dataSections.append([offset, address, size, data, DolFile.SectionType.Data]) + self.dataSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Data}) f.seek(DolFile.bssInfoLoc) self.bssAddress = read_uint32(f) @@ -56,38 +56,38 @@ class DolFile(object): f.seek(DolFile.entryInfoLoc) self.entryPoint = read_uint32(f) - self._currLogicAddr = self.get_first_section()[1] + self._currLogicAddr = self.first_section["address"] self.seek(self._currLogicAddr) f.seek(0) - def __str__(self): - return "Nintendo DOL format executable for the Wii and Gamecube" + def __repr__(self) -> str: + return f"repr={vars(self)}" + + def __str__(self) -> str: + return f"Nintendo DOL executable {self.__repr__()}" - # Internal function for 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 offset, address, size, data, sectiontype in self.textSections: - if address <= gcAddr < address+size: - return offset, address, size, data, sectiontype - for offset, address, size, data, sectiontype in self.dataSections: - if address <= gcAddr < address+size: - return offset, address, size, data, sectiontype + 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 _, address, size, _, _ in self.textSections: - if address > (gcAddr + buffer) or address+size < gcAddr: + + for section in self.sections: + if section["address"] > (gcAddr + buffer) or (section["address"] + section["size"]) < gcAddr: continue - gcAddr = address + size - for _, address, size, _, _ in self.dataSections: - if address > (gcAddr + buffer) or address+size < gcAddr: - continue - gcAddr = address + size + gcAddr = section["address"] + section["size"] + + try: + self.resolve_address(gcAddr) + except UnmappedAddressError: + break return gcAddr @property @@ -98,24 +98,23 @@ class DolFile(object): yield i for i in self.dataSections: yield i - - return - def get_final_section(self) -> tuple: + @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, sectionData in enumerate(self.textSections): - if sectionData[0] > largestOffset: - largestOffset = sectionData[0] + for i, section in enumerate(self.textSections): + if section["offset"] > largestOffset: + largestOffset = section["offset"] indexToTarget = i targetType = DolFile.SectionType.Text - for i, sectionData in enumerate(self.dataSections): - if sectionData[0] > largestOffset: - largestOffset = sectionData[0] + for i, section in enumerate(self.dataSections): + if section["offset"] > largestOffset: + largestOffset = section["offset"] indexToTarget = i targetType = DolFile.SectionType.Data @@ -124,21 +123,22 @@ class DolFile(object): else: return self.dataSections[indexToTarget] - def get_first_section(self) -> tuple: + @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, sectionData in enumerate(self.textSections): - if sectionData[0] < smallestOffset: - smallestOffset = sectionData[0] + for i, section in enumerate(self.textSections): + if section["offset"] < smallestOffset: + smallestOffset = section["offset"] indexToTarget = i targetType = DolFile.SectionType.Text - for i, sectionData in enumerate(self.dataSections): - if sectionData[0] < smallestOffset: - smallestOffset = sectionData[0] + for i, section in enumerate(self.dataSections): + if section["offset"] < smallestOffset: + smallestOffset = section["offset"] indexToTarget = i targetType = DolFile.SectionType.Data @@ -149,32 +149,32 @@ class DolFile(object): # Unsupported: Reading an entire dol file # Assumption: A read should not go beyond the current section - def read(self, _size) -> bytes: - _, address, size, data, _ = self.resolve_address(self._currLogicAddr) - if self._currLogicAddr + _size > address + size: + 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 data.read(_size) + return section["data"].read(_size) # Assumption: A write should not go beyond the current section - def write(self, _data): - _, address, size, data, _ = self.resolve_address(self._currLogicAddr) - if self._currLogicAddr + len(_data) > address + size: + 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") - data.write(_data) + section["data"].write(_data) self._currLogicAddr += len(_data) - def seek(self, where, whence=0): + def seek(self, where: int, whence: int = 0): if whence == 0: - _, address, _, data, _ = self.resolve_address(where) - data.seek(where - address) + section = self.resolve_address(where) + section["data"].seek(where - section["address"]) self._currLogicAddr = where elif whence == 1: - _, address, _, data, _ = self.resolve_address(self._currLogicAddr + where) - data.seek((self._currLogicAddr + where) - address) + section = self.resolve_address(self._currLogicAddr + where) + section["data"].seek((self._currLogicAddr + where) - section["address"]) self._currLogicAddr += where else: @@ -185,29 +185,23 @@ class DolFile(object): def save(self, f): f.seek(0) - f.write(b"\x00" * self.get_full_size()) + f.write(b"\x00" * self.size) - for i in range(DolFile.maxTextSections + DolFile.maxDataSections): - if i < DolFile.maxTextSections: - if i < len(self.textSections): - offset, address, size, data, _ = self.textSections[i] - else: - continue + for i, section in enumerate(self.sections): + if section["type"] == DolFile.SectionType.Data: + entry = i + (DolFile.maxTextSections - len(self.textSections)) else: - if i - DolFile.maxTextSections < len(self.dataSections): - offset, address, size, data, _ = self.dataSections[i - DolFile.maxTextSections] - else: - continue + entry = i - f.seek(DolFile.offsetInfoLoc + (i << 2)) - write_uint32(f, offset) #offset in file - f.seek(DolFile.addressInfoLoc + (i << 2)) - write_uint32(f, address) #game address - f.seek(DolFile.sizeInfoLoc + (i << 2)) - write_uint32(f, size) #size in file + 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(offset) - f.write(data.getbuffer()) + f.seek(section["offset"]) + f.write(section["data"].getbuffer()) f.seek(DolFile.bssInfoLoc) write_uint32(f, self.bssAddress) @@ -217,10 +211,11 @@ class DolFile(object): write_uint32(f, self.entryPoint) align_byte_size(f, 256) - def get_full_size(self) -> int: + @property + def size(self) -> int: try: - offset, _, size, _, _ = self.get_final_section() - return (offset + size + 255) & -256 + section = self.last_section + return (section["offset"] + section["size"] + 255) & -256 except IndexError: return 0x100 @@ -229,20 +224,20 @@ class DolFile(object): section: DolFile.SectionType """ if section == DolFile.SectionType.Text: - return self.textSections[index][2] + return self.textSections[index]["size"] else: - return self.dataSections[index][2] + return self.dataSections[index]["size"] - def append_text_sections(self, sectionsList: list) -> bool: + 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}") - fOffset, _, fSize, _, _ = self.get_final_section() - _, pAddress, pSize, _, _ = self.textSections[len(self.textSections) - 1] + finalSection = self.last_section + lastSection = self.textSections[len(self.textSections) - 1] data, address = dataSet if not hasattr(data, "getbuffer"): @@ -252,7 +247,7 @@ class DolFile(object): else: data = BytesIO(data) - offset = fOffset + fSize + offset = finalSection["offset"] + finalSection["size"] if i < len(sectionsList) - 1: size = (len(data.getbuffer()) + 31) & -32 @@ -260,22 +255,22 @@ class DolFile(object): size = (len(data.getbuffer()) + 255) & -256 if address is None: - address = self.seek_nearest_unmapped(pAddress + pSize, size) + 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, address, size, data, DolFile.SectionType.Text)) + self.textSections.append({"offset": offset, "address": address, "size": size, "data": data, "type": DolFile.SectionType.Text}) - def append_data_sections(self, sectionsList: list) -> bool: + 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}") - fOffset, _, fSize, _, _ = self.get_final_section() - _, pAddress, pSize, _, _ = self.dataSections[len(self.dataSections) - 1] + finalSection = self.last_section + lastSection = self.dataSections[len(self.dataSections) - 1] data, address = dataSet if not hasattr(data, "getbuffer"): @@ -285,7 +280,7 @@ class DolFile(object): else: data = BytesIO(data) - offset = fOffset + fSize + offset = finalSection["offset"] + finalSection["size"] if i < len(sectionsList) - 1: size = (len(data.getbuffer()) + 31) & -32 @@ -293,12 +288,12 @@ class DolFile(object): size = (len(data.getbuffer()) + 255) & -256 if address is None: - address = self.seek_nearest_unmapped(pAddress + pSize, size) + 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, address, size, data, DolFile.SectionType.Data)) + 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 @@ -309,7 +304,6 @@ class DolFile(object): _from &= 0xFFFFFFFC to &= 0xFFFFFFFC self.seek(_from) - print(hex(to), hex(_from), hex((to - _from) & 0x3FFFFFD | 0x48000000 | lk)) write_uint32(self, (to - _from) & 0x3FFFFFD | 0x48000000 | lk) def extract_branch_addr(self, bAddr: int) -> tuple: @@ -389,7 +383,7 @@ class DolFile(object): 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.get_full_size():X}" ] + "File length:".ljust(16, " ") + f"0x{self.size:X}" ] print("\n".join(info) + "\n") diff --git a/fileutils.py b/fileutils.py index 8b173ed..6b1bffa 100644 --- a/fileutils.py +++ b/fileutils.py @@ -2,30 +2,32 @@ import os import struct import sys +from pathlib import Path + from tools import align_byte_size, get_alignment -def resource_path(relative_path: str = "") -> str: +def resource_path(relative_path: 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 = os.path.dirname(sys.executable) + base_path = Path(sys.executable).parent else: - base_path = os.path.dirname(os.path.abspath(__file__)) + base_path = Path(__file__).parent - return os.path.join(base_path, relative_path) + return base_path / relative_path -def get_program_folder(folder: str = "") -> str: +def get_program_folder(folder: str = "") -> Path: """ Get path to appdata """ if sys.platform == "win32": - datapath = os.path.join(os.getenv("APPDATA"), folder) + datapath = Path(os.getenv("APPDATA")) / folder elif sys.platform == "darwin": if folder: folder = "." + folder - datapath = os.path.join(os.path.expanduser("~"), "Library", "Application Support", folder) + datapath = Path("~/Library/Application Support").expanduser() / folder elif "linux" in sys.platform: if folder: folder = "." + folder - datapath = os.path.join(os.getenv("HOME"), folder) + datapath = Path.home() / folder else: raise NotImplementedError(f"{sys.platform} OS is unsupported") return datapath @@ -82,5 +84,4 @@ def read_bool(f, vSize=1): return struct.unpack("B", f.read(vSize))[0] > 0 def write_bool(f, val, vSize=1): - if val is True: f.write(b'\x00'*(vSize-1) + b'\x01') - else: f.write(b'\x00' * vSize) + f.write(b'\x00'*(vSize-1) + b'\x01') if val is True else f.write(b'\x00' * vSize) \ No newline at end of file diff --git a/kernel.py b/kernel.py index 19f6b6b..297cdba 100644 --- a/kernel.py +++ b/kernel.py @@ -4,10 +4,12 @@ import re import sys import time import functools + from io import BytesIO +from pathlib import Path import tools -from fileutils import * +from fileutils import read_uint16, read_uint32, write_bool, write_ubyte, write_uint16, write_uint32, write_sint32, get_alignment from dolreader import DolFile, SectionCountFullError, UnmappedAddressError try: @@ -22,7 +24,7 @@ def timer(func): 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)) + print(tools.color_text(f"\n :: Completed in {(end - start):0.4f} seconds!\n", defaultColor=tools.TGREENLIT)) return value return wrapper @@ -34,53 +36,53 @@ class GCT(object): self.codeList = BytesIO(f.read()) self.rawLineCount = tools.stream_size(self.codeList) >> 3 self.lineCount = self.rawLineCount - 2 - self.size = tools.stream_size(self.codeList) 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) + 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')): + 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"\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"\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) + 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 + codelist = b"\x00\xD0\xC0\xDE"*2 skipcodes = 0 - with open(os.path.join(os.path.expanduser("~"), "Desktop", "suss.gct"), "wb") as suss: - suss.write(self.codeList.getbuffer()) - 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) + 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')): + 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) + counter = int.from_bytes(info[:-2], byteorder="big", signed=False) value = info[2:] while counter + 1 > 0: @@ -88,11 +90,11 @@ class GCT(object): counter -= 1 continue - elif (codetype.startswith(b'\x02') or codetype.startswith(b'\x03') - or codetype.startswith(b'\x12') or codetype.startswith(b'\x13')): + 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) + counter = int.from_bytes(info[:-2], byteorder="big", signed=False) value = info[2:] while counter + 1 > 0: @@ -100,17 +102,17 @@ class GCT(object): counter -= 1 continue - elif (codetype.startswith(b'\x04') or codetype.startswith(b'\x05') - or codetype.startswith(b'\x14') or codetype.startswith(b'\x15')): + 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')): + 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) + arraylength = int.from_bytes(info, byteorder="big", signed=False) padding = get_alignment(arraylength, 8) dolFile.write(self.codeList.read(arraylength)) @@ -118,29 +120,29 @@ class GCT(object): 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')): + 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) + value = int.from_bytes(info, byteorder="big", signed=False) data = read_uint16(self.codeList) - size = data & 0x3 + 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) + write_ubyte(dolFile, value & 0xFF) dolFile.seek(-1, 1) elif size == 1: - write_uint16(dolFile, value) + 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)) + raise ValueError("Size type {} does not match 08 codetype specs".format(size)) dolFile.seek(address_increment, 1) value += value_increment @@ -149,19 +151,19 @@ class GCT(object): 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) + 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'): + if codetype.hex().startswith("2") or codetype.hex().startswith("3"): skipcodes += 1 - elif codetype.startswith(b'\xE0'): + elif codetype.startswith(b"\xE0"): skipcodes -= 1 - elif codetype.startswith(b'\xF0'): - codelist += b'\xF0\x00\x00\x00\x00\x00\x00\x00' + elif codetype.startswith(b"\xF0"): + codelist += b"\xF0\x00\x00\x00\x00\x00\x00\x00" break self.codeList.seek(-8, 1) @@ -172,7 +174,6 @@ class GCT(object): codelist += self.codeList.read(GCT.determine_codelength(codetype, info)) self.codeList = BytesIO(codelist) - self.size = len(self.codeList.getbuffer()) class CodeHandler(object): @@ -183,7 +184,7 @@ class CodeHandler(object): def __init__(self, f): self._rawData = BytesIO(f.read()) - '''Get codelist pointer''' + """Get codelist pointer""" self._rawData.seek(0xFA) codelistUpper = self._rawData.read(2).hex() self._rawData.seek(0xFE) @@ -194,12 +195,12 @@ class CodeHandler(object): 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.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 @@ -215,65 +216,71 @@ class CodeHandler(object): f.seek(0) - def init_gct(self, gctFile: str, tmpdir: str=""): - if '.' in gctFile: - if os.path.splitext(gctFile)[1].lower() == '.txt': - with open(os.path.join(tmpdir, 'gct.bin'), 'wb+') as temp: - temp.write(bytes.fromhex('00D0C0DE'*2 + self.parse_input(gctFile) + 'F000000000000000')) - temp.seek(0) - self.geckoCodes = GCT(temp) - elif os.path.splitext(gctFile)[1].lower() == '.gct': - with open(gctFile, 'rb') as gct: - self.geckoCodes = GCT(gct) + def init_gct(self, gctFile: Path, tmpdir: Path=None): + if tmpdir is not None: + _tmpGct = tmpdir / "gct.bin" else: - with open(os.path.join(tmpdir, 'gct.bin'), 'wb+') as temp: - temp.write(b'\x00\xD0\xC0\xDE'*2) + _tmpGct = Path("gct.bin") - for file in os.listdir(gctFile): - if os.path.isfile(os.path.join(gctFile, file)): - if os.path.splitext(file)[1].lower() == '.txt': - temp.write(bytes.fromhex(self.parse_input(os.path.join(gctFile, file)))) - elif os.path.splitext(file)[1].lower() == '.gct': - with open(os.path.join(gctFile, file), 'rb') as gct: - temp.write(gct.read()[8:-8]) - else: - print(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') + 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) - def parse_input(self, geckoText) -> str: - with open(r'{}'.format(geckoText), 'rb') as gecko: + 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'] + encodeType = result["encoding"] - with open(r'{}'.format(geckoText), 'r', encoding=encodeType) as gecko: - geckoCodes = '' + with geckoText.open("r", encoding=encodeType) as gecko: + geckoCodes = "" state = None for line in gecko.readlines(): - if line in ('', '\n'): + if line in ("", "\n"): continue if state is None: - if line.startswith('$') or line.startswith('['): - state = 'Dolphin' + if line.startswith("$") or line.startswith("["): + state = "Dolphin" else: - state = 'OcarinaM' + state = "OcarinaM" try: - if state == 'OcarinaM': + if state == "OcarinaM": if self.includeAll: - geckoLine = re.findall(r'[A-F0-9]{8}[\t\f ][A-F0-9]{8}', line, re.IGNORECASE)[0] + 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] + 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: self._rawData.seek(0) - if self._rawData.read(4) == variable: - return self._rawData.tell() - 4 - while sample := self._rawData.read(4): if sample == variable: return self._rawData.tell() - 4 @@ -329,7 +333,7 @@ class CodeHandler(object): write_uint32(self._rawData, ppc) def set_variables(self, dolFile: DolFile): - varOffset = self.find_variable_data(b'\x00\xDE\xDE\xDE') + 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)) @@ -347,7 +351,6 @@ class KernelLoader(object): self._gpDiscDataList = None self._gpKeyAddrList = None self._cli = cli - self.patchJob = None self.initAddress = None self.protect = False self.verbosity = 0 @@ -368,22 +371,22 @@ class KernelLoader(object): return while sample := self._rawData.read(2): - if sample == b'GH': + if sample == b"GH": self._rawData.seek(-2, 1) write_uint16(self._rawData, self._gpModDataList[0]) - elif sample == b'GL': + elif sample == b"GL": self._rawData.seek(-2, 1) write_uint16(self._rawData, baseOffset + self._gpModDataList[1]) - elif sample == b'IH': + elif sample == b"IH": self._rawData.seek(-2, 1) write_uint16(self._rawData, entryPoint[0]) - elif sample == b'IL': + elif sample == b"IL": self._rawData.seek(-2, 1) write_uint16(self._rawData, entryPoint[1]) - elif sample == b'KH': + elif sample == b"KH": self._rawData.seek(-2, 1) write_uint16(self._rawData, self._gpKeyAddrList[0]) - elif sample == b'KL': + elif sample == b"KL": self._rawData.seek(-2, 1) write_uint16(self._rawData, baseOffset + self._gpKeyAddrList[1]) @@ -393,48 +396,42 @@ class KernelLoader(object): self._rawData.seek(0) while sample := self._rawData.read(4): - if sample == b'HEAP': #Found keyword "HEAP". Goes with the resize of the heap + if sample == b"HEAP": #Found keyword "HEAP". Goes with the resize of the heap self._rawData.seek(-4, 1) gpModInfoOffset = self._rawData.tell() - if _lowerAddr + gpModInfoOffset > 0x7FFF: #Absolute addressing - gpModUpperAddr = _upperAddr + 1 - else: - gpModUpperAddr = _upperAddr - + 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 + 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 + 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 + 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 + 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 + 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 + elif sample == b"CYPT": #Found keyword "CYPT". Encryption Key self._rawData.seek(-4, 1) gpKeyOffset = self._rawData.tell() - if _lowerAddr + gpKeyOffset > 0x7FFF: #Absolute addressing - gpKeyUpperAddr = _upperAddr + 1 - else: - gpKeyUpperAddr = _upperAddr + gpKeyUpperAddr = _upperAddr + 1 if (_lowerAddr + gpKeyOffset) > 0x7FFF else _upperAddr #Absolute addressing write_uint32(self._rawData, CodeHandler.encrypt_key(_key)) @@ -461,8 +458,7 @@ class KernelLoader(object): 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)) - return False, 'There are no unused sections left for GeckoLoader to use!' + 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 @@ -479,74 +475,67 @@ class KernelLoader(object): 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 False, 'There are no unused sections left for GeckoLoader to use!' + 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') + 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 chunk in protectdata: - codeHandler.geckoCodes.codeList.write(chunk) - codeHandler.geckoCodes.codeList.write(b'\xF0\x00\x00\x00\x00\x00\x00\x00') - codeHandler.geckoCodes.codeList.seek(0, 2) - codeHandler.geckoCodes.size = codeHandler.geckoCodes.codeList.tell() + + 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, dolFile: DolFile, codeHandler: CodeHandler, tmpdir, dump): + def build(self, gctFile: Path, dolFile: DolFile, codeHandler: CodeHandler, tmpdir: Path, dump: Path): _oldStart = dolFile.entryPoint - '''Initialize our codes''' + """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)) + 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 and self.patchJob == "ARENA": + if self.protect: self.protect_game(codeHandler) - - if self.patchJob == 'AUTO': - if codeHandler.initAddress + codeHandler.handlerLength + codeHandler.geckoCodes.size > 0x80002FFF: - self.patchJob = 'ARENA' - else: - self.patchJob = 'LEGACY' - '''Get entrypoint (or BSS midpoint) for insert''' + """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 RuntimeError: + 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) @@ -555,82 +544,59 @@ class KernelLoader(object): if codeHandler.optimizeList: codeHandler.geckoCodes.optimize_codelist(dolFile) - '''Is codelist optimized away?''' + """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 open(dump, 'wb') as final: + 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) + print("-"*64) if self.verbosity >= 1: - print(tools.color_text('\n :: All codes have been successfully pre patched', defaultColor=tools.TGREENLIT)) + print(tools.color_text("\n :: All codes have been successfully pre patched", defaultColor=tools.TGREENLIT)) return - - if self.patchJob == 'LEGACY': - legacy = True - codeHandler.allocation = 0x80003000 - (codeHandler.initAddress + codeHandler.handlerLength) - hooked = determine_codehook(dolFile, codeHandler, True) - if hooked: - _status, _msg = self.patch_legacy(codeHandler, dolFile) + hooked = determine_codehook(dolFile, codeHandler, False) + if hooked: + _status, _msg = self.patch_arena(codeHandler, dolFile) else: - legacy = False - hooked = determine_codehook(dolFile, codeHandler, False) - if hooked: - _status, _msg = self.patch_arena(codeHandler, dolFile) - - if not hooked: - self.error(tools.color_text('Failed to find a hook address. Try using option --codehook to use your own address\n', defaultColor=tools.TREDLIT)) - elif _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)) + self.error(tools.color_text("Failed to find a hook address. Try using option --codehook to use your own address\n", defaultColor=tools.TREDLIT)) - with open(dump, 'wb') as final: + 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 codeHandler.allocation > 0x70000: - print(tools.color_text(f'\n :: WARNING: Allocations beyond 0x70000 will crash certain games. You allocated 0x{codeHandler.allocation:X}', defaultColor=tools.TYELLOW)) - - elif codeHandler.allocation > 0x40000: - print(tools.color_text(f'\n :: HINT: Recommended allocation limit is 0x40000. You allocated 0x{codeHandler.allocation:X}', defaultColor=tools.TYELLOWLIT)) if self.verbosity >= 3: dolFile.print_info() - print('-'*64) + print("-"*64) if self.verbosity >= 2: - print('') - if legacy is False: - 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'] - else: - info = [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'] + 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}"'] + 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)) @@ -647,33 +613,33 @@ def determine_codehook(dolFile: DolFile, codeHandler: CodeHandler, hook=False) - def assert_code_hook(dolFile: DolFile, codeHandler: CodeHandler) -> bool: - for _, address, size, _, _ in dolFile.textSections: - dolFile.seek(address) - sample = dolFile.read(size) + for section in dolFile.textSections: + dolFile.seek(section["address"]) + sample = dolFile.read(section["size"]) - if codeHandler.hookType == 'VI': + if codeHandler.hookType == "VI": result = sample.find(codeHandler.gcnVIHook) - elif codeHandler.hookType == 'GX': + elif codeHandler.hookType == "GX": result = sample.find(codeHandler.gcnGXDrawHook) - elif codeHandler.hookType == 'PAD': + 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)) + raise NotImplementedError(tools.color_text(f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) if result >= 0: - dolFile.seek(address + result) + dolFile.seek(section["address"] + result) else: - if codeHandler.hookType == 'VI': + if codeHandler.hookType == "VI": result = sample.find(codeHandler.wiiVIHook) - elif codeHandler.hookType == 'GX': + elif codeHandler.hookType == "GX": result = sample.find(codeHandler.wiiGXDrawHook) - elif codeHandler.hookType == 'PAD': + 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)) + raise NotImplementedError(tools.color_text(f"Unsupported hook type specified ({codeHandler.hookType})", defaultColor=tools.TREDLIT)) if result >= 0: - dolFile.seek(address + result) + dolFile.seek(section["address"] + result) else: continue @@ -691,7 +657,7 @@ def insert_code_hook(dolFile: DolFile, codeHandler: CodeHandler, address: int): 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)) + 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) \ No newline at end of file diff --git a/main_ui.py b/main_ui.py index 0b946fd..34005f2 100644 --- a/main_ui.py +++ b/main_ui.py @@ -13,11 +13,6 @@ from dolreader import DolFile from fileutils import resource_path from kernel import CodeHandler, KernelLoader -class CmdWrapper(object): - @staticmethod - def call(prog: str, *args): - return subprocess.run(" ".join([prog, *args]), shell=True, capture_output=True, text=True) - class MainWindow(QtWidgets.QMainWindow): def __init__(self, version: str): super().__init__() @@ -71,7 +66,7 @@ class MainWindow(QtWidgets.QMainWindow): font.setWeight(42) self.setFont(font) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(resource_path(os.path.join("bin", "icon.ico"))), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(str(resource_path(os.path.join("bin", "icon.ico")))), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) #Top level widget @@ -346,34 +341,6 @@ class MainWindow(QtWidgets.QMainWindow): self.allocLineEdit.setObjectName("allocLineEdit") self.optionsLayout.addWidget(self.allocLineEdit, 2, 0, 1, 1) - #Patch label - self.patchLabel = QtWidgets.QLabel(self.centerWidget) - self.patchLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.patchLabel.sizePolicy().hasHeightForWidth()) - self.patchLabel.setSizePolicy(sizePolicy) - self.patchLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.patchLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.patchLabel.setTextFormat(QtCore.Qt.PlainText) - self.patchLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.patchLabel.setObjectName("patchLabel") - self.optionsLayout.addWidget(self.patchLabel, 1, 1, 1, 1) - - #Patch selection - self.patchTypeSelect = 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.patchTypeSelect.setSizePolicy(sizePolicy) - self.patchTypeSelect.setMinimumSize(QtCore.QSize(79, 23)) - self.patchTypeSelect.setMaximumSize(QtCore.QSize(79, 23)) - self.patchTypeSelect.setObjectName("patchTypeSelect") - self.patchTypeSelect.addItems(["AUTO", "LEGACY", "ARENA"]) - self.optionsLayout.addWidget(self.patchTypeSelect, 2, 1, 1, 1) - #handlerType label self.handlerTypeLabel = QtWidgets.QLabel(self.centerWidget) self.handlerTypeLabel.setEnabled(False) @@ -387,7 +354,7 @@ class MainWindow(QtWidgets.QMainWindow): 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, 2, 1, 1) + self.optionsLayout.addWidget(self.handlerTypeLabel, 1, 1, 1, 1) #handlerType selection self.handlerTypeSelect = QtWidgets.QComboBox(self.centerWidget) @@ -400,7 +367,7 @@ class MainWindow(QtWidgets.QMainWindow): self.handlerTypeSelect.setMaximumSize(QtCore.QSize(79, 23)) self.handlerTypeSelect.setObjectName("handlerTypeSelect") self.handlerTypeSelect.addItems(["FULL", "MINI"]) - self.optionsLayout.addWidget(self.handlerTypeSelect, 2, 2, 1, 1) + self.optionsLayout.addWidget(self.handlerTypeSelect, 2, 1, 1, 1) #hookType label self.hookTypeLabel = QtWidgets.QLabel(self.centerWidget) @@ -415,7 +382,7 @@ class MainWindow(QtWidgets.QMainWindow): 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, 3, 1, 1) + self.optionsLayout.addWidget(self.hookTypeLabel, 1, 2, 1, 1) #hookType selection self.hookTypeSelect = QtWidgets.QComboBox(self.centerWidget) @@ -428,7 +395,7 @@ class MainWindow(QtWidgets.QMainWindow): self.hookTypeSelect.setMaximumSize(QtCore.QSize(79, 23)) self.hookTypeSelect.setObjectName("hookTypeSelect") self.hookTypeSelect.addItems(["VI", "GX", "PAD"]) - self.optionsLayout.addWidget(self.hookTypeSelect, 2, 3, 1, 1) + self.optionsLayout.addWidget(self.hookTypeSelect, 2, 2, 1, 1) #txtCodesInclude label self.txtCodesIncludeLabel = QtWidgets.QLabel(self.centerWidget) @@ -443,7 +410,7 @@ class MainWindow(QtWidgets.QMainWindow): self.txtCodesIncludeLabel.setTextFormat(QtCore.Qt.PlainText) self.txtCodesIncludeLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) self.txtCodesIncludeLabel.setObjectName("txtCodesIncludeLabel") - self.optionsLayout.addWidget(self.txtCodesIncludeLabel, 3, 0, 1, 1) + self.optionsLayout.addWidget(self.txtCodesIncludeLabel, 1, 3, 1, 1) #txtCodesInclude selection self.txtCodesIncludeSelect = QtWidgets.QComboBox(self.centerWidget) @@ -456,35 +423,16 @@ class MainWindow(QtWidgets.QMainWindow): self.txtCodesIncludeSelect.setMaximumSize(QtCore.QSize(79, 23)) self.txtCodesIncludeSelect.setObjectName("txtCodesIncludeSelect") self.txtCodesIncludeSelect.addItems(["ACTIVE", "ALL"]) - self.optionsLayout.addWidget(self.txtCodesIncludeSelect, 4, 0, 1, 1) + self.optionsLayout.addWidget(self.txtCodesIncludeSelect, 2, 3, 1, 1) - #optimize label - self.optimizeLabel = QtWidgets.QLabel(self.centerWidget) - self.optimizeLabel.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.optimizeLabel.sizePolicy().hasHeightForWidth()) - self.optimizeLabel.setSizePolicy(sizePolicy) - self.optimizeLabel.setMinimumSize(QtCore.QSize(79, 23)) - self.optimizeLabel.setMaximumSize(QtCore.QSize(16777215, 23)) - self.optimizeLabel.setTextFormat(QtCore.Qt.PlainText) - self.optimizeLabel.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) - self.optimizeLabel.setObjectName("optimizeLabel") - self.optionsLayout.addWidget(self.optimizeLabel, 3, 1, 1, 1) - - #optimize selection - self.optimizeSelect = 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.optimizeSelect.setSizePolicy(sizePolicy) - self.optimizeSelect.setMinimumSize(QtCore.QSize(79, 23)) - self.optimizeSelect.setMaximumSize(QtCore.QSize(79, 23)) - self.optimizeSelect.setObjectName("optimizeSelect") - self.optimizeSelect.addItems(["TRUE", "FALSE"]) - self.optionsLayout.addWidget(self.optimizeSelect, 4, 1, 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) @@ -498,7 +446,7 @@ class MainWindow(QtWidgets.QMainWindow): self.exOptionsButton.setFlat(False) self.exOptionsButton.setDisabled(True) self.exOptionsButton.setObjectName("exOptionsButton") - self.optionsLayout.addWidget(self.exOptionsButton, 4, 2, 1, 2) + self.optionsLayout.addWidget(self.exOptionsButton, 4, 0, 1, 4) #horizontal separater 1 self.horiSepA = QtWidgets.QFrame(self.centerWidget) @@ -718,16 +666,12 @@ class MainWindow(QtWidgets.QMainWindow): self.optionsLabel.setEnabled(True) self.allocLabel.setEnabled(True) self.allocLineEdit.setEnabled(True) - self.patchLabel.setEnabled(True) - self.patchTypeSelect.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.optimizeLabel.setEnabled(True) - self.optimizeSelect.setEnabled(True) self.exOptionsButton.setEnabled(True) self.actionSave.setEnabled(True) self.actionSave_As.setEnabled(True) @@ -805,11 +749,6 @@ class MainWindow(QtWidgets.QMainWindow): self.allocLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Allocation", None)) self.allocLineEdit.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "AUTO", None)) - self.patchLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Patch Type", None)) - self.patchTypeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "AUTO", None)) - self.patchTypeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "LEGACY", None)) - self.patchTypeSelect.setItemText(2, QtWidgets.QApplication.translate("Dialog", "ARENA", 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)) @@ -823,10 +762,6 @@ class MainWindow(QtWidgets.QMainWindow): self.txtCodesIncludeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "ACTIVE", None)) self.txtCodesIncludeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "ALL", None)) - self.optimizeLabel.setText(QtWidgets.QApplication.translate("MainWindow", "Optimize", None)) - self.optimizeSelect.setItemText(0, QtWidgets.QApplication.translate("Dialog", "TRUE", None)) - self.optimizeSelect.setItemText(1, QtWidgets.QApplication.translate("Dialog", "FALSE", None)) - self.exOptionsButton.setText(QtWidgets.QApplication.translate("MainWindow", "Advanced Settings", None)) self.compileButton.setText(QtWidgets.QApplication.translate("MainWindow", "RUN", None)) diff --git a/setup.py b/setup.py index e02211f..33c9fa1 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ executables = [ ] setup(name = "GeckoLoader", - version = "7.0.0", + 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",