code:
# function definitions
from lxml import etree
import file_ops, xml

# custom import (C-like include)
# geckoloader_path and wit_path will be defined there
exec(open("bin_tool.py").read())

# function to return an attribute string with the
# {$__gameid}, {$__region}, {$__maker} string replacements done
def rtn_def_param_str(string, params_list):

  # return this!
  return file_ops.get_path_str(string.replace("{$__gameid}", params_list[0]).replace("{$__region}", params_list[1]).replace("{$__maker}", params_list[2]))

# function to check if the game image is valid
def check_game_image(game_image_path):
  result = exec_subprocess([wit_path, "verify", "--test", game_image_path])
  if (result.returncode != 0):
    return False
  return True

# function to check if the game image is valid for the riivolution XML
def check_game_xml_id(game_image_path, riiv_xml_id):

  # get the actual game id
  game_id = exec_subprocess([wit_path, "ID6", game_image_path]).stdout.replace("\n", "")

  # riiv_xml_id will be formatted as
  # ["XXX", ["X", "X", ...], "XX"]
  #   RMG     E    P   ...    01    (example)

  # see which parts of the game id are valid
  is_id_sec_valid = [False, False, False]

  # first 3 letters
  if (riiv_xml_id[0] == game_id[:-3] or riiv_xml_id[0] == ""):
    is_id_sec_valid[0] = True
  # 4th letter
  if (len(riiv_xml_id[1]) == 0):
    is_id_sec_valid[1] = True
  else:
    for reg in riiv_xml_id[1]:
      if (reg == game_id[3:-2]):
        is_id_sec_valid[1] = True
  # last 2 letters
  if (riiv_xml_id[2] == game_id[4:] or riiv_xml_id[2] == ""):
    is_id_sec_valid[2] = True

  # verify all parts
  if (False in is_id_sec_valid):
    return False
  return game_id

# check_riiv_xml() function
# function to check if a riivolution XML is valid using a XSD file
# if the function returns False and the file actually seems right
# then you have to use a service like the one in this page to
# manually check what is the issue
def check_riiv_xml(xml_path):

  # check agaisnt MyRiivolution.xsd
  return xml.check_with_sch(xml_path, "MyRiivolution.xsd")

# get_patch_elem_root_path() function
# function to get the root path of the files inside a patch element
def get_patch_elem_root_path(root_elem, patch_elem):

  # get the root path of the whole XML
  root_path = ""
  if ("root" in root_elem.attrib):
    root_path = root_path + root_elem.attrib["root"]
  # get the relative path of the patch element
  if ("root" in patch_elem.attrib):
    root_path = root_path + patch_elem.attrib["root"]
  return file_ops.get_path_str(root_path)

# check_riiv_patches() function
# function to check the information about the patches to be applied to the game
# this function will basically check if filepaths are valid both for the XML and the game
def check_riiv_patches(game_path, xml_path, mod_files_folder):

  ##################################
  # check the game file and xml file
  if (check_game_image(game_path) == False):
    print("Invalid ISO/WBFS")
    return False
  if (check_riiv_xml(xml_path) == False):
    print("Invalid Riivolution XML")
    return False

  # check if the game id matches the game id from the riivolution file
  riiv_game_id = ["", [], ""]
  xml = etree.parse(xml_path, etree.XMLParser(ns_clean = True, remove_comments = True)) # parse the xml file
  root = xml.getroot()

  #########################################
  # check the id tag in the riivolution XML
  id_elem = root.find("id")
  if (id_elem != None):
    # grab the game id tag
    if ("game" in id_elem.attrib):
      # game part
      riiv_game_id[0] = id_elem.attrib["game"]
      # region part
      if (len(riiv_game_id[0]) == 3): # check if the game id is 3 characters long
        if (len(id_elem) != 0):
          for reg in id_elem:
            riiv_game_id[1].append(reg.attrib["type"])
      else: # it is 4 characters long
        riiv_game_id[1].append(riiv_game_id[0][3:])
        riiv_game_id[0] = riiv_game_id[0][:-1]
      # developer part
      if ("developer" in id_elem.attrib):
        riiv_game_id[2] = id_elem.attrib["developer"]

  # check all the possible combinations of a full game id
  game_id = ["", "", ""]
  game_id[0] = check_game_xml_id(game_path, riiv_game_id)
  if (game_id == False):
    print("Riivolution XML is not compatible with the Wii game provided")
    return False
  game_id[1] = game_id[0][3:-2]
  game_id[2] = game_id[0][4:]
  game_id[0] = game_id[0][:-3]

  ######################################################################
  # parse the XML file and check the patches and their file replacements
  opt_patches_id = []
  for child in root:
    if (child.tag == "options"):
      for sec in child:
        for opt in sec:
          for choice in opt:
            for patch in choice:
              opt_patches_id.append(patch.attrib["id"]) # lol
  elems_patch_id = []
  for patch in root.findall("patch"):
    elems_patch_id.append(patch.attrib["id"])
  # check if all the options patches id exist in the patch tags down in the XML
  patch_elems = []
  for patch_id in opt_patches_id:
    if (patch_id not in elems_patch_id):
      print("Patch to apply from options does not exist in the XML")
      return False
    # at the same time store the patch elements that will actually get read by riivolution
    for patch in root.findall("patch"):
      if (patch.attrib["id"] == patch_id):
        patch_elems.append(patch)

  ######################################################
  # check the patch tags file/folder/memory replacements
  # but before that, extract the game files list (yikes)
  game_files = exec_subprocess([wit_path, "files", game_path]).stdout.split("\n")
  # remove non-used elements
  cont_loop = True
  while (cont_loop == True):
    for i in range(0, len(game_files)):
      if (("DATA/files" not in game_files[i]) and ("./files" not in game_files[i])):
        game_files.pop(i)
        cont_loop = True
        break
      cont_loop = False
  # do another for loop to get rid of "DATA/files/" and "./files/" starting strings
  for i in range(0, len(game_files)):
    if ("DATA/files" in game_files[i]):
      game_files[i] = file_ops.get_path_str(game_files[i].replace("DATA/files", ""))
    else:
      game_files[i] = file_ops.get_path_str(game_files[i].replace("./files", ""))

  # ~ for i in range(0, len(game_files)):
    # ~ print(game_files[i])

  ##########################################
  # start the loop! and check the file paths
  for patch in patch_elems:
    # get the root path of the current pacthes
    patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
                                        + get_patch_elem_root_path(root, patch), game_id)
    # check the folder, file tag patches
    # Note: the offset, length and other attributes for file and folder
    #       elements are weird and I don't know how to support them
    for patch_type in patch:
      # file patch
      if (patch_type.tag == "file"):
        fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
        if (file_ops.f_exists(fpath) == False):
          print("Line %d: File patch has invalid external path" % (patch_type.sourceline))
          print(fpath)
          return False
        else: # check if the file exists on disc
          if (patch_type.attrib["disc"] not in game_files):
            if ("create" not in patch_type.attrib):
              print("Line %d: File patch creates non-existant disc file without create attribute" % (patch_type.sourceline))
              return False
            else: # create attribute exists
              if (patch_type.attrib["create"] == "false"):
                print("Line %d: File patch creates non-existant disc file with create = False" % (patch_type.sourceline))
                return False

      # folder patch
      if (patch_type.tag == "folder"):
        fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
        if (file_ops.f_exists(fpath) == False):
          print("Line %d: Folder patch has invalid external path" % (patch_type.sourceline))
          print(fpath)
          return False
        if ("disc" in patch_type.attrib):
          if (rtn_def_param_str(patch_type.attrib["disc"], game_id) not in game_files):
            # check if the create attribute is present
            if ("create" in patch_type.attrib):
              if (patch_type.attrib["create"] == "False"):
                print("Line %d: Folder patch creates non-existant disc folder with create = False" % (patch_type.sourceline))
                return False
            else: # cannot create a disc non-existant folder without a create attribute
              print("Line %d: Folder patch creates non-existant disc folder without create attribute" % (patch_type.sourceline))
              return False
      # memory patch (incomplete, full check happens when building the mod)
      if (patch_type.tag == "memory"):
        if ("valuefile" in patch_type.attrib):
          fpath = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["valuefile"], game_id)
          if (file_ops.f_exists(fpath) == False):
            print("Line %d: Memory patch has invalid paths")
            print(fpath)
            return False
        # restrict patches to be applied to the 0x80000000 memory area
        if ("0x8" not in patch_type.attrib["offset"]):
          print("Line %d: Memory patch tries patching to a range outside the 0x80000000 memory area" % (patch_type.sourceline))
          return False
        # check if a ram dump is needed for the patching process
        if ("original" in patch_type.attrib):
          if (file_ops.f_exists("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin") == False
              or file_ops.get_file_size("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin") != 0x1800000):
            print("Line %d: Memory element has original condition." % (patch_type.sourceline))
            print("No/Invalid ram dump of \"%s\" was found in ram_dumps/" % (game_id[0] + game_id[1] + game_id[2]))
            return False

  ###############################
  # hardcore memory patches check
  # "original" patches (frequently used) can only be checked with a ram dumb of the game
  # the folder "ram_dumps" will hold wii game ram dumps (24 mb approx) named as the game ID
  # if a patch original condition fails it is simply not applied so it isn't an error to be checked

  # everything is good for now
  return True

# function to be able to remove the tmp folder
# after the check_riiv_patches() function check
def check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder):

  # safe the return value of check_riiv_patches()
  result = check_riiv_patches(game_path, xml_path, mod_files_folder)
  file_ops.rm_folder("tmp")
  return result

# get_riiv_patches_inf() function
# function to return information about the patches used in the riivolution file
def get_riiv_patches_inf(game_path, xml_path, mod_files_folder):

  # first check if the XML is well formatted
  # if the game relates to the XML
  # and if the mod files folder/xml file/game file relate to each other
  if (check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder) == False):
    return None

  # to be able to store correctly the XML information
  # literally the first time doing classes stuff (*jesus what is this*)
  class section:
      def __init__(self, name):
        self.name = name
        self.options = []
  class option:
      def __init__(self, name):
        self.name = name
        self.choices = []
  class choice:
      def __init__(self, name):
        self.name = name
        self.patches = []
  class patch:
      def __init__(self, id):
        self.id = id

  # parse the XML and find the options element
  # return a list of section classes with their respective option -> choice
  sections = []
  for sec in etree.parse(xml_path, etree.XMLParser(remove_comments = True)).find("options"):
    sections.append(section(sec.attrib["name"]))
    for opt in sec:
      sections[-1].options.append(option(opt.attrib["name"]))
      for cho in opt:
        sections[-1].options[-1].choices.append(choice(cho.attrib["name"]))
        for pat in cho:
          sections[-1].options[-1].choices[-1].patches.append(patch(pat.attrib["id"]))

  # ~ print()
  # ~ for sec in sections:
    # ~ print("Section name: %s" % (sec.name))
    # ~ print("#-Options names:")
    # ~ for opt in sec.options:
      # ~ print("----%s" % (opt.name))
      # ~ print("##----Choices names:")
      # ~ for cho in opt.choices:
        # ~ print("--------%s" % (cho.name))
        # ~ print("###-------Patches ids:")
        # ~ for pat in cho.patches:
          # ~ print("------------%s" % (pat.id))
  # ~ print()

  # return dis shit
  return sections

# apply_riiv_patches() function
def apply_riiv_patches(game_path, xml_path, mod_files_folder, choice_str_list):

  # first check everything
  if (check_riiv_patches_wrapper(game_path, xml_path, mod_files_folder) == False):
    return False

  # check if choice_str_list contains valid XML choices
  # parse the XML and find the options element
  xml = etree.parse(xml_path, etree.XMLParser(remove_comments = True))
  root = xml.getroot()
  choices_names = []
  for sec in root.find("options"):
    for opt in sec:
      for cho in opt:
        choices_names.append(cho.attrib["name"])
  for choice_str in choice_str_list:
    if (choice_str not in choices_names):
      print("Invalid choice selected: %s" % (choice_str))
      return False

  # start the patching process

  # get the patches ids to apply
  patches_ids = []
  for sec in root.find("options"):
    for opt in sec:
      for cho in opt:
        if (cho.attrib["name"] in choice_str_list):
          for patch in cho:
            patches_ids.append(patch.attrib["id"])
  # get the patch elements on the xml
  patch_elems = []
  for patch in root.findall("patch"):
    if (patch.attrib["id"] in patches_ids):
      patch_elems.append(patch)

  # get game id code pieces
  game_id = ["", "", ""]
  game_id[0] = exec_subprocess([wit_path, "ID6", game_path]).stdout.replace("\n", "")
  game_id[1] = game_id[0][3:-2]
  game_id[2] = game_id[0][4:]
  game_id[0] = game_id[0][:-3]

  # extract the files from the game (wish I would know a way to just
  # have to deal only with the mod files and somehow "merge them" on
  # the original WBFS so no complete extraction has to be done)
  result = exec_subprocess([wit_path, "extract", game_path, "tmp/"]).returncode
  if (result != 0):
    print("wit could not extract correctly the game files")
    return False

  # define the actual root of files to change (its either "tmp/" or "tmp/DATA/")
  ext_game_path = "tmp/files/"
  if (file_ops.f_exists("tmp/DATA/")):
    ext_game_path = "tmp/DATA/files/"

  # First, deal with the file/folder replacements
  memory_original_exists = False # see if the original condition exists in a memory patch
  for patch in patch_elems:
    # get the root path of the current pacthes
    patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
                                        + get_patch_elem_root_path(root, patch), game_id)
    # go through each patch type
    for patch_type in patch:
      # file patch
      if (patch_type.tag == "file"):
        fpath1 = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
        fpath2 = rtn_def_param_str(ext_game_path + patch_type.attrib["disc"], game_id)
        file_ops.cp_file(fpath1, fpath2)
      # folder patch
      if (patch_type.tag == "folder"):
        # check if the folder in the extracted files exists
        if ("disc" in patch_type.attrib):
          fpath1 = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["external"], game_id)
          fpath2 = rtn_def_param_str(ext_game_path + patch_type.attrib["disc"], game_id)
          file_ops.cp_folder(fpath1, fpath2, False)
      # memory patch
      if (patch_type.tag == "memory"):
        if ("original" in patch_type.attrib):
          memory_original_exists = True

  # Now, handle the memory patches
  # wit dolpatch and geckoloader will be used
  # - wit will patch offsets above 0x8000FFFF
  # - geckoloader will patch offsets from 0x8000FFFF and below
  # for the wit patches a temporal riivolution xml will be made
  # for the geckoloader patches a temporal gecko code list will be done
  # all patches will be checked agaisnt a ram dump of the game
  ram_dump = None
  if (memory_original_exists): # open the ram dump
    ram_dump = open("ram_dumps/" + game_id[0] + game_id[1] + game_id[2] + ".bin", "rb")
  tmp_gl = open("tmp_gl.txt", "w")
  tmp_wit = open("tmp_wit.xml", "w")

  # start the loop!
  for patch in patch_elems:
    patch_root_path = rtn_def_param_str(file_ops.get_base_path(mod_files_folder, True) + "/"
                                        + get_patch_elem_root_path(root, patch), game_id)
    for patch_type in patch:
      # only go through the memory patches this time
      if (patch_type.tag != "memory"):
        continue
      offset_hex_str = patch_type.attrib["offset"][2:]
      offset_hex_int = int(offset_hex_str, 16)

      # check the original value (if it has one)
      if ("original" in patch_type.attrib):
        og_hex = patch_type.attrib["original"]
        ram_dump.seek(offset_hex_int - 0x80000000)
        dmp_hex = ram_dump.read(int(len(og_hex) / 2)).hex()
        # ignore the patch if original does not match
        if (og_hex.upper() != dmp_hex.upper()):
          continue
        # wit hotfix (report this inconsistency)
        patch_type.attrib["original"] = patch_type.attrib["original"].replace("0x", "")

      # check if the memory patch contains a valuefile attribute
      if ("valuefile" in patch_type.attrib):
        val_file_path = patch_root_path + "/" + rtn_def_param_str(patch_type.attrib["valuefile"], game_id)
        tmp = open(val_file_path, "rb")
        del patch_type.attrib["valuefile"]
        patch_type.set("value", tmp.read(file_ops.get_file_size(val_file_path)).hex().upper())
        tmp.close()
      else: # it has a value attribute
        # wit hotfix (report this inconsistency)
        patch_type.attrib["value"] = patch_type.attrib["value"].replace("0x", "")

      # for wit
      if (offset_hex_int > 0x8000FFFF):
        tmp = etree.tostring(patch_type).decode("utf-8")
        while (tmp[0] in " \t\r\n"):
          tmp = tmp[1:]
        while (tmp[-1] in " \t\r\n"):
          tmp = tmp[:-1]
        tmp_wit.write(tmp + "\n")
      # for geckoloader
      else:
        tmp = patch_type.attrib["value"]
        last_offset = offset_hex_int # variable to be used later
        if (len(tmp) > 8):
          for i in range(0, int(len(tmp) / 8)):
            last_offset = offset_hex_int + (4 * i)
            if (len(tmp) > 8):
              tmp_gl.write("%08X %s\n" % (last_offset - 0x80000000 + 0x04000000, tmp[:-len(tmp) + 8]))
            else:
              tmp_gl.write("%08X %s\n" % (last_offset - 0x80000000 + 0x04000000, tmp))
            tmp = tmp[8:]
          last_offset += 4
        # write the rest of tmp depending on its size
        if (len(tmp) == 6):
          tmp_gl.write("%08X 0000%s\n" % (last_offset - 0x80000000 + 0x02000000, tmp[:-2]))
          tmp_gl.write("%08X 000000%s\n" % (last_offset + 2 - 0x80000000, tmp[4:]))
        elif (len(tmp) == 4):
          tmp_gl.write("%08X 0000%s\n" % (last_offset - 0x80000000 + 0x02000000, tmp))
        elif (len(tmp) == 2):
          tmp_gl.write("%08X 000000%s\n" % (last_offset - 0x80000000, tmp))

  # close files
  tmp_wit.close()
  tmp_gl.close()
  if (memory_original_exists):
    ram_dump.close()

  # define the main.dol location
  dol_path = ext_game_path.replace("files/", "sys/main.dol")
  dol_tmp_path = dol_path.replace("main.dol", "main_tmp.dol")

  # patch the forsaken main.dol
  # geckoloader part
  if (file_ops.get_file_size("tmp_gl.txt") != 0):
    # check the return code from geckoloader
    result = exec_subprocess(geckoloader_path + [dol_path, "tmp_gl.txt", "--dest",
                             dol_tmp_path, "--txtcodes", "ALL", "--optimize"]).returncode
    if (result != 0):
      print("geckoloader could not handle tmp_gl.txt properly")
      return False
    file_ops.cp_file(dol_tmp_path, dol_path)
    file_ops.rm_file(dol_tmp_path)
  # wit part
  if (file_ops.get_file_size("tmp_wit.xml") != 0):
    # check the return code from wit
    result = exec_subprocess([wit_path, "dolpatch", dol_path,
                             "XML=tmp_wit.xml", "--dest", dol_tmp_path]).returncode
    if (result != 0):
      print("wit could not handle tmp_wit.xml properly")
      return False
    file_ops.cp_file(dol_tmp_path, dol_path)
    file_ops.rm_file(dol_tmp_path)

  # build game image file
  exec_subprocess([wit_path, "copy", "tmp/", "result.wbfs"])

  # eliminate temp files
  file_ops.rm_folder("tmp/")
  file_ops.rm_file("tmp_gl.txt")
  file_ops.rm_file("tmp_wit.xml")

  # done! (hopefully)
  return True

# apply_riiv_patches_wrapper() function
def apply_riiv_patches_wrapper(game_path, xml_path, mod_files_folder, choice_str_list):

  # safe the return value of apply_riiv_patches()
  result = apply_riiv_patches(game_path, xml_path, mod_files_folder, choice_str_list)
  file_ops.rm_folder("tmp/")
  file_ops.rm_file("tmp_gl.txt")
  file_ops.rm_file("tmp_wit.xml")
  return result


code:
# 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 io import StringIO
from pathlib import Path

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.1"

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(
            "--handlerpath",
            help="Define the path to the codehandler file, overrides the default",
            metavar="PATH",
        )
        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 / 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:
            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

        if args.handlerpath:
            codeHandlerFile = Path(args.handlerpath).resolve()
        else:
            codeHandlerFile = Path("bin/codehandler.bin")

        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"]

            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:
        LOAD_DEST = 0
        LOAD_GCT = 1
        LOAD_FOLDER = 2
        LOAD_DOL = 3
        LOAD_SESSION = 4
        SAVE_SESSION = 5
        SAVE_SESSION_AS = 6

    def __init__(self, cli: GeckoLoaderCli):
        self.cli = cli
        self.app = None
        self.ui = None
        self.uiprefs = None
        self.uiexSettings = None
        self.dolPath = None
        self.codePath = [None, None]
        self.destPath = None
        self.sessionPath = None
        self.prefs = {"qtstyle": "Default", "darktheme": False}
        self.style_log = []
        self.compileCount = 0

        self.log = logging.getLogger(f"GeckoLoader {self.version}")

        if not get_program_folder("GeckoLoader").exists():
            get_program_folder("GeckoLoader").mkdir()

        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)

    def show_dialog(self, dialog_type=None):
        if dialog_type == "aboutqt":
            QtWidgets.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 ",
                    "This application supports various features, such as ",
                    "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.version}\n\n"
                    "Copyright (c) 2020\n\n",
                    "JoshuaMK <joshuamkw2002@gmail.com> \n\n",
                    "All rights reserved.",
                ]
            )

            QtWidgets.QMessageBox.about(
                self.app.activeWindow(), "About GeckoLoader", desc
            )
        elif dialog_type == "Preferences":
            self.uiprefs.show()
        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",
                    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),
                    "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 = Path(fname).resolve()

        if self.dolPath.is_file():
            self.ui.dolTextBox.setText(str(self.dolPath))
            return True, None
        else:
            return False, "The file does not exist!"

    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",
                        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),
                        "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,
                    )
                )
            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,
                    )
                )

        if fname == "" or fname is None:  # Make sure we have something to open
            return False, None
        else:
            self.codePath = [Path(fname).resolve(), isFolder]

            if not isFolder:
                self.ui.gctFileTextBox.setText(str(self.codePath[0]))
                self.ui.gctFolderTextBox.setText("")
            else:
                self.ui.gctFileTextBox.setText("")
                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",
                    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),
                    "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 = 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",
                    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),
                    "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 = Path(fname).resolve()

            with self.sessionPath.open("rb") as session:
                p = cPickle.load(session)

                self.ui.dolTextBox.setText(p["dolPath"])

                if p["gctFilePath"] != "":
                    self.ui.gctFileTextBox.setText(p["gctFilePath"])
                    self.ui.gctFolderTextBox.setText("")
                elif p["gctFolderPath"] != "":
                    self.ui.gctFileTextBox.setText("")
                    self.ui.gctFolderTextBox.setText(p["gctFolderPath"])
                else:
                    self.ui.gctFileTextBox.setText("")
                    self.ui.gctFolderTextBox.setText("")

                self.ui.destTextBox.setText(p["destPath"])
                self.ui.allocLineEdit.setText(p["alloc"])
                self.ui.handlerTypeSelect.setCurrentIndex(p["handlerIndex"])
                self.ui.hookTypeSelect.setCurrentIndex(p["hookIndex"])
                self.ui.txtCodesIncludeSelect.setCurrentIndex(p["txtIndex"])
                self.uiexSettings.optimizeCodes.setChecked(p["optimize"])
                self.uiexSettings.protectCodes.setChecked(p["protect"])
                self.uiexSettings.encryptCodes.setChecked(p["encrypt"])
                self.uiexSettings.codehookLineEdit.setText(p["hookAddress"])
                self.uiexSettings.kernelHookLineEdit.setText(p["initAddress"])
                self.uiexSettings.verbositySelect.setCurrentIndex(p["verbosity"])

            return True, None

    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()),
                        "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),
                        "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 = Path(fname).resolve()

        try:
            with self.sessionPath.open("wb") as session:
                p = {}

                p["dolPath"] = self.ui.dolTextBox.text().strip()
                p["gctFilePath"] = self.ui.gctFileTextBox.text().strip()
                p["gctFolderPath"] = self.ui.gctFolderTextBox.text().strip()
                p["destPath"] = self.ui.destTextBox.text().strip()
                p["alloc"] = self.ui.allocLineEdit.text().strip()
                p["handlerIndex"] = self.ui.handlerTypeSelect.currentIndex()
                p["hookIndex"] = self.ui.hookTypeSelect.currentIndex()
                p["txtIndex"] = self.ui.txtCodesIncludeSelect.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()
                p["initAddress"] = self.uiexSettings.kernelHookLineEdit.text().strip()
                p["verbosity"] = self.uiexSettings.verbositySelect.currentIndex()

                try:
                    cPickle.dump(p, session)
                except cPickle.PicklingError as e:
                    return False, str(e)

            return True, None
        except (IOError, PermissionError) as e:
            return False, str(e)

    def file_dialog_exec(self, event: Dialogs):
        try:
            if event == GUI.Dialogs.LOAD_DOL:
                status, msg = self._open_dol()
            elif event == GUI.Dialogs.LOAD_GCT:
                status, msg = self._load_codes(False)
            elif event == GUI.Dialogs.LOAD_FOLDER:
                status, msg = self._load_codes(True)
            elif event == GUI.Dialogs.LOAD_DEST:
                status, msg = self._open_dest()
            elif event == GUI.Dialogs.LOAD_SESSION:
                status, msg = self._load_session()
            elif event == GUI.Dialogs.SAVE_SESSION:
                status, msg = self._save_session()
            elif event == GUI.Dialogs.SAVE_SESSION_AS:
                status, msg = self._save_session(True)
            else:
                return
        except IndexError:
            self.ui.set_edit_fields()
            return

        if status is False and msg is not None:
            reply = QtWidgets.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.exec_()
        else:
            self.ui.set_edit_fields()

    def close_session(self):
        self.dolPath = None
        self.codePath = None
        self.sessionPath = None
        self.ui.dolTextBox.setText("")
        self.ui.gctFileTextBox.setText("")
        self.ui.gctFolderTextBox.setText("")
        self.ui.destTextBox.setText("")
        self.ui.allocLineEdit.setText("")
        self.ui.handlerTypeSelect.setCurrentIndex(0)
        self.ui.hookTypeSelect.setCurrentIndex(0)
        self.ui.txtCodesIncludeSelect.setCurrentIndex(0)
        self.ui.optimizeSelect.setCurrentIndex(0)
        self.ui.responses.setPlainText("")
        self.uiexSettings.protectCodes.setChecked(False)
        self.uiexSettings.encryptCodes.setChecked(False)
        self.uiexSettings.codehookLineEdit.setText("")
        self.uiexSettings.kernelHookLineEdit.setText("")
        self.uiexSettings.verbositySelect.setCurrentIndex(0)
        self.ui.set_edit_fields()

    def load_prefs(self):
        datapath = get_program_folder("GeckoLoader")

        try:
            with (datapath / ".GeckoLoader.conf").open("rb") as f:
                try:
                    p = cPickle.load(f)
                except cPickle.UnpicklingError as e:
                    self.log.exception(e)  # Use defaults for prefs
                else:
                    # Input validation
                    if (
                        p.get("qtstyle") in list(QtWidgets.QStyleFactory.keys())
                        or p.get("qtstyle") == "Default"
                    ):
                        self.prefs["qtstyle"] = p.get("qtstyle")

                    if p.get("darktheme") in (True, False):
                        self.prefs["darktheme"] = p.get("darktheme")

                    setCIndex = self.uiprefs.qtstyleSelect.setCurrentIndex

                    if self.prefs.get("qtstyle") in (None, "Default"):
                        setCIndex(0)
                    else:
                        setCIndex(
                            self.uiprefs.qtstyleSelect.findText(
                                self.prefs.get("qtstyle"),
                                flags=QtCore.Qt.MatchFixedString,
                            )
                        )

                    self.uiprefs.qtdarkButton.setChecked(self.prefs.get("darktheme"))
                    self.update_theme()

        except FileNotFoundError:
            self.log.warning("No preferences file found; using defaults.")

    def save_prefs(self):
        datapath = get_program_folder("GeckoLoader")

        self.prefs["qtstyle"] = str(self.uiprefs.qtstyleSelect.currentText())
        self.prefs["darktheme"] = self.uiprefs.qtdarkButton.isChecked()

        try:
            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()]
        )

        if len(self.style_log) > 2:
            self.style_log.pop(0)

        if style != "Default":
            self.app.setStyle(style)
        else:
            self.app.setStyle(self.default_qtstyle)

        if first_style_load:
            setCIndex = self.uiprefs.qtstyleSelect.setCurrentIndex
            setCIndex(
                self.uiprefs.qtstyleSelect.findText(
                    style, flags=QtCore.Qt.MatchFixedString
                )
            )

    def update_theme(self):
        if self.uiprefs.qtdarkButton.isChecked():
            self.app.setPalette(self.ui.DarkTheme)
            self.load_qtstyle("Fusion", True)
            self.uiprefs.qtstyleSelect.setDisabled(True)
        else:
            self.app.setPalette(self.ui.LightTheme)
            self.load_qtstyle(self.style_log[0][1], True)
            self.uiprefs.qtstyleSelect.setEnabled(True)

    def display_update(self):
        _outpipe = StringIO()
        _errpipe = StringIO()

        with redirect_stdout(_outpipe), redirect_stderr(_errpipe):
            try:
                self.cli.check_updates()
            except SystemExit:
                _status = False
            else:
                _status = True

        icon = QtGui.QIcon()
        icon.addPixmap(
            QtGui.QPixmap(str(resource_path(Path("bin/icon.ico")))),
            QtGui.QIcon.Normal,
            QtGui.QIcon.Off,
        )

        if _status is False:
            reply = QtWidgets.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.exec_()
        else:
            reply = QtWidgets.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.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")
        )
        self.ui.actionAbout_Qt.triggered.connect(lambda: self.show_dialog("aboutqt"))
        self.ui.actionAbout_GeckoLoader.triggered.connect(
            lambda: self.show_dialog("aboutGeckoLoader")
        )
        self.ui.actionCheck_Update.triggered.connect(lambda: self.display_update())

        self.ui.actionOpen.triggered.connect(
            lambda: self.file_dialog_exec(GUI.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)
        )
        self.ui.actionSave.triggered.connect(
            lambda: self.file_dialog_exec(GUI.Dialogs.SAVE_SESSION)
        )

        self.ui.dolButton.clicked.connect(
            lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_DOL)
        )
        self.ui.gctFileButton.clicked.connect(
            lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_GCT)
        )
        self.ui.gctFolderButton.clicked.connect(
            lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_FOLDER)
        )
        self.ui.destButton.clicked.connect(
            lambda: self.file_dialog_exec(GUI.Dialogs.LOAD_DEST)
        )

        self.ui.dolTextBox.textChanged.connect(lambda: self.ui.set_edit_fields())
        self.ui.gctFolderTextBox.textChanged.connect(lambda: self.ui.set_edit_fields())
        self.ui.gctFileTextBox.textChanged.connect(lambda: self.ui.set_edit_fields())
        self.ui.destTextBox.textChanged.connect(lambda: self.ui.set_edit_fields())

        self.ui.allocLineEdit.textChanged.connect(
            lambda: self._enforce_mask(self.ui.allocLineEdit, 0xFFFFFC)
        )

        self.ui.exOptionsButton.clicked.connect(
            lambda: self.show_dialog("Advanced Settings")
        )
        self.ui.compileButton.clicked.connect(lambda: self._exec_api())

        self.uiprefs.buttonBox.accepted.connect(self.save_prefs)
        self.uiprefs.qtstyleSelect.currentIndexChanged.connect(
            lambda: self.load_qtstyle(self.uiprefs.qtstyleSelect.currentText())
        )
        self.uiprefs.qtdarkButton.clicked.connect(lambda: self.update_theme())

        self.uiexSettings.codehookLineEdit.textChanged.connect(
            lambda: self._enforce_mask(
                self.uiexSettings.codehookLineEdit, 0x817FFFFC, 0x80000000
            )
        )
        self.uiexSettings.kernelHookLineEdit.textChanged.connect(
            lambda: self._enforce_mask(
                self.uiexSettings.kernelHookLineEdit, 0x817FFFFC, 0x80000000
            )
        )

    def _exec_api(self):
        if sys.platform == "win32":
            self.ui.responses.appendPlainText(
                f"| Session {self.compileCount} |".center(84, "=") + "\n"
            )
        else:
            self.ui.responses.appendPlainText(
                f"| Session {self.compileCount} |".center(76, "=") + "\n"
            )

        self.compileCount += 1

        if self.ui.dolTextBox.isEnabled and 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 = self.ui.gctFileTextBox.text().strip()
        elif (
            self.ui.gctFolderTextBox.isEnabled
            and 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()
        hookType = self.ui.hookTypeSelect.currentText().strip()
        hookAddress = self.uiexSettings.codehookLineEdit.text().strip()
        initAddress = self.uiexSettings.kernelHookLineEdit.text().strip()
        txtInclude = self.ui.txtCodesIncludeSelect.currentText().strip()
        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,
            "-t",
            txtInclude,
            "--hooktype",
            hookType,
        ]

        if alloc != "":
            argslist.append("-a")
            argslist.append(hex(int(alloc, 16) & 0xFFFFFC)[2:].upper())

        if hookAddress != "":
            if int(hookAddress, 16) < 0x80000000:
                self.ui.responses.appendPlainText(
                    "The specified code hook is invalid" + "\n"
                )
                return

            argslist.append("--hookaddress")
            argslist.append(hookAddress)

        if initAddress != "":
            if int(initAddress, 16) < 0x80000000:
                self.ui.responses.appendPlainText(
                    "The specified initialization address is invalid" + "\n"
                )
                return

            argslist.append("-i")
            argslist.append(initAddress)

        if dest != "":
            if dest.lower().endswith(".dol") and len(dest) > 4:
                argslist.append("--dest")
                argslist.append(dest)
            else:
                self.ui.responses.appendPlainText(
                    "The destination file path is not a valid DOL file\n"
                )
                return

        if optimize:
            argslist.append("-o")

        if protect:
            argslist.append("-p")

        if encrypt:
            argslist.append("--encrypt")

        if verbosity > 0:
            argslist.append("-" + "v" * verbosity)
        else:
            argslist.append("-q")

        args = self.cli.parse_args(argslist)

        _outpipe = StringIO()
        _errpipe = StringIO()
        _status = False
        _msg = ""

        with redirect_stdout(_outpipe), redirect_stderr(_errpipe):
            try:
                self.cli._exec(args, tmpdir=TMPDIR)
            except (SystemExit, Exception):
                _status = False
            else:
                _status = True

        if _status is False:
            _msg = f"Arguments failed! GeckoLoader couldn't execute the job\n\nArgs: {args.__repr__()}\n\nstderr: {self._remove_ansi(_errpipe.getvalue())}"
            self.ui.responses.appendPlainText(_msg.strip() + "\n")
        else:
            for line in self._remove_ansi(_outpipe.getvalue()).split("\n"):
                _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_())

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)

repo_name: Humming/Owl

license: mit


code:
![GeckoLoaderLogo](https://user-images.githubusercontent.com/60854312/159154005-b666fa45-67a6-4c6b-8a61-b986676e6083.png)

---

### 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

![Imgur](https://i.imgur.com/6cDiMW7.png)

---

## 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.

### 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` also supports patching the same `dol` file multiple times until the file becomes filled with section data.

### Compilation

Then you can do either:

   1. In command prompt, input `GeckoLoader -h` for help on syntax and options
   2. Run the command `GeckoLoader <dol> <codelist> <options>` filling in the variables as needed

Or:

   1. Fill out the relevant data in the GUI
   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

**Make sure to give proper credit to myself (JoshuaMK) and this project when you utilize it in a mod! It's much appreciated**


code:
import sys

from PyQt5 import QtCore, QtGui, QtWidgets

from fileutils import resource_path

class PrefWindow(QtWidgets.QDialog):
    def __init__(self):
        super().__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)

        self.setup_ui()

    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)
        self.setWindowIcon(icon)

        #Buttonbox
        self.buttonBox = QtWidgets.QDialogButtonBox(self)
        self.buttonBox.setFixedSize(280, 30)
        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
        self.buttonBox.setObjectName("buttonBox")

        self.formLayoutWidget = QtWidgets.QGridLayout(self)
        self.formLayoutWidget.setObjectName("formLayoutWidget")

        self.comboBoxLayout = QtWidgets.QGridLayout()
        self.comboBoxLayout.setVerticalSpacing(10)
        self.comboBoxLayout.setObjectName("comboBoxLayout")

        #qtstyle box
        self.qtstyleSelect = QtWidgets.QComboBox()
        self.qtstyleSelect.setObjectName("qtstyleSelect")
        self.comboBoxLayout.addWidget(self.qtstyleSelect, 1, 1, 1, 1)

        #qtstyle label
        self.qtstyleLabel = QtWidgets.QLabel()
        self.qtstyleLabel.setObjectName("qtstyleLabel")
        self.comboBoxLayout.addWidget(self.qtstyleLabel, 1, 0, 1, 1)

        #qtdark theme
        self.qtdarkButton = QtWidgets.QCheckBox()
        self.qtdarkButton.setObjectName("formalnaming")
        self.qtdarkButton.setText("Dark Theme")
        self.comboBoxLayout.addWidget(self.qtdarkButton, 2, 0, 1, 1)

        self.formLayoutWidget.addLayout(self.comboBoxLayout, 0, 0, 1, 1)
        self.formLayoutWidget.addWidget(self.buttonBox, 1, 0, 1, 1)

        self.retranslateUi()

        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)
        QtCore.QMetaObject.connectSlotsByName(self)

    def retranslateUi(self):
        self.setWindowTitle(QtWidgets.QApplication.translate("Dialog", "Preferences", None))
        self.qtstyleLabel.setText(QtWidgets.QApplication.translate("Dialog", "GUI Style:", None))

class SettingsWindow(QtWidgets.QDialog):
    def __init__(self):
        super().__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.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)
        self.setWindowIcon(icon)

        #Buttonbox
        self.buttonBox = QtWidgets.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.setObjectName("buttonBox")

        self.formLayoutWidget = QtWidgets.QGridLayout(self)
        self.formLayoutWidget.setObjectName("formLayoutWidget")

        self.comboBoxLayout = QtWidgets.QGridLayout()
        self.comboBoxLayout.setVerticalSpacing(10)
        self.comboBoxLayout.setObjectName("comboBoxLayout")

        #protect codes
        self.protectCodes = QtWidgets.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()
        self.encryptCodes.setObjectName("encryptCodes")
        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, 4, 0, 1, 1)

        #Codehook Address Textbox
        self.codehookLineEdit = QtWidgets.QLineEdit()
        self.codehookLineEdit.setEnabled(False)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        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()
        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.setObjectName("codehookLineEdit")
        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, 5, 0, 1, 1)

        #kernelHook Address Textbox
        self.kernelHookLineEdit = QtWidgets.QLineEdit()
        self.kernelHookLineEdit.setEnabled(False)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        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()
        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.setObjectName("kernelHookLineEdit")
        self.comboBoxLayout.addWidget(self.kernelHookLineEdit, 5, 1, 1, 1)

        #verbosity label
        self.verbosityLabel = QtWidgets.QLabel()
        self.verbosityLabel.setObjectName("verbosityLabel")
        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, 6, 1, 1, 1)

        self.formLayoutWidget.addLayout(self.comboBoxLayout, 0, 0, 1, 1)
        self.formLayoutWidget.addWidget(self.buttonBox, 1, 0, 1, 1)

        self.set_edit_fields()
        self.retranslateUi()

        self.buttonBox.accepted.connect(self.accept)
        QtCore.QMetaObject.connectSlotsByName(self)

    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)

    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))


code:
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(<Bytes>Data, <Int>GameAddress or None), tuple(<Bytes>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(<Bytes>Data, <Int>GameAddress or None), tuple(<Bytes>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)


code:
import struct
import sys
from os import getenv
from pathlib import Path

from tools import align_byte_size, get_alignment

def resource_path(relPath: str = "") -> Path:
    """
    Get absolute path to resource, works for dev and for cx_freeze
    """
    import sys

    if hasattr(sys, "_MEIPASS"):
        return Path(getattr(sys, "_MEIPASS", Path(__file__).parent)) / relPath
    else:
        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"""
    if sys.platform == "win32":
        datapath = Path(getenv("APPDATA")) / folder
    elif sys.platform == "darwin":
        if folder:
            folder = "." + folder
        datapath = Path("~/Library/Application Support").expanduser() / folder
    elif "linux" in sys.platform:
        if folder:
            folder = "." + folder
        datapath = Path.home() / folder
    else:
        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)
    )