├── icon.ico ├── openfile.bat ├── README.md ├── windowstuff.py ├── database.xml ├── globalstuff.py ├── titles.py ├── .gitignore ├── common.py ├── options.py ├── widgets.py ├── database.py ├── codeeditor.py ├── exporting.py ├── codelist.py ├── main.py └── importing.py /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CLF78/CodeManager2/HEAD/icon.ico -------------------------------------------------------------------------------- /openfile.bat: -------------------------------------------------------------------------------- 1 | C:\Users\CLF78\Desktop\CodeManagerProj\main.py "%1" 2 | pause -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Manager Reborn 2 | A revamped version of the Ocarina Code Manager 3 | 4 | # Source 5 | To run from source, these modules are required: 6 | * chardet 7 | * lxml 8 | * PyQt5 9 | 10 | # Special Thanks 11 | * Seeky, tZ and Brawlboxgaming for bearing with me through the entirety of development 12 | * Cryoma for the icon 13 | -------------------------------------------------------------------------------- /windowstuff.py: -------------------------------------------------------------------------------- 1 | import globalstuff 2 | from PyQt5.QtCore import QPoint, QRect 3 | 4 | 5 | def Half(isright=False): 6 | win = globalstuff.mainWindow.mdi.currentSubWindow() 7 | if win: 8 | pos = QPoint(globalstuff.mainWindow.mdi.width() // 2 if isright else 0, 0) 9 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width() // 2, globalstuff.mainWindow.mdi.height()) 10 | win.setGeometry(rect) 11 | win.move(pos) 12 | 13 | 14 | def TileHorizontal(): 15 | pos = QPoint(0, 0) 16 | for window in globalstuff.mainWindow.mdi.subWindowList(): 17 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width() // len(globalstuff.mainWindow.mdi.subWindowList()), globalstuff.mainWindow.mdi.height()) 18 | window.setGeometry(rect) 19 | window.move(pos) 20 | pos.setX(pos.x() + window.width()) 21 | 22 | 23 | def TileVertical(): 24 | pos = QPoint(0, 0) 25 | for window in globalstuff.mainWindow.mdi.subWindowList(): 26 | rect = QRect(0, 0, globalstuff.mainWindow.mdi.width(), globalstuff.mainWindow.mdi.height() // len(globalstuff.mainWindow.mdi.subWindowList())) 27 | window.setGeometry(rect) 28 | window.move(pos) 29 | pos.setY(pos.y() + window.height()) 30 | 31 | 32 | def MinimizeAll(): 33 | for window in globalstuff.mainWindow.mdi.subWindowList(): 34 | window.showMinimized() 35 | 36 | 37 | def CloseAll(): 38 | for window in globalstuff.mainWindow.mdi.subWindowList(): 39 | window.close() 40 | -------------------------------------------------------------------------------- /database.xml: -------------------------------------------------------------------------------- 1 | 2 | RMCP01 3 | https://raw.githubusercontent.com/H1dd3nM1nd/CodeManager2/master/database.xml 4 | 5 | 6 | 7 | 06001600 000000B8 8 | 38E00008 4800002C 9 | 38E0000B 48000024 10 | 38E0000C 4800001C 11 | 38E0000D 48000014 12 | 38E00004 4800000C 13 | 38E0000F 48000004 14 | 9421FFF0 7C0802A6 15 | 90010014 80C30004 16 | 3C80809C 80843618 17 | 80840014 38600000 18 | 80A40000 7C053000 19 | 41820018 38840248 20 | 38630001 2C03000C 21 | 41820038 4BFFFFE4 22 | 7C832378 80830090 23 | 38840001 90830090 24 | 3C80809C 608436B8 25 | 1CE7001C 7C84382E 26 | 2C040000 4182000C 27 | 7C8903A6 4E800421 28 | 3860FFFF 80010014 29 | 7C0803A6 38210010 30 | 4E800020 00000000 31 | 047A66C4 60000000 32 | 04796D30 38600000 33 | 04790EF0 39800001 34 | 04790EF4 39600001 35 | 04790EF8 39400001 36 | 04790EFC 39200001 37 | 048B54B8 80001600 38 | 048B54D0 80001608 39 | 048B54E8 80001610 40 | 048B54F4 80001618 41 | 048B5500 80001620 42 | 048B550C 80001628 43 | 44 | 45 | 46 | 47 | 48 | C2790E94 00000002 49 | 3A600010 92640004 50 | 60000000 00000000 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /globalstuff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fuck globals, all my homies hate globals 3 | """ 4 | import os 5 | import sys 6 | 7 | from PyQt5.QtGui import QPalette, QColor 8 | from PyQt5.Qt import Qt 9 | 10 | # Main window 11 | app = None 12 | mainWindow = None 13 | 14 | # Wii Title Database 15 | wiitdb = os.path.join(os.path.dirname(sys.argv[0]), 'wiitdb.txt') 16 | 17 | # GCT specific data 18 | gctmagic = b'\0\xd0\xc0\xde' * 2 19 | gctend = b'\xf0' + b'\0' * 7 20 | 21 | # Program settings 22 | nowarn = False 23 | theme = 'default' 24 | 25 | # Palettes 26 | # This palette is a workaround so that QMdiSubWindow titles don't look like crap 27 | textpal = QPalette() 28 | textpal.setColor(QPalette.Text, Qt.white) 29 | 30 | # The actual dark mode palette 31 | darkpal = QPalette() 32 | darkpal.setColor(QPalette.Window, QColor(53, 53, 53)) 33 | darkpal.setColor(QPalette.WindowText, Qt.white) 34 | darkpal.setColor(QPalette.Base, QColor(25, 25, 25)) 35 | darkpal.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) 36 | darkpal.setColor(QPalette.ToolTipBase, Qt.white) 37 | darkpal.setColor(QPalette.ToolTipText, Qt.white) 38 | darkpal.setColor(QPalette.Button, QColor(53, 53, 53)) 39 | darkpal.setColor(QPalette.ButtonText, Qt.white) 40 | darkpal.setColor(QPalette.BrightText, Qt.red) 41 | darkpal.setColor(QPalette.Link, QColor(42, 130, 218)) 42 | darkpal.setColor(QPalette.Highlight, QColor(42, 130, 218)) 43 | darkpal.setColor(QPalette.Disabled, QPalette.ButtonText, Qt.darkGray) 44 | 45 | # Empty icon for sub windows 46 | empty = None 47 | 48 | # Stylesheet for TreeWidgets because Qt sucks 49 | treeqss = """ 50 | QTreeView::indicator:unchecked { border: 1px solid #C0C0C0 } 51 | QTreeView::item { color: #FFFFFF } 52 | """ 53 | 54 | # Stylesheet for Options Menu, also because Qt sucks 55 | checkqss = """ 56 | QCheckBox::indicator:unchecked { border: 1px solid #C0C0C0 } 57 | QComboBox::item { color: #FFFFFF } 58 | """ 59 | 60 | # Program icon 61 | progico = None 62 | -------------------------------------------------------------------------------- /titles.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.request 3 | 4 | import globalstuff 5 | from PyQt5 import QtWidgets 6 | 7 | 8 | def DownloadError(): 9 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Title Database Missing', 10 | 'The Title Database (wiitdb.txt) is missing. Do you want to download it?') 11 | if msgbox == QtWidgets.QMessageBox.Yes: 12 | return DownloadTitles() 13 | return False 14 | 15 | 16 | def DownloadTitles(): 17 | try: 18 | with urllib.request.urlopen('https://www.gametdb.com/wiitdb.txt?LANG=EN') as src, open(globalstuff.wiitdb, 'wb') as dst: 19 | dst.write(src.read()) 20 | return True 21 | except: 22 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Download Error', 23 | 'There was an error during the database download. Retry?') 24 | if msgbox == QtWidgets.QMessageBox.Yes: 25 | DownloadTitles() 26 | else: 27 | return False 28 | 29 | 30 | def TitleLookup(gid: str): 31 | """ 32 | Looks up the game name for the given game id in the title database txt 33 | """ 34 | # First, check the file is still here 35 | if os.path.exists(globalstuff.wiitdb): 36 | with open(globalstuff.wiitdb, 'rb') as f: 37 | next(f) # Skip first line 38 | while True: 39 | try: # Read the line, split it and check the game id. If it matches, return the game name 40 | line = next(f).decode('utf-8', 'ignore').split(' = ') 41 | if line[0].lower() == gid.lower(): 42 | return line[1].rstrip('\r\n') 43 | except StopIteration: # We've reached EOF 44 | return 'Unknown Game' 45 | else: 46 | # Ask the user if they want to download the database 47 | retry = DownloadError() 48 | if retry: 49 | TitleLookup(gid) # Try again 50 | else: 51 | return 'Unknown Game' # Gave up, RIP 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # My stuff 133 | old/ 134 | .idea/ 135 | config.ini 136 | wiitdb.txt 137 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains functions that are used by multiple windows to prevent duplication. 3 | """ 4 | from PyQt5.Qt import Qt 5 | from PyQt5 import QtWidgets 6 | 7 | import globalstuff 8 | 9 | 10 | def GameIDMismatch(): 11 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Game ID Mismatch', 12 | "The Game ID in this codelist doesn't match this file's." 13 | "Do you want to continue?") 14 | return msgbox 15 | 16 | 17 | def CheckChildren(item: QtWidgets.QTreeWidgetItem): 18 | """ 19 | Recursively enables the check on an item's children 20 | """ 21 | for i in range(item.childCount()): 22 | child = item.child(i) 23 | if child.childCount(): 24 | CheckChildren(child) 25 | else: 26 | child.setCheckState(0, Qt.Checked) 27 | 28 | 29 | def CountCheckedCodes(source: QtWidgets.QTreeWidget, userecursive: bool): 30 | """ 31 | Returns a list of the codes currently enabled, based on certain criteria. Matchflag returns 64 if userecursive is 32 | False, 1 if True. 33 | """ 34 | return filter(lambda x: bool(x.checkState(0)), source.findItems('', Qt.MatchContains | Qt.MatchFlag(64 >> 6 * int(not userecursive)))) 35 | 36 | 37 | def SelectItems(source: QtWidgets.QTreeWidget): 38 | """ 39 | Marks items as checked if they are selected, otherwise unchecks them 40 | """ 41 | bucketlist = source.findItems('', Qt.MatchContains | Qt.MatchRecursive) 42 | for item in bucketlist: 43 | if item in source.selectedItems(): 44 | item.setCheckState(0, Qt.Checked) 45 | else: 46 | item.setCheckState(0, Qt.Unchecked) 47 | 48 | # This for categories which aren't expanded 49 | for item in filter(lambda x: x in source.selectedItems() and x.childCount() and not x.isExpanded(), bucketlist): 50 | CheckChildren(item) 51 | 52 | 53 | def CleanChildren(item: QtWidgets.QTreeWidgetItem): 54 | """ 55 | The clone function duplicates unchecked children as well, so we're cleaning those off. I'm sorry, little ones. 56 | """ 57 | for i in range(item.childCount()): 58 | child = item.child(i) 59 | if child: # Failsafe 60 | if child.childCount(): 61 | CleanChildren(child) 62 | elif child.checkState(0) == Qt.Unchecked: 63 | item.takeChild(i) 64 | 65 | 66 | def AssembleCode(code: str): 67 | """ 68 | Takes an unformatted string and adds spaces and newlines. 69 | """ 70 | assembledcode = '' 71 | for index, char in enumerate(code): 72 | if not index % 16 and index: 73 | assembledcode = '\n'.join([assembledcode, char.upper()]) 74 | elif not index % 8 and index: 75 | assembledcode = ' '.join([assembledcode, char.upper()]) 76 | else: 77 | assembledcode = ''.join([assembledcode, char.upper()]) 78 | return assembledcode 79 | -------------------------------------------------------------------------------- /options.py: -------------------------------------------------------------------------------- 1 | """ 2 | A tiny settings widget 3 | """ 4 | import configparser 5 | import os 6 | 7 | from PyQt5 import QtWidgets 8 | from PyQt5.Qt import Qt 9 | 10 | import globalstuff 11 | 12 | 13 | class SettingsWidget(QtWidgets.QDialog): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | # Disable the "?" button and resizing 18 | self.setWindowFlag(Qt.WindowContextHelpButtonHint, False) 19 | self.setFixedSize(self.minimumSize()) 20 | self.setWindowIcon(globalstuff.progico) 21 | 22 | # Initialize some variables 23 | themelist = ['Default', 'Dark'] 24 | 25 | # Autosave checkbox 26 | self.NoWarnLabel = QtWidgets.QLabel('Disable Close Warning') 27 | self.NoWarnCheckbox = QtWidgets.QCheckBox() 28 | self.NoWarnCheckbox.setChecked(globalstuff.nowarn) 29 | self.NoWarnCheckbox.stateChanged.connect(self.HandleNoWarn) 30 | 31 | # Theme selector 32 | self.ThemeLabel = QtWidgets.QLabel('Theme') 33 | self.Theme = QtWidgets.QComboBox() 34 | self.Theme.addItems(themelist) 35 | for index, content in enumerate(themelist): 36 | if content.lower() == globalstuff.theme: 37 | self.Theme.setCurrentIndex(index) 38 | break 39 | self.Theme.currentIndexChanged.connect(self.HandleThemeChoose) 40 | 41 | # Add elements to layout 42 | L = QtWidgets.QGridLayout() 43 | L.addWidget(self.NoWarnLabel, 0, 0) 44 | L.addWidget(self.NoWarnCheckbox, 0, 1) 45 | L.addWidget(self.ThemeLabel, 1, 0) 46 | L.addWidget(self.Theme, 1, 1) 47 | self.setLayout(L) 48 | self.setWindowTitle('Settings') 49 | 50 | if globalstuff.theme == 'dark': 51 | self.setStyleSheet(globalstuff.checkqss) 52 | 53 | def HandleNoWarn(self, state: int): 54 | globalstuff.nowarn = bool(state) 55 | 56 | def HandleThemeChoose(self, index: int): 57 | globalstuff.theme = self.Theme.itemText(index).lower() 58 | if globalstuff.theme == 'dark': 59 | SetDarkPalette() 60 | self.setStyleSheet(globalstuff.checkqss) 61 | else: 62 | SetLightPalette() 63 | self.setStyleSheet('') 64 | 65 | 66 | def readconfig(config: configparser.ConfigParser, file='config.ini'): 67 | """ 68 | Reads a config file, or creates one if it doesn't exist 69 | """ 70 | if not os.path.isfile(file): 71 | config['General'] = {'NoWarning': 'False', 'Theme': 'default'} 72 | else: 73 | config.read(file) 74 | 75 | # Set the globals 76 | globalstuff.nowarn = config.getboolean('General', 'NoWarning') 77 | globalstuff.theme = config['General']['Theme'] 78 | 79 | 80 | def writeconfig(config: configparser.ConfigParser, file='config.ini'): 81 | """ 82 | Writes settings to an ini file. 83 | """ 84 | config.set('General', 'NoWarning', str(globalstuff.nowarn)) 85 | config.set('General', 'Theme', globalstuff.theme) 86 | with open(file, 'w') as file: 87 | config.write(file) 88 | 89 | 90 | def SetDarkPalette(): 91 | """ 92 | Does all the changes required for dark mode. 93 | """ 94 | # Set style to fusion 95 | globalstuff.app.setStyle('Fusion') 96 | 97 | # Set mdi area bg 98 | globalstuff.mainWindow.mdi.setBackground(Qt.darkGray) 99 | 100 | # Set palette 101 | globalstuff.app.setPalette(globalstuff.darkpal) 102 | for window in globalstuff.mainWindow.mdi.subWindowList(): 103 | window.widget().setPalette(globalstuff.textpal) 104 | if hasattr(window.widget(), 'TreeWidget'): 105 | window.widget().TreeWidget.setStyleSheet(globalstuff.treeqss) 106 | 107 | # Force stylesheet on menu bar because it doesn't want to cooperate 108 | qss = """ 109 | QMenu::item { color: white } 110 | QMenu::item:disabled { color: transparent } 111 | """ 112 | globalstuff.mainWindow.menuBar().setStyleSheet(qss) 113 | 114 | 115 | def SetLightPalette(): 116 | """ 117 | Resets the default palette. 118 | """ 119 | # Set style to the first element of keys, which should be OS-specific 120 | globalstuff.app.setStyle(QtWidgets.QStyleFactory.keys()[0]) 121 | 122 | # Reset mdi area bg 123 | globalstuff.mainWindow.mdi.setBackground(Qt.gray) 124 | 125 | # Reset palette 126 | globalstuff.app.setPalette(globalstuff.app.style().standardPalette()) 127 | for window in globalstuff.mainWindow.mdi.subWindowList(): 128 | window.widget().setPalette(globalstuff.app.style().standardPalette()) 129 | if hasattr(window.widget(), 'TreeWidget'): 130 | window.widget().TreeWidget.setStyleSheet('') 131 | 132 | # Reset menu bar stylesheet 133 | globalstuff.mainWindow.menuBar().setStyleSheet('') 134 | -------------------------------------------------------------------------------- /widgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains modified widgets used by various windows. 3 | """ 4 | import globalstuff 5 | from PyQt5 import QtWidgets, QtGui 6 | from PyQt5.Qt import Qt 7 | 8 | 9 | class ModdedTreeWidget(QtWidgets.QTreeWidget): 10 | """ 11 | This modded tree widget lets me move codes between subwindows without losing data 12 | """ 13 | def __init__(self): 14 | super().__init__() 15 | 16 | # Hide header, enable multiple selection and reordering, add space to the right and set edit trigger to select+click 17 | self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) 18 | self.setHeaderHidden(True) 19 | self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection) 20 | self.setEditTriggers(QtWidgets.QAbstractItemView.SelectedClicked) 21 | header = self.header() 22 | header.setStretchLastSection(False) 23 | header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) 24 | 25 | def dragEnterEvent(self, e: QtGui.QDragEnterEvent): 26 | """ 27 | This forces the widget to accept drops, which would otherwise be rejected due to the InternalMove flag. 28 | """ 29 | src = e.source() 30 | if isinstance(src, QtWidgets.QTreeWidget): 31 | e.accept() 32 | 33 | def dropEvent(self, e: QtGui.QDropEvent): 34 | """ 35 | This bad hack adds a copy of the source widget's selected items in the destination widget. This is due to PyQt 36 | clearing the hidden columns, which we don't want. 37 | """ 38 | src = e.source() 39 | if src is not self: 40 | for item in src.selectedItems(): 41 | clone = item.clone() 42 | clone.setFlags(clone.flags() | Qt.ItemIsEditable) 43 | self.addTopLevelItem(clone) 44 | super().dropEvent(e) # Call the original function 45 | 46 | 47 | class ModdedTreeWidgetItem(QtWidgets.QTreeWidgetItem): 48 | """ 49 | Basically a glorified QTreeWidgetItem, with a couple of improvements that should make them less annoying to use. 50 | """ 51 | def __init__(self, text: str, iscategory: bool, iseditable: bool): 52 | super().__init__() 53 | 54 | # Set check state 55 | self.setCheckState(0, Qt.Unchecked) 56 | 57 | # Set default text 58 | if text: 59 | self.setText(0, text) 60 | elif iscategory: 61 | self.setText(0, 'New Category') 62 | else: 63 | self.setText(0, 'New Code') 64 | 65 | # Set flags based on the given directives 66 | self.setAsCategory(iscategory) 67 | self.setAsEditable(iseditable) 68 | 69 | def setAsCategory(self, iscategory: bool): 70 | if iscategory: 71 | self.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator) 72 | self.setFlags(self.flags() | Qt.ItemIsAutoTristate | Qt.ItemIsDropEnabled) 73 | else: 74 | self.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.DontShowIndicator) 75 | self.setFlags(self.flags() ^ Qt.ItemIsDropEnabled ^ Qt.ItemIsAutoTristate) 76 | 77 | def setAsEditable(self, iseditable: bool): 78 | if iseditable: 79 | self.setFlags(self.flags() | Qt.ItemIsEditable) 80 | elif self.flags() & Qt.ItemIsEditable: 81 | self.setFlags(self.flags() ^ Qt.ItemIsEditable) 82 | 83 | 84 | class ModdedSubWindow(QtWidgets.QMdiSubWindow): 85 | """ 86 | Dark mode and box updating functionality. 87 | """ 88 | def __init__(self, islist: bool): 89 | super().__init__() 90 | self.islist = islist 91 | self.setWindowIcon(globalstuff.empty) 92 | self.setAttribute(Qt.WA_DeleteOnClose) 93 | 94 | def setWidget(self, widget: QtWidgets.QWidget): 95 | """ 96 | Adds a fix for dark theme if it's enabled 97 | """ 98 | super().setWidget(widget) 99 | if globalstuff.theme == 'dark': 100 | w = self.widget() 101 | w.setPalette(globalstuff.textpal) 102 | if hasattr(w, 'TreeWidget'): 103 | w.TreeWidget.setStyleSheet(globalstuff.treeqss) 104 | 105 | def closeEvent(self, e: QtGui.QCloseEvent): 106 | super().closeEvent(e) 107 | if self.islist: 108 | globalstuff.mainWindow.updateboxes() 109 | 110 | 111 | class ModdedMdiArea(QtWidgets.QMdiArea): 112 | """ 113 | Modded MdiArea to accept file drops 114 | """ 115 | def __init__(self): 116 | super().__init__() 117 | self.setAcceptDrops(True) 118 | 119 | def dragEnterEvent(self, e: QtGui.QDragEnterEvent): 120 | if e.mimeData().hasUrls: 121 | e.accept() 122 | else: 123 | e.ignore() 124 | 125 | def dragMoveEvent(self, e: QtGui.QDragMoveEvent): 126 | if e.mimeData().hasUrls: 127 | e.setDropAction(Qt.CopyAction) 128 | e.accept() 129 | else: 130 | e.ignore() 131 | 132 | def dropEvent(self, e: QtGui.QDropEvent): 133 | if e.mimeData().hasUrls(): 134 | e.setDropAction(Qt.CopyAction) 135 | e.accept() 136 | links = [str(url.toLocalFile()) for url in e.mimeData().urls()] 137 | globalstuff.mainWindow.openCodelist(None, links) 138 | else: 139 | e.ignore() 140 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Databases are basically read-only lists of codes read from an xml, which adds extra information to the manager. 3 | """ 4 | import os 5 | import shutil 6 | import urllib.request 7 | from pkg_resources import parse_version as vercomp 8 | 9 | from lxml import etree 10 | from PyQt5 import QtWidgets 11 | from PyQt5.Qt import Qt 12 | 13 | import globalstuff 14 | from codelist import CodeList 15 | from codeeditor import CodeEditor, HandleCodeOpen, CleanParentz 16 | from common import CountCheckedCodes, SelectItems 17 | from titles import TitleLookup 18 | from widgets import ModdedTreeWidgetItem 19 | 20 | 21 | class Database(QtWidgets.QWidget): 22 | def __init__(self, name): 23 | super().__init__() 24 | 25 | # Create the Database Browser and connect it to the handlers 26 | self.TreeWidget = QtWidgets.QTreeWidget() 27 | self.TreeWidget.itemSelectionChanged.connect(self.HandleSelection) 28 | self.TreeWidget.itemDoubleClicked.connect(lambda x: HandleCodeOpen(x, True)) 29 | self.TreeWidget.itemClicked.connect(self.EnableButtons) 30 | 31 | # Hide header, enable multiple selection, set items as draggable and add some space on the right 32 | self.TreeWidget.setHeaderHidden(True) 33 | self.TreeWidget.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection) 34 | self.TreeWidget.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly) 35 | header = self.TreeWidget.header() 36 | header.setStretchLastSection(False) 37 | header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) 38 | 39 | # Add the search bar 40 | self.SearchBar = QtWidgets.QLineEdit() 41 | self.SearchBar.setPlaceholderText('Search codes...') 42 | self.SearchBar.textEdited.connect(self.HandleSearch) 43 | 44 | # Add the opened codelist combo box... 45 | self.Combox = QtWidgets.QComboBox() 46 | self.Combox.addItem('Create New Codelist') 47 | 48 | # ...and the "Add" button 49 | self.AddButton = QtWidgets.QPushButton('Add to Codelist') 50 | self.AddButton.setEnabled(False) 51 | self.AddButton.clicked.connect(self.HandleAdd) 52 | 53 | # Finally, add the "Update" button 54 | self.UpdateButton = QtWidgets.QPushButton('Download Updates') 55 | self.UpdateButton.clicked.connect(self.UpdateDatabase) 56 | 57 | # Make a layout and set it 58 | lyt = QtWidgets.QGridLayout() 59 | lyt.addWidget(self.SearchBar, 0, 0, 1, 2) 60 | lyt.addWidget(self.TreeWidget, 1, 0, 1, 2) 61 | lyt.addWidget(self.Combox, 2, 0) 62 | lyt.addWidget(self.AddButton, 2, 1) 63 | lyt.addWidget(self.UpdateButton, 3, 0, 1, 2) 64 | self.setLayout(lyt) 65 | 66 | # Open the database 67 | self.dbfile = name 68 | tree = etree.parse(name).getroot() 69 | 70 | # Parse game id, lookup the corresponding name, then apply them to the window title 71 | try: 72 | self.gameID = tree.xpath('id')[0].text 73 | self.gameName = TitleLookup(self.gameID) 74 | except: 75 | self.gameID = 'UNKW00' # Failsafe 76 | self.gameName = 'Unknown Game' 77 | self.setWindowTitle('Database Browser - {} [{}]'.format(self.gameName, self.gameID)) 78 | 79 | # Add the update url 80 | try: 81 | self.ver = tree.xpath('update')[0].attrib['version'] 82 | self.updateURL = tree.xpath('update')[0].text 83 | except: 84 | self.ver = '0' 85 | self.updateURL = '' 86 | 87 | # Enable the update button if an url is present 88 | self.UpdateButton.setEnabled(bool(self.updateURL)) 89 | 90 | # Import the codes (the second tree is because there can be codes without a category) 91 | self.ParseDatabase(tree.xpath('category') + tree.xpath('code')) 92 | 93 | def ParseDatabase(self, tree: etree, parent: QtWidgets.QTreeWidgetItem = None): 94 | """ 95 | Recursively create the code tree based on the xml 96 | """ 97 | for entry in tree: 98 | newitem = ModdedTreeWidgetItem(entry.attrib['name'], entry.tag == 'category', False) 99 | 100 | # Determine parenthood 101 | if parent: 102 | parent.addChild(newitem) 103 | else: 104 | self.TreeWidget.addTopLevelItem(newitem) 105 | 106 | # Determine type of entry. Elif makes sure unknown entries are ignored. 107 | if entry.tag == 'category': 108 | self.ParseDatabase(entry, newitem) 109 | elif entry.tag == 'code': 110 | newitem.setText(1, entry[0].text.strip().upper()) 111 | newitem.setText(2, entry.attrib['comment']) 112 | newitem.setText(4, entry.attrib['author']) 113 | 114 | def HandleSelection(self): 115 | """ 116 | Self explanatory. 117 | """ 118 | SelectItems(self.TreeWidget) 119 | self.EnableButtons() 120 | 121 | def EnableButtons(self): 122 | """ 123 | Updates the Add button. 124 | """ 125 | self.AddButton.setEnabled(bool(list(CountCheckedCodes(self.TreeWidget, False)))) 126 | 127 | def HandleSearch(self, text: str): 128 | """ 129 | Filters codes based on a given string 130 | """ 131 | for item in self.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive): 132 | # Hide all items 133 | item.setHidden(True) 134 | 135 | # Unhide the item if its name or code match, then unhide its parents 136 | if item.text(1) and any(text.lower() in item.text(i).lower() for i in range(2)): 137 | item.setHidden(False) 138 | self.UnhideParent(item) 139 | 140 | def UnhideParent(self, item: QtWidgets.QTreeWidgetItem): 141 | """ 142 | Recursively unhides a given item's parents 143 | """ 144 | if item.parent(): 145 | item.parent().setHidden(False) 146 | self.UnhideParent(item.parent()) 147 | 148 | def HandleAdd(self): 149 | """ 150 | Transfers the selected codes to the chosen codelist 151 | """ 152 | enabledlist = CountCheckedCodes(self.TreeWidget, False) 153 | if self.Combox.currentIndex() > 0: 154 | self.Combox.currentData().AddFromDatabase(enabledlist, self.gameID) 155 | else: 156 | win = globalstuff.mainWindow.CreateNewWindow(CodeList()) 157 | win.AddFromDatabase(enabledlist, self.gameID) 158 | 159 | def UpdateDatabase(self): 160 | """ 161 | Updates the database from the given url. 162 | """ 163 | # Download the file 164 | try: 165 | with urllib.request.urlopen(self.updateURL) as src, open('tmp.xml', 'wb') as dst: 166 | dst.write(src.read()) 167 | except: 168 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Download Error', 169 | 'There was an error during the database download. Retry?') 170 | if msgbox == QtWidgets.QMessageBox.Yes: 171 | self.UpdateDatabase() 172 | else: 173 | return 174 | 175 | # Get the tree and the version. If the program fails to do so, quietly exit 176 | try: 177 | tree = etree.parse('tmp.xml').getroot() 178 | ver = tree.xpath('update')[0].attrib['version'] 179 | except: 180 | os.remove('tmp.xml') 181 | return 182 | 183 | # Check that the new version is actually newer, otherwise exit 184 | if vercomp(ver) <= vercomp(self.ver): 185 | QtWidgets.QMessageBox.information(globalstuff.mainWindow, 'Up to date', 'Database is up to date!') 186 | os.remove('tmp.xml') 187 | return 188 | 189 | # Change the string 190 | self.ver = ver 191 | 192 | # Clean the parentz parameter of affected Code Editors 193 | # The window list is created earlier so it isn't generated a gazillion times in the for loop 194 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)] 195 | for item in filter(lambda x: bool(x.text(1)), self.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive)): 196 | CleanParentz(item, wlist) 197 | 198 | # Clear the tree and import the codes 199 | self.TreeWidget.clear() 200 | self.ParseDatabase(tree.xpath('category') + tree.xpath('code')) 201 | 202 | # Overwrite the original file and disable the update button, since we no longer need it. 203 | shutil.move('tmp.xml', self.dbfile) 204 | self.UpdateButton.setEnabled(False) 205 | -------------------------------------------------------------------------------- /codeeditor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The CodeEditor is a relatively simple window which shows a code, its name, author and comment. It also lets you edit it 3 | and open it with other windows, or add it to different lists. 4 | """ 5 | import re 6 | 7 | from PyQt5 import QtGui, QtWidgets 8 | 9 | import globalstuff 10 | from common import AssembleCode 11 | 12 | 13 | class CodeEditor(QtWidgets.QWidget): 14 | def __init__(self, parent: QtWidgets.QTreeWidgetItem = None, fromdb: bool = None): 15 | super().__init__() 16 | 17 | # Initialize vars 18 | self.parentz = parent # This is named parentz due to a name conflict 19 | self.fromdb = fromdb if fromdb else False 20 | self.dirty = False 21 | name = 'New Code' 22 | code = comment = author = '' 23 | 24 | if self.parentz: 25 | name = parent.text(0) 26 | code = parent.text(1) 27 | comment = parent.text(2) 28 | author = parent.text(4) 29 | 30 | # Create the author, code and comment forms 31 | self.NameLabel = QtWidgets.QLabel('Name:') 32 | self.CodeName = QtWidgets.QLineEdit(name) 33 | self.AuthorLabel = QtWidgets.QLabel('Author:') 34 | self.CodeAuthor = QtWidgets.QLineEdit(author) 35 | self.CodeLabel = QtWidgets.QLabel('Code:') 36 | self.CodeContent = QtWidgets.QPlainTextEdit(code) 37 | self.CommentLabel = QtWidgets.QLabel('Comment:') 38 | self.CodeComment = QtWidgets.QPlainTextEdit(comment) 39 | 40 | # Use Monospaced font for the code 41 | self.CodeContent.setFont(QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)) 42 | 43 | # Connect 44 | self.CodeName.textEdited.connect(self.SetDirty) 45 | self.CodeAuthor.textEdited.connect(self.SetDirty) 46 | self.CodeContent.textChanged.connect(self.SetDirty) 47 | self.CodeComment.textChanged.connect(self.SetDirty) 48 | 49 | # Save button 50 | self.SaveButton = QtWidgets.QPushButton('Save Changes') 51 | self.SaveButton.setEnabled(False) 52 | self.SaveButton.clicked.connect(self.SaveCode) 53 | 54 | # Add the opened codelist combo box 55 | self.Combox = QtWidgets.QComboBox() 56 | self.Combox.addItem('Create New Codelist') 57 | 58 | # Finally, add the "Add" button 59 | self.AddButton = QtWidgets.QPushButton('Save to Codelist') 60 | self.AddButton.clicked.connect(lambda: globalstuff.mainWindow.AddFromEditor(self, self.Combox.currentData())) 61 | if not code: 62 | self.AddButton.setEnabled(False) 63 | 64 | # Set the window title 65 | if author: 66 | self.setWindowTitle('Code Editor - {} [{}]'.format(name, author)) 67 | else: 68 | self.setWindowTitle('Code Editor - {}'.format(name)) 69 | 70 | # Make a layout and set it 71 | lyt = QtWidgets.QGridLayout() 72 | lyt.addWidget(self.NameLabel, 0, 0, 1, 2) 73 | lyt.addWidget(self.CodeName, 1, 0, 1, 2) 74 | lyt.addWidget(self.AuthorLabel, 2, 0, 1, 2) 75 | lyt.addWidget(self.CodeAuthor, 3, 0, 1, 2) 76 | lyt.addWidget(self.CodeLabel, 4, 0, 1, 2) 77 | lyt.addWidget(self.CodeContent, 5, 0, 1, 2) 78 | lyt.addWidget(self.CommentLabel, 6, 0, 1, 2) 79 | lyt.addWidget(self.CodeComment, 7, 0, 1, 2) 80 | lyt.addWidget(self.SaveButton, 8, 0, 1, 2) 81 | lyt.addWidget(self.Combox, 9, 0) 82 | lyt.addWidget(self.AddButton, 9, 1) 83 | self.setLayout(lyt) 84 | 85 | def SetDirty(self): 86 | """ 87 | Enables the save button if the code is not empty and the parent is set (otherwise we'd have nowhere to save to) 88 | """ 89 | # Add some dirt 90 | self.dirty = True 91 | 92 | # Set the buttons 93 | if self.CodeContent.toPlainText() and self.CodeName.text(): 94 | if not self.fromdb: 95 | self.SaveButton.setEnabled(bool(self.parentz)) 96 | self.AddButton.setEnabled(True) 97 | else: 98 | self.SaveButton.setEnabled(False) 99 | self.AddButton.setEnabled(False) 100 | 101 | # Add asterisk to window title 102 | if not self.windowTitle().startswith('*'): 103 | self.setWindowTitle('*' + self.windowTitle()) 104 | 105 | def SaveCode(self): 106 | """ 107 | Saves the code to the designated parent. 108 | """ 109 | # Initialize vars 110 | code = self.ParseCode() 111 | comment = re.sub('\n{2,}', '\n', self.CodeComment.toPlainText()) 112 | author = self.CodeAuthor.text() 113 | 114 | # Save the stuff 115 | self.parentz.setText(0, self.CodeName.text()) 116 | self.parentz.setText(1, code) 117 | self.parentz.setText(2, comment) 118 | self.parentz.setText(4, author) 119 | 120 | # Update the fields 121 | self.CodeContent.setPlainText(code) 122 | self.CodeComment.setPlainText(comment) 123 | 124 | # Update window title 125 | self.ParseAuthor(author) 126 | 127 | # Disable the save button 128 | self.SaveButton.setEnabled(False) 129 | 130 | # Clean dirt 131 | self.setWindowTitle(self.windowTitle().lstrip('*')) 132 | self.dirty = False 133 | 134 | def ParseCode(self): 135 | """ 136 | Parses the code to make sure it is formatted properly. 137 | """ 138 | # Remove spaces and new lines 139 | code = re.sub('[* \n]', '', self.CodeContent.toPlainText()) 140 | 141 | # Add padding if the code is not a multiple of 16 142 | while len(code) % 16: 143 | code += '0' 144 | 145 | # Assemble the code and force uppercase 146 | return AssembleCode(code) 147 | 148 | def ParseAuthor(self, author): 149 | """ 150 | Because we really don't like duplication. 151 | """ 152 | # Update the window title 153 | if author: 154 | self.setWindowTitle('Code Editor - {} [{}]'.format(self.CodeName.text(), author)) 155 | else: 156 | self.setWindowTitle('Code Editor - {}'.format(self.CodeName.text())) 157 | 158 | def closeEvent(self, e: QtGui.QCloseEvent): 159 | """ 160 | Overrides close event to ask the user if they want to save. 161 | """ 162 | if self.dirty: 163 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Unsaved Changes', "The code has unsaved changes in it. Save?") 164 | if msgbox == QtWidgets.QMessageBox.Yes: 165 | if self.parentz and not self.fromdb: 166 | self.SaveCode() 167 | else: 168 | globalstuff.mainWindow.AddFromEditor(self) 169 | super().closeEvent(e) 170 | 171 | 172 | def HandleCodeOpen(item: QtWidgets.QTreeWidgetItem, fromdb: bool, willcreate=True): 173 | """ 174 | Opens a tree's currently selected code in a CodeEditor window. 175 | """ 176 | if item.text(1): 177 | for window in globalstuff.mainWindow.mdi.subWindowList(): # Find if there's an existing CodeEditor with same parent and window title 178 | if isinstance(window.widget(), CodeEditor) and window.widget().parentz == item: 179 | willcreate = False 180 | globalstuff.mainWindow.mdi.setActiveSubWindow(window) # This code was already opened, so let's just set the focus on the existing window 181 | break 182 | if willcreate: # If the code is not already open, go ahead and do it 183 | globalstuff.mainWindow.CreateNewWindow(CodeEditor(item, fromdb)) 184 | 185 | 186 | def CleanParentz(item: QtWidgets.QTreeWidgetItem, wlist: list): 187 | """ 188 | Unsets the parentz parameter for the removed tree item. 189 | """ 190 | for window in wlist: 191 | if window.parentz == item: 192 | window.parentz = None 193 | break 194 | 195 | 196 | def RenameWindows(item: QtWidgets.QTreeWidgetItem): 197 | """ 198 | When you rename a code, the program will look for code editors that originated from that code and update their 199 | window title accordingly 200 | """ 201 | # First, verify that the name is not empty. If so, restore the original string and clear the backup. 202 | if not item.text(0): 203 | item.setText(0, item.statusTip(0)) 204 | item.setStatusTip(0, '') 205 | return # Since there was no update, we don't need to run the below stuff 206 | 207 | # Do the rename 208 | for w in globalstuff.mainWindow.mdi.subWindowList(): 209 | if isinstance(w.widget(), CodeEditor) and w.widget().parentz == item: 210 | w.widget().CodeName.setText(item.text(0)) 211 | if item.text(4): 212 | w.widget().setWindowTitle('Code Editor - {} [{}]'.format(item.text(0), item.text(4))) 213 | else: 214 | w.widget().setWindowTitle('Code Editor - {}'.format(item.text(0))) 215 | return 216 | -------------------------------------------------------------------------------- /exporting.py: -------------------------------------------------------------------------------- 1 | """ 2 | This files contains multiple functions to export codelists. 3 | """ 4 | import os 5 | import re 6 | from binascii import unhexlify 7 | 8 | from PyQt5 import QtWidgets 9 | from PyQt5.Qt import Qt 10 | 11 | import globalstuff 12 | from codelist import CodeList 13 | from common import CountCheckedCodes 14 | 15 | 16 | def WriteCheck(filename: str, silent: bool): 17 | """ 18 | This function performs a couple preliminary operations before importing can take place. Very informative, i know. 19 | """ 20 | # Check if we can write the file. If not, trigger an error message. 21 | if not os.access(filename, os.W_OK): 22 | if not silent: 23 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'File Write Error', "Can't write file " + filename) 24 | return False 25 | return True 26 | 27 | 28 | def WriteItems(f, enabledlist, depth): 29 | """ 30 | This recursive function is used by the TXT exporter. So much fun. 31 | """ 32 | for item in enabledlist: 33 | 34 | # It's a category. Write it only if it's not empty. 35 | if not item.text(1): 36 | if item.childCount(): 37 | f.write(''.join(['#' * depth, item.text(0), '\n\n'])) # Add the hashtags if we're in a nested category 38 | WriteItems(f, [item.child(i) for i in range(item.childCount())], depth + 1) # Recursive :o 39 | 40 | # It's a code 41 | else: 42 | 43 | # Write the code name 44 | f.write(item.text(0)) 45 | 46 | # If the code has an author, add it between "[]" 47 | if item.text(4): 48 | f.write(''.join([' [', item.text(4), ']'])) 49 | 50 | # If the code is enabled, add an asterisk at the beginning of each line 51 | if item.checkState(0) == Qt.Checked: 52 | f.writelines(['\n* ' + line for line in item.text(1).splitlines()]) 53 | 54 | # Otherwise just add a new line and write the entire code 55 | else: 56 | f.write('\n') 57 | f.write(item.text(1)) 58 | 59 | # Add the comment if it exists, preceded by a newline 60 | if item.text(2): 61 | f.write('\n') 62 | f.write(item.text(2)) 63 | 64 | # Add the final padding newlines 65 | f.write('\n\n') 66 | 67 | # We have reached the end of the list (or category). If we're in the latter, write the category escape character and the newlines 68 | if depth > 0: 69 | f.write('#' * depth) 70 | f.write('\n\n') 71 | 72 | 73 | def InvalidCharacter(name: str, line: int, char: list): 74 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Invalid Line', ''.join(['Invalid character "', char, 75 | '" in code "', name, 76 | '" in line ', str(line), 77 | '. Continue exporting?'])) 78 | return msgbox 79 | 80 | 81 | def ExportTXT(filename: str, source: CodeList, silent: bool): 82 | # Open the file 83 | f = open(filename, 'w') 84 | 85 | # Now that we opened the file, we can check if it can be written. 86 | if not WriteCheck(filename, silent): 87 | f.close() 88 | os.remove(filename) 89 | return False 90 | 91 | # Initialize vars 92 | enabledlist = source.TreeWidget.findItems('', Qt.MatchContains) 93 | 94 | # Write the game id and name 95 | f.write('\n'.join([source.gameID, source.gameName, '', ''])) 96 | 97 | # Write the codes! 98 | WriteItems(f, enabledlist, 0) 99 | 100 | # Remove the extra newline at the end, then close the file! 101 | f.seek(f.tell() - 2) # We have to use seek type 0 or the program will crash 102 | f.truncate() 103 | f.close() 104 | return True 105 | 106 | 107 | def ExportINI(filename: str, source: CodeList, silent: bool): 108 | """ 109 | The simplest export function so far. A real piece of cake. 110 | """ 111 | # Open the file. Not using "with" here due to error handling later on 112 | f = open(filename, 'w') 113 | 114 | # Now that we opened the file, we can check if it can be written. 115 | if not WriteCheck(filename, silent): 116 | f.close() 117 | os.remove(filename) 118 | return False 119 | 120 | # Initialize vars 121 | linerule = re.compile('^[\dA-F]{8} [\dA-F]{8}$', re.I | re.M) # Ignore case + multiple lines 122 | enabledlist = filter(lambda x: bool(x.text(1)), source.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive)) 123 | geckostr = '[Gecko]' 124 | geckoenabledstr = '\n[Gecko_Enabled]' # Adding a new line because it's not at the beginning of the file 125 | 126 | # Assemble the giant strings 127 | for item in enabledlist: 128 | 129 | # Add code name, code and author if present. Code must be lowercase because Dolphin. 130 | if item.text(4): 131 | geckostr = ''.join([geckostr, '\n$', item.text(0), ' [', item.text(4), ']\n', item.text(1).lower()]) 132 | else: 133 | geckostr = ''.join([geckostr, '\n$', item.text(0), '\n', item.text(1).lower()]) 134 | 135 | # Add comment if present 136 | if item.text(2): 137 | for line in item.text(2).splitlines(): 138 | geckostr = '\n*'.join([geckostr, line]) 139 | else: 140 | geckostr += '\n*' 141 | 142 | # Add to Gecko_Enabled if checked, but only if the code is valid 143 | if item.checkState(0) == Qt.Checked and len(re.findall(linerule, item.text(1))) == item.text(1).count('\n') + 1: 144 | geckoenabledstr = '\n$'.join([geckoenabledstr, item.text(0)]) 145 | 146 | # Write the codes! 147 | f.write(geckostr) 148 | 149 | # Only write gecko enabled if at least one code is enabled 150 | if len(geckoenabledstr) > 16: 151 | f.write(geckoenabledstr) 152 | 153 | # Autosaved data was found, ask the user what they want to do with it. The warning is fake as per usual. 154 | if source.scrap: 155 | if not silent: 156 | msgbox = QtWidgets.QMessageBox.question(globalstuff.mainWindow, 'Additional Data Found', 157 | 'Additional data was found in a previously imported .ini file.' 158 | 'Port the data over to this file?') 159 | if silent or msgbox == QtWidgets.QMessageBox.Yes: 160 | f.write('\n') 161 | f.write(source.scrap) 162 | source.scrap = '' 163 | 164 | # Write the final newline and close the file. Time to pack up and go home. 165 | f.write('\n') 166 | f.close() 167 | return True 168 | 169 | 170 | def ExportGCT(filename: str, source: CodeList, silent: bool): 171 | """ 172 | Exports a GCT in the regular format (screw BrawlBox) 173 | """ 174 | # Open the file. Not using "with" here due to error handling later on 175 | f = open(filename, 'wb') 176 | 177 | # Now that we opened the file, we can check if it can be written. 178 | if not WriteCheck(filename, silent): 179 | f.close() 180 | os.remove(filename) 181 | return False 182 | 183 | # Initialize vars 184 | charrule = re.compile('[\d A-F]', re.I) 185 | linerule = re.compile('^[\dA-F]{8} [\dA-F]{8}$', re.I) 186 | enabledlist = filter(lambda x: bool(x.text(1)), CountCheckedCodes(source.TreeWidget, True)) 187 | 188 | # Write the gct! 189 | f.write(globalstuff.gctmagic) 190 | for item in enabledlist: 191 | code = item.text(1).splitlines() # Remove newlines 192 | currline = 1 193 | for line in code: 194 | 195 | # Make sure there are no non-hex characters 196 | if re.match(linerule, line): 197 | f.write(unhexlify(line.replace(' ', ''))) # Didn't strip spaces earlier for line count purposes ;) 198 | currline += 1 199 | 200 | # There's an invalid character! FIND HIM! 201 | else: 202 | char = re.sub(charrule, '', line)[0] 203 | 204 | # Caught the offender. You're under arrest! 205 | if not silent and InvalidCharacter(item.text(0), currline, char) == QtWidgets.QMessageBox.No: 206 | f.close() 207 | os.remove(filename) # Remove the incomplete file 208 | return False 209 | else: 210 | f.seek(-8 * currline, 1) # Go back to the beginning of this code 211 | f.truncate() # Remove all lines after it, the code is broken 212 | break # Go to next code 213 | 214 | # Finish it off 215 | f.write(globalstuff.gctend) 216 | flen = f.tell() 217 | f.close() 218 | 219 | # If we didn't write anything at all, might as well remove the file 220 | if flen == 16: 221 | os.remove(filename) 222 | return False 223 | return True 224 | -------------------------------------------------------------------------------- /codelist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Codelists are different from databases, as they accept adding/removing, importing/exporting, reordering, dropping 3 | and more. 4 | """ 5 | from PyQt5 import QtWidgets 6 | from PyQt5.Qt import Qt 7 | 8 | import globalstuff 9 | from codeeditor import CodeEditor, HandleCodeOpen, CleanParentz, RenameWindows 10 | from common import CountCheckedCodes, SelectItems, GameIDMismatch, CleanChildren 11 | from titles import TitleLookup 12 | from widgets import ModdedTreeWidget, ModdedTreeWidgetItem 13 | 14 | 15 | class CodeList(QtWidgets.QWidget): 16 | def __init__(self, wintitle: str = None): 17 | super().__init__() 18 | 19 | # Create the codelist and connect it to the handlers 20 | self.TreeWidget = ModdedTreeWidget() 21 | self.TreeWidget.itemSelectionChanged.connect(self.HandleSelection) 22 | self.TreeWidget.itemDoubleClicked.connect(lambda x: HandleCodeOpen(x, False)) 23 | self.TreeWidget.itemChanged.connect(RenameWindows) 24 | self.TreeWidget.itemClicked.connect(self.HandleClicking) 25 | 26 | # Merge button, up here for widget height purposes 27 | self.mergeButton = QtWidgets.QPushButton('Merge Selected') 28 | self.mergeButton.clicked.connect(lambda: self.HandleMerge(CountCheckedCodes(self.TreeWidget, True))) 29 | #self.mergeButton.setShortcut('Ctrl+M') 30 | 31 | # Add button+menu 32 | addMenu = QtWidgets.QMenu() 33 | deface = addMenu.addAction('Add Code', lambda: globalstuff.mainWindow.CreateNewWindow(CodeEditor())) 34 | #deface.setShortcut('Ctrl+N') 35 | ace2 = addMenu.addAction('Add Category', self.HandleAddCategory) 36 | #ace2.setShortcut('Ctrl+Shift+N') 37 | self.addButton = QtWidgets.QToolButton() 38 | self.addButton.setDefaultAction(deface) # Do this if you click the Add button instead of the arrow 39 | self.addButton.setFixedHeight(self.mergeButton.sizeHint().height()) # Makes this the same height as QPushButton 40 | self.addButton.setMenu(addMenu) 41 | self.addButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) 42 | self.addButton.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) # Use full widget width 43 | self.addButton.setText('Add') 44 | 45 | # Sort button+menu 46 | sortMenu = QtWidgets.QMenu() 47 | defact = sortMenu.addAction('Alphabetical', lambda: self.TreeWidget.sortItems(0, Qt.AscendingOrder)) 48 | sortMenu.addAction('Alphabetical (Reverse)', lambda: self.TreeWidget.sortItems(0, Qt.DescendingOrder)) 49 | sortMenu.addAction('Size', self.SortListSize) 50 | self.sortButton = QtWidgets.QToolButton() 51 | self.sortButton.setDefaultAction(defact) # Do this if you click the Sort button instead of the arrow 52 | self.sortButton.setMenu(sortMenu) 53 | self.sortButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) 54 | self.sortButton.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) # Use full widget width 55 | self.sortButton.setText('Sort') 56 | 57 | # Import, Export and Remove buttons 58 | self.importButton = QtWidgets.QPushButton('Import List') 59 | self.exportButton = QtWidgets.QPushButton('Export List') 60 | self.removeButton = QtWidgets.QPushButton('Remove Selected') 61 | self.importButton.clicked.connect(lambda: globalstuff.mainWindow.openCodelist(self)) 62 | self.exportButton.clicked.connect(lambda: globalstuff.mainWindow.exportList(self)) 63 | self.removeButton.clicked.connect(self.HandleRemove) 64 | #self.importButton.setShortcut('Ctrl+I') 65 | #self.exportButton.setShortcut('Ctrl+E') 66 | self.removeButton.setShortcut(Qt.Key_Delete) 67 | 68 | # Configure the buttons 69 | self.EnableButtons() 70 | 71 | # Game ID text field + save button 72 | self.gidInput = QtWidgets.QLineEdit() 73 | self.gidInput.setPlaceholderText('Insert GameID here...') 74 | self.gidInput.setMaxLength(6) 75 | self.gidInput.textEdited.connect(self.UpdateButton) 76 | self.savegid = QtWidgets.QPushButton('Save') 77 | self.savegid.setEnabled(False) 78 | self.savegid.clicked.connect(lambda: self.SetGameID(self.gidInput.text())) 79 | 80 | # Set game id, game name and update the window title accordingly. Also add the scrap 81 | self.gameID = 'UNKW00' 82 | self.gameName = 'Unknown Game' 83 | self.SetGameID(wintitle if wintitle else '') 84 | self.scrap = '' 85 | 86 | # Make a horizontal layout for these two 87 | hlyt = QtWidgets.QHBoxLayout() 88 | hlyt.addWidget(self.gidInput) 89 | hlyt.addWidget(self.savegid) 90 | 91 | # Line counter 92 | self.lineLabel = QtWidgets.QLabel('Lines: 2') 93 | self.lineLabel.setAlignment(Qt.AlignRight) 94 | 95 | # Make a layout and set it 96 | lyt = QtWidgets.QGridLayout() 97 | lyt.addLayout(hlyt, 0, 0, 1, 2) 98 | lyt.addWidget(self.TreeWidget, 1, 0, 1, 2) 99 | lyt.addWidget(self.lineLabel, 2, 0, 1, 2) 100 | lyt.addWidget(self.addButton, 3, 0) 101 | lyt.addWidget(self.sortButton, 3, 1) 102 | lyt.addWidget(self.mergeButton, 4, 0) 103 | lyt.addWidget(self.removeButton, 4, 1) 104 | lyt.addWidget(self.importButton, 5, 0) 105 | lyt.addWidget(self.exportButton, 5, 1) 106 | self.setLayout(lyt) 107 | 108 | def AddFromDatabase(self, enabledlist: list, gameid: str): 109 | """ 110 | Takes a list of the enabled items and clones it in the codelist. 111 | """ 112 | # Check for game id mismatch and update if necessary 113 | if gameid != self.gameID: 114 | if self.gameID != 'UNKW00' and GameIDMismatch() == QtWidgets.QMessageBox.No: 115 | return 116 | self.SetGameID(gameid) 117 | 118 | # Add the codes 119 | for item in enabledlist: 120 | clone = item.clone() 121 | clone.setFlags(clone.flags() | Qt.ItemIsEditable) # Enable renaming 122 | self.TreeWidget.addTopLevelItem(clone) 123 | CleanChildren(clone) 124 | 125 | # Update the selection 126 | self.HandleSelection() 127 | self.UpdateLines() 128 | 129 | def HandleSelection(self): 130 | """ 131 | Self explanatory 132 | """ 133 | SelectItems(self.TreeWidget) 134 | self.EnableButtons() 135 | self.UpdateLines() 136 | 137 | def HandleClicking(self, item: QtWidgets.QTreeWidgetItem): 138 | """ 139 | Backs up the codename and checks the buttons 140 | """ 141 | item.setStatusTip(0, item.text(0)) 142 | self.EnableButtons() 143 | self.UpdateLines() 144 | 145 | def EnableButtons(self, canexport=False, canremove=False, canmerge=False): 146 | """ 147 | Enables the Remove, Export and Merge button if the respective conditions are met 148 | """ 149 | for item in CountCheckedCodes(self.TreeWidget, True): 150 | if item.text(1): 151 | if canexport: 152 | canmerge = True 153 | break # All the options are already enabled, no need to parse the list any further 154 | canexport = True 155 | canremove = True 156 | 157 | self.removeButton.setEnabled(canremove) 158 | self.exportButton.setEnabled(canexport) 159 | self.mergeButton.setEnabled(canmerge) 160 | 161 | def HandleAddCategory(self): 162 | """ 163 | Adds a new category to the codelist 164 | """ 165 | newitem = ModdedTreeWidgetItem('', True, True) 166 | self.TreeWidget.addTopLevelItem(newitem) 167 | self.TreeWidget.editItem(newitem, 0) # Let the user rename it immediately 168 | 169 | def SortListSize(self): 170 | """ 171 | Temporarily removes all items without children, then orders the remaining items alphabetically. The removed 172 | items will then be ordered by code size and re-added to the tree. 173 | """ 174 | # Remove all codes 175 | backuplist = [] 176 | for item in filter(lambda x: bool(x.text(1)), self.TreeWidget.findItems('', Qt.MatchContains)): 177 | backuplist.append(self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item))) 178 | 179 | # Sort the categories alphabetically 180 | self.TreeWidget.sortItems(0, Qt.AscendingOrder) 181 | 182 | # Sort the backup list by code size (bigger codes first) 183 | backuplist.sort(key=lambda x: len(x.text(1)), reverse=True) 184 | 185 | # Reinsert the items 186 | self.TreeWidget.insertTopLevelItems(self.TreeWidget.topLevelItemCount(), backuplist) 187 | 188 | def HandleMerge(self, mergedlist: list): 189 | """ 190 | Merges codes together 191 | """ 192 | # Initialize vars 193 | destination = None 194 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)] 195 | 196 | # Begin working 197 | for item in filter(lambda x: bool(x.text(1)), mergedlist): 198 | 199 | # We have a destination 200 | if destination: 201 | # Merge the codes 202 | destination.setText(1, '\n'.join([destination.text(1), item.text(1)])) 203 | 204 | # Kill any reference to the item 205 | CleanParentz(item, wlist) 206 | if item.parent(): 207 | item.parent().takeChild(item.parent().indexOfChild(item)) 208 | else: 209 | self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item)) 210 | 211 | # It's the first code in the list, set it as destination 212 | else: 213 | destination = item 214 | destination.setText(2, '') # Clear the comment, as it no longer applies 215 | 216 | # Now find all instances of CodeEditor that have the destination code open, and update their code widget 217 | for window in wlist: 218 | if window.parentz == destination: 219 | window.setPlainText(destination.text(1)) 220 | return 221 | 222 | def HandleRemove(self): 223 | """ 224 | Handles item removal. Not much to say here :P 225 | """ 226 | wlist = [w.widget() for w in globalstuff.mainWindow.mdi.subWindowList() if isinstance(w.widget(), CodeEditor)] 227 | for item in filter(lambda x: x.checkState(0) == Qt.Checked, CountCheckedCodes(self.TreeWidget, True)): 228 | 229 | # Remove the item 230 | if item.parent(): 231 | item.parent().takeChild(item.parent().indexOfChild(item)) 232 | else: 233 | self.TreeWidget.takeTopLevelItem(self.TreeWidget.indexOfTopLevelItem(item)) 234 | 235 | # Set all code editor widgets that had this item as parent to None 236 | if item.text(1): 237 | CleanParentz(item, wlist) 238 | 239 | def UpdateButton(self): 240 | """ 241 | Enables the button to save the game id if it's valid 242 | """ 243 | self.savegid.setEnabled(len(self.gidInput.text()) > 3) 244 | 245 | def SetGameID(self, gameid: str): 246 | """ 247 | Sets the given game id in the variable, game id text field and window title. Also looks up the game name. 248 | """ 249 | if 4 <= len(gameid) <= 6: 250 | self.gameID = gameid 251 | self.gameName = TitleLookup(gameid) 252 | self.gidInput.setText(gameid) 253 | self.savegid.setEnabled(False) 254 | self.setWindowTitle('Codelist - {} [{}]'.format(self.gameName, gameid if gameid else self.gameID)) 255 | globalstuff.mainWindow.updateboxes() 256 | 257 | def UpdateLines(self): 258 | """ 259 | Updates the number of total code lines in the list 260 | """ 261 | lines = 2 # One for the magic and one for the F0 terminator 262 | for item in filter(lambda x: bool(x.text(1)), CountCheckedCodes(self.TreeWidget, True)): 263 | lines += item.text(1).count('\n') + 1 # +1 is because the first line doesn't have an "\n" character 264 | self.lineLabel.setText('Lines: ' + str(lines)) 265 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main executable, unsurprisingly. Also known as the circular import prevention junkyard. 3 | """ 4 | import configparser 5 | import os 6 | import re 7 | import sys 8 | 9 | from PyQt5 import QtWidgets, QtGui 10 | from PyQt5.Qt import Qt 11 | 12 | import exporting 13 | import importing 14 | import globalstuff 15 | from codeeditor import CodeEditor 16 | from codelist import CodeList 17 | from database import Database 18 | from options import SettingsWidget, SetDarkPalette, readconfig, writeconfig 19 | from titles import DownloadError 20 | from widgets import ModdedSubWindow, ModdedTreeWidgetItem, ModdedMdiArea 21 | from windowstuff import TileVertical, TileHorizontal, MinimizeAll, CloseAll, Half 22 | 23 | 24 | class MainWindow(QtWidgets.QMainWindow): 25 | def __init__(self): 26 | super().__init__() 27 | 28 | # Create the interface 29 | self.mdi = ModdedMdiArea() 30 | self.setCentralWidget(self.mdi) 31 | 32 | # Create the menubar 33 | self.optgct = self.optini = self.opttxt = None 34 | self.createMenubar() 35 | 36 | # Add the program icon 37 | globalstuff.progico = QtGui.QIcon('icon.ico') 38 | self.setWindowIcon(globalstuff.progico) 39 | 40 | # Set window title and show the window maximized 41 | self.setWindowTitle('Code Manager Reborn') 42 | self.showMaximized() 43 | 44 | # Check for the wiitdb.txt file 45 | if not os.path.isfile(globalstuff.wiitdb): 46 | DownloadError() 47 | 48 | def createMenubar(self): 49 | """ 50 | Sets up the menubar 51 | """ 52 | bar = self.menuBar() 53 | 54 | # File Menu 55 | file = bar.addMenu('File') 56 | 57 | # New 58 | newmenu = file.addMenu('New') 59 | newmenu.addAction('New Codelist', lambda: self.CreateNewWindow(CodeList())) 60 | newmenu.addAction('New Code', lambda: self.CreateNewWindow(CodeEditor())) 61 | 62 | # Import menu 63 | imports = file.addMenu('Import') 64 | imports.addAction('Import Codelist', self.openCodelist) 65 | imports.addAction('Import Database', self.openDatabase) 66 | 67 | # Export menu 68 | exports = file.addMenu('Export All') 69 | self.optgct = exports.addAction('GCT', lambda: self.exportMultiple('gct')) 70 | self.opttxt = exports.addAction('TXT', lambda: self.exportMultiple('txt')) 71 | self.optini = exports.addAction('INI', lambda: self.exportMultiple('ini')) 72 | file.addSeparator() 73 | 74 | # Settings 75 | file.addAction('Options', lambda: SettingsWidget().exec_()) 76 | 77 | # Exit 78 | file.addAction('Exit', self.close) 79 | 80 | # Window Menu 81 | ws = bar.addMenu('Windows') 82 | 83 | # Half menu 84 | half = ws.addMenu('Half') 85 | half.addAction('Left', Half) 86 | half.addAction('Right', lambda: Half(True)) 87 | ws.addSeparator() 88 | 89 | # Tile menu 90 | tile = ws.addMenu('Tile') 91 | tile.addAction('Horizontal', TileHorizontal) 92 | tile.addAction('Vertical', TileVertical) 93 | ws.addSeparator() 94 | 95 | # Automatic arrangements 96 | ws.addAction('Arrange', self.mdi.tileSubWindows) 97 | ws.addAction('Cascade', self.mdi.cascadeSubWindows) 98 | ws.addSeparator() 99 | 100 | # Mass minimize/close 101 | ws.addAction('Minimize All', MinimizeAll) 102 | ws.addAction('Close All', CloseAll) 103 | 104 | # Update the menu 105 | self.updateboxes() 106 | 107 | def openDatabase(self): 108 | """ 109 | Opens a dialog to let the user choose a database. 110 | """ 111 | name = QtWidgets.QFileDialog.getOpenFileName(self, 'Open Database', '', 'Code Database (*.xml)')[0] 112 | if name: 113 | self.CreateNewWindow(Database(name)) 114 | 115 | def openCodelist(self, source: QtWidgets.QTreeWidget = None, files: list = None): 116 | """ 117 | Opens a QFileDialog to import a file 118 | """ 119 | if not files: 120 | files = QtWidgets.QFileDialog.getOpenFileNames(self, 'Open Files', '', 121 | 'All supported formats (*.txt *.ini *.gct *.dol);;' 122 | 'Text File (*.txt);;' 123 | 'Dolphin INI (*.ini);;' 124 | 'Gecko Code Table (*.gct);;' 125 | 'Dolphin Executable (*.dol)')[0] 126 | 127 | # Run the correct function based on the chosen format 128 | for file in files: 129 | func = getattr(importing, 'Import' + os.path.splitext(file)[1].lstrip('.').upper(), None) 130 | if func: 131 | func(file, source) 132 | 133 | def exportList(self, source: QtWidgets.QTreeWidget): 134 | """ 135 | Opens a QFileDialog to save a single codelist to a file. 136 | """ 137 | file = QtWidgets.QFileDialog.getSaveFileName(self, 'Save Codelist To', source.gameID, 138 | 'Gecko Code Table (*.gct);;' 139 | 'Text File (*.txt);;' 140 | 'Dolphin INI (*.ini);;')[0] 141 | 142 | # Run the correct function based on the chosen format 143 | func = getattr(exporting, 'Export' + os.path.splitext(file)[1].lstrip('.').upper(), None) 144 | if func: 145 | success = func(file, source, False) 146 | 147 | # Inform the user 148 | if success: 149 | QtWidgets.QMessageBox.information(self, 'Export Complete', 'List exported succesfully!') 150 | 151 | def exportMultiple(self, ext: str): 152 | """ 153 | Exports all the currently opened codelists at once to the given format. Filename defaults to the game id. 154 | """ 155 | # Get destination and codelists 156 | dest = QtWidgets.QFileDialog.getExistingDirectory(self, 'Save all Codelists to', '', QtWidgets.QFileDialog.ShowDirsOnly) 157 | success = total = 0 158 | overwrite = permanent = False 159 | 160 | # Do the thing 161 | if dest: 162 | for window in filter(lambda x: isinstance(x.widget(), CodeList), self.mdi.subWindowList()): 163 | 164 | # Initialize vars 165 | filename = os.path.join(dest, '.'.join([window.widget().gameID, ext])) 166 | i = 2 167 | 168 | # If the file already exists, ask the user what to do 169 | if os.path.isfile(filename) and not permanent: 170 | msgbox = QtWidgets.QMessageBox(self) 171 | msgbox.setIcon(QtWidgets.QMessageBox.Question) 172 | msgbox.setWindowTitle('Overwrite file?') 173 | msgbox.setText(os.path.basename(filename) + ' already exists. Overwrite?') 174 | msgbox.setStandardButtons(QtWidgets.QMessageBox.YesToAll | QtWidgets.QMessageBox.Yes | 175 | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.NoToAll | 176 | QtWidgets.QMessageBox.Ignore) 177 | ret = msgbox.exec_() 178 | 179 | # If the user presses ignore, skip writing, else set the flags 180 | if ret == QtWidgets.QMessageBox.Ignore: 181 | continue 182 | overwrite = ret <= QtWidgets.QMessageBox.YesToAll 183 | permanent = ret == QtWidgets.QMessageBox.YesToAll or ret == QtWidgets.QMessageBox.NoToAll 184 | 185 | # If we don't want to overwrite, check that the file doesn't exist and if so bump up the number 186 | if not overwrite: 187 | while os.path.isfile(filename): 188 | filename = os.path.join(dest, '{}_{}.{}'.format(window.widget().gameID, i, ext)) 189 | i += 1 190 | 191 | # Choose the correct function based on the provided extension 192 | func = getattr(exporting, 'Export' + ext.upper(), None) 193 | if func: 194 | success += func(filename, window.widget(), True) 195 | total += 1 196 | 197 | # Reset overwrite flag is permanent is not active 198 | if not permanent: 199 | overwrite = False 200 | 201 | # Inform the user 202 | if success: 203 | QtWidgets.QMessageBox.information(self, 'Export Complete', '{}/{} lists exported successfully!'.format(success, total)) 204 | 205 | def updateboxes(self): 206 | """ 207 | Looks for opened codelist sub-windows and adds them to each database' combo box. 208 | """ 209 | # Initialize vars 210 | dblist = [] 211 | entries = [] 212 | 213 | # Fill the two lists 214 | for window in self.mdi.subWindowList(): 215 | if isinstance(window.widget(), CodeList): 216 | entries.append(window.widget()) 217 | else: 218 | dblist.append(window.widget()) 219 | 220 | # Update the "Export All" option 221 | notempty = bool(entries) 222 | self.optgct.setEnabled(notempty) 223 | self.opttxt.setEnabled(notempty) 224 | self.optini.setEnabled(notempty) 225 | 226 | # Begin updating each combo box 227 | for window in dblist: 228 | 229 | # Process the combo box in reverse, so we can safely delete items without worrying about wrong indexes 230 | for i in reversed(range(1, window.Combox.count())): 231 | item = window.Combox.itemData(i) 232 | if item.parentWidget() not in self.mdi.subWindowList(): 233 | window.Combox.removeItem(i) 234 | else: 235 | # Sometimes this fails, so i added an except because i'm lame 236 | try: 237 | entries.remove(item) 238 | except ValueError: 239 | continue 240 | 241 | # Add the remaining windows if they meet the condition 242 | for entry in entries: 243 | window.Combox.addItem(entry.windowTitle().lstrip('Codelist - '), entry) # Only keep game name and id 244 | 245 | def CodeLookup(self, item: QtWidgets.QTreeWidgetItem, codelist: QtWidgets.QTreeWidget, gid: str): 246 | """ 247 | Looks for a possible match in opened windows with the same game id. 248 | """ 249 | # Initialize vars 250 | wlist = [w.widget() for w in self.mdi.subWindowList() if isinstance(w.widget(), Database) 251 | or isinstance(w.widget(), CodeList) and w.widget().TreeWidget is not codelist] 252 | lsplt = re.split('[ \n]', item.text(1)) 253 | totalen = len(lsplt) 254 | 255 | # Begin search! 256 | for widget in wlist: 257 | 258 | # Mark code matches from different game ids with an additional asterisk 259 | regmatch = int(not(bool(widget.gameID == gid))) + 1 260 | 261 | # Process the widget's tree 262 | for child in filter(lambda x: x.text(1) and 'Unknown Code' not in x.text(0), 263 | widget.TreeWidget.findItems('', Qt.MatchContains | Qt.MatchRecursive)): 264 | matches = 0 265 | 266 | # For each code, check each line of the code we're looking a name for 267 | for line in lsplt: 268 | if line in child.text(1): 269 | matches += 1 270 | 271 | # If more than 2/3rds of the code match, we found the code we were looking for 272 | if matches / totalen >= 2 / 3: 273 | item.setText(0, child.text(0) + '*' * regmatch) 274 | item.setText(2, child.text(2)) # Copy comment 275 | item.setText(4, child.text(4)) # Copy author 276 | return 277 | 278 | def AddFromEditor(self, src: CodeEditor, dest: CodeList = None): 279 | """ 280 | Transfers the code editor's content to a code in a codelist. If you're wondering why this is here, it's to 281 | prevent circular imports. Fuck circular imports. 282 | """ 283 | # Initialize vars 284 | code = src.ParseCode() 285 | comment = re.sub('\n{2,}', '\n', src.CodeComment.toPlainText()) # Consecutive new lines can screw things up 286 | author = src.CodeAuthor.text() 287 | 288 | # Create a new codelist if dest is None 289 | if not dest: 290 | dest = self.CreateNewWindow(CodeList()) 291 | 292 | # Save the stuff 293 | newitem = ModdedTreeWidgetItem(src.CodeName.text(), False, True) 294 | newitem.setText(1, code) 295 | newitem.setText(2, comment) 296 | newitem.setText(4, author) 297 | 298 | # Update the fields 299 | src.CodeContent.setPlainText(code) 300 | src.CodeComment.setPlainText(comment) 301 | 302 | # Update window title 303 | src.ParseAuthor(author) 304 | 305 | # Remove the dirt 306 | src.dirty = False 307 | src.setWindowTitle(src.windowTitle().lstrip('*')) 308 | 309 | # Add the item to the widget 310 | dest.TreeWidget.addTopLevelItem(newitem) 311 | 312 | def closeEvent(self, e: QtGui.QCloseEvent): 313 | """ 314 | Overrides the close event to warn the user of opened lists/codes. 315 | """ 316 | # Check if the warning is disabled and that we have any code list/editor open 317 | if not globalstuff.nowarn and len([w for w in self.mdi.subWindowList() if isinstance(w.widget(), CodeList) or isinstance(w.widget(), CodeEditor)]): 318 | 319 | # Raise awareness! 320 | msgbox = QtWidgets.QMessageBox(self) 321 | cb = QtWidgets.QCheckBox("Don't show this again") 322 | msgbox.setIcon(QtWidgets.QMessageBox.Question) 323 | msgbox.setWindowTitle('Opened Codes') 324 | msgbox.setText('Some codes are still open, are you sure you want to close?') 325 | msgbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 326 | msgbox.setCheckBox(cb) 327 | ret = msgbox.exec_() 328 | 329 | # Update warning disable parameter 330 | globalstuff.nowarn = bool(cb.checkState()) 331 | 332 | # Act in accordance to the user's choice 333 | if ret == QtWidgets.QMessageBox.No: 334 | e.ignore() 335 | return 336 | e.accept() 337 | 338 | def CreateNewWindow(self, widget: QtWidgets.QWidget): 339 | win = ModdedSubWindow(isinstance(widget, CodeList)) 340 | win.setWidget(widget) 341 | self.mdi.addSubWindow(win) 342 | self.updateboxes() 343 | win.show() 344 | return widget 345 | 346 | 347 | def main(): 348 | 349 | # Load config 350 | config = configparser.ConfigParser() 351 | readconfig(config) 352 | 353 | # Start the application 354 | globalstuff.app = QtWidgets.QApplication(sys.argv) 355 | globalstuff.mainWindow = MainWindow() 356 | 357 | # Add the empty icon 358 | icon = QtGui.QPixmap(1, 1) 359 | icon.fill(Qt.transparent) 360 | globalstuff.empty = QtGui.QIcon(icon) 361 | 362 | # Open codelists passed through the shell 363 | flist = [file for file in sys.argv[1:] if os.path.isfile(file)] 364 | if flist: 365 | globalstuff.mainWindow.openCodelist(None, flist) 366 | 367 | # Apply theme if dark mode is enabled 368 | if globalstuff.theme == 'dark': 369 | SetDarkPalette() 370 | 371 | # Execute 372 | ret = globalstuff.app.exec_() 373 | 374 | # Update config 375 | writeconfig(config) 376 | 377 | # Quit the process 378 | sys.exit(ret) 379 | 380 | 381 | if __name__ == '__main__': 382 | main() 383 | -------------------------------------------------------------------------------- /importing.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains multiple functions to import codelists. 3 | """ 4 | import os 5 | import re 6 | from itertools import chain 7 | from typing import Optional, BinaryIO 8 | from struct import unpack 9 | 10 | from chardet import detect 11 | from PyQt5 import QtWidgets 12 | from PyQt5.Qt import Qt 13 | 14 | import globalstuff 15 | from common import GameIDMismatch, AssembleCode 16 | from codelist import CodeList 17 | from widgets import ModdedTreeWidgetItem 18 | 19 | 20 | def GameIDCheck(gameid: str, codelist: CodeList): 21 | """ 22 | Checks if the game id matches the codelist's current one. If not, it alerts the user, asking them whether they 23 | want to continue importing or not. 24 | """ 25 | if codelist.gameID != gameid: 26 | if codelist.gameID != 'UNKW00' and GameIDMismatch() == QtWidgets.QMessageBox.No: 27 | return False 28 | codelist.SetGameID(gameid.upper()) 29 | return True 30 | 31 | 32 | def DoPreliminaryOperations(filename: str, codelist: Optional[CodeList]): 33 | """ 34 | This function performs a couple preliminary operations before importing can take place. Very informative, i know. 35 | """ 36 | # Check if we can read the file. If not, trigger an error message. 37 | if not os.access(filename, os.R_OK): 38 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'File Read Error', "Couldn't read file " + filename) 39 | return None 40 | 41 | # If the codelist param is not set, we want to create a new window, so do that 42 | if not codelist: 43 | return globalstuff.mainWindow.CreateNewWindow(CodeList()) 44 | return codelist 45 | 46 | 47 | def ImportTXT(filename: str, codelist: CodeList): 48 | """ 49 | Imports a TXT. This took longer than it should have. 50 | """ 51 | # Perform the initial operations. If they fail, abort everything. 52 | codelist = DoPreliminaryOperations(filename, codelist) 53 | if not codelist: 54 | return 55 | 56 | # Initialize vars 57 | linerule = re.compile('^(\* )?[\w]{8} [\w]{8}$', re.I) 58 | unkcount = 1 # Used for codes without names 59 | currdepth = 0 # Current depth, used for sub-categories 60 | parents = {0: None} # This dict stores the parent for each level. Not the best solution, but it gets the job done. 61 | 62 | # Set the tree widget 63 | listwidget = codelist.TreeWidget 64 | 65 | # Open the file and read it 66 | with open(filename, 'rb') as f: 67 | rawdata = f.read() 68 | 69 | # Now that we read the file, detect its encoding and split it into groups (there's an empty line between each). 70 | # This is done because the original Code Manager saves in UTF-16, which would fuck up the formatting if not decoded. 71 | rawdata = rawdata.decode(detect(rawdata)['encoding'], 'ignore').split(os.linesep * 2) 72 | 73 | # The first group contains the gameid, so check it with regex and set it if it's valid 74 | gameid = rawdata[0].splitlines()[0].strip() 75 | if 4 <= len(gameid) <= 6: 76 | if not GameIDCheck(gameid, codelist): 77 | return 78 | rawdata.pop(0) # Remove the parsed group 79 | 80 | # Begin parsing codes 81 | for group in rawdata: 82 | 83 | # Initialize vars 84 | name = code = comment = author = '' 85 | isenabled = False 86 | 87 | # Parse group 88 | for line in group.splitlines(): 89 | m = re.match(linerule, line) 90 | 91 | # It's a code line 92 | if m: 93 | if not isenabled and '*' in m[0]: # Asterisks are used to mark enabled codes, so mark it as such 94 | isenabled = True 95 | code = '\n'.join([code, m[0].lstrip('* ')]) 96 | 97 | # It's not a code line 98 | else: 99 | if name: # We already have a name set, so add this line to the comment 100 | comment = '\n'.join([comment, line]) 101 | else: # The code doesn't have a name yet, so set it to this line. Also check for the author name 102 | lspl = line.split(' [') 103 | name = lspl[0] 104 | if len(lspl) > 1: 105 | author = lspl[1].rstrip(']') # Remove the last character 106 | 107 | # Failsafe if the code name is fully empty 108 | if not name: 109 | name = 'Unknown Code ' 110 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly): 111 | unkcount += 1 112 | name += str(unkcount) 113 | 114 | # If the name only contains "#" characters, it represents the end of a category, so don't add it to the tree 115 | if not name.lstrip('#'): 116 | currdepth = name.count('#') - 1 117 | 118 | # Else, create the tree item 119 | else: 120 | newitem = ModdedTreeWidgetItem(name.lstrip('#'), not(bool(code)), True) 121 | 122 | # If it's a category, set the depth and the parents key 123 | if not code: 124 | currdepth = name.count('#') 125 | parents[currdepth+1] = newitem 126 | 127 | # Otherwise, it's a code, so add the code, comment and author 128 | else: 129 | newitem.setText(1, code.lstrip('\n').upper()) # Force uppercase, because lowercase sucks. 130 | newitem.setText(2, comment.lstrip('\n')) 131 | newitem.setText(4, author) 132 | 133 | # If enabled, tick the check 134 | if isenabled: 135 | newitem.setCheckState(0, Qt.Checked) 136 | 137 | # If the name is unknown, look it up 138 | if 'Unknown Code' in newitem.text(0): 139 | globalstuff.mainWindow.CodeLookup(newitem, codelist, gameid) 140 | 141 | # Set the item's parent. If there's a key error, don't do anything. Gotta stay safe. 142 | try: 143 | parent = parents[currdepth] 144 | except KeyError: 145 | pass 146 | 147 | # Determine parenthood. Don't believe the warning! Currdepth is 0 even if all parent changes are skipped ;) 148 | if parent: 149 | parent.addChild(newitem) 150 | else: 151 | listwidget.addTopLevelItem(newitem) 152 | 153 | # Add 1 to depth, as children will be 1 level further down 154 | if not code: 155 | currdepth += 1 156 | 157 | # Finally, trigger the buttons in the codelist 158 | codelist.EnableButtons() 159 | codelist.UpdateLines() 160 | 161 | 162 | def ImportINI(filename: str, codelist: CodeList): 163 | """ 164 | ImportTXT's uglier brother. Also, Dolphin is an asshole. 165 | """ 166 | # Perform the initial operations. If they fail, abort everything. 167 | codelist = DoPreliminaryOperations(filename, codelist) 168 | if not codelist: 169 | return 170 | 171 | # Set the tree widget 172 | listwidget = codelist.TreeWidget 173 | 174 | # Set the gameID 175 | gameid = os.path.splitext(os.path.basename(filename))[0] # Remove the file extension 176 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist): 177 | return 178 | 179 | # Open the file 180 | with open(filename) as f: 181 | rawdata = f.read().splitlines() 182 | 183 | # First, we have to find the sections containing the codes between all the file's sections 184 | length = len(rawdata) 185 | n = o = 0 186 | m = p = length # These will be set to the end of the file, in case there are no other sections than what we need 187 | for i, line in enumerate(rawdata, 1): # This starts from 1, in case of the first section being at index 0 188 | if line == '[Gecko]': 189 | n = i 190 | elif line == '[Gecko_Enabled]': 191 | o = i 192 | elif i < length - 1 and rawdata[i].startswith('['): 193 | """ 194 | If the next line begins a section, set this line as the end of the current section, but with some limits: 195 | - If n > o, we're in the Gecko section. But if m is set, we're somewhere between them, so don't do anything 196 | - If n < o, we're in the Gecko_Enabled section. But if p is set, we're somewhere between them, so don't do anything 197 | - Finally, if n = o, it means we're in an unknown section, so don't do anything either. 198 | """ 199 | if n > o and m == length: 200 | m = i 201 | elif n < o and p == length: 202 | p = i 203 | 204 | # We got the indexes, create the subsections. My palms are already sweating. 205 | gecko = rawdata[n:m] 206 | geckoenabled = rawdata[o:p] 207 | 208 | # The rest of the file won't be wasted! It will be stored so if the user exports the list as ini, this data will be 209 | # ported over. 210 | if n or p != length or m != o-1: 211 | scrap = '\n'.join(chain(rawdata[:n-1], rawdata[m:o-1], rawdata[p:])) 212 | if scrap: 213 | codelist.scrap = scrap 214 | 215 | # Initialize vars 216 | entrylist = [] 217 | unkcount = 1 218 | 219 | # Parse the gecko section 220 | for line in gecko: 221 | 222 | # It's a code name, and code names need some extra parsing 223 | if line.startswith('$'): 224 | 225 | # First, we must exclude the author from the code name, as it will fuck up Gecko_Enabled otherwise 226 | lspl = line.split(' [') 227 | name = lspl[0].lstrip('$') # Remove the first character 228 | 229 | # Set the author name if present 230 | author = '' 231 | if len(lspl) > 1: 232 | author = lspl[1].rstrip(']') # Remove the last character 233 | 234 | # If the resulting name is empty, apply the following failsafe 235 | if not name: 236 | name = 'Unknown Code ' 237 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly): 238 | unkcount += 1 239 | name += str(unkcount) 240 | unkcount += 1 241 | 242 | # Create the widget 243 | newitem = ModdedTreeWidgetItem(name, False, True) 244 | newitem.setText(4, author) 245 | entrylist.append(newitem) 246 | 247 | # It's a comment line. Not using "and" because the line would end up in the "else" 248 | elif line.startswith('*'): 249 | if len(line) > 1: 250 | newitem.setText(2, '\n'.join([newitem.text(2), line.lstrip('*')])) # Only add if the line is not empty 251 | 252 | # It's a code line 253 | else: 254 | newitem.setText(1, '\n'.join([newitem.text(1), line.upper()])) 255 | 256 | # Parse the geckoenabled section and add the newly created widgets to the codelist 257 | for item in entrylist: 258 | 259 | # Enable the check if the name matches 260 | if '$' + item.text(0) in geckoenabled: 261 | item.setCheckState(0, Qt.Checked) 262 | 263 | # Remove the extra newlines at the beginning of these two fields 264 | item.setText(1, item.text(1).lstrip('\n')) 265 | item.setText(2, item.text(2).lstrip('\n')) 266 | 267 | # Do code lookup if code doesn't have a name 268 | if 'Unknown Code' in item.text(0): 269 | globalstuff.mainWindow.CodeLookup(item, codelist, gameid) 270 | 271 | # Add to tree widget 272 | listwidget.addTopLevelItem(item) 273 | 274 | # Finally, trigger the buttons in the codelist 275 | codelist.EnableButtons() 276 | codelist.UpdateLines() 277 | 278 | 279 | def ImportGCT(filename: str, codelist: CodeList): 280 | """ 281 | ImportTXT's siamese twins. 282 | """ 283 | # Perform the initial operations. If they fail, abort everything. 284 | codelist = DoPreliminaryOperations(filename, codelist) 285 | if not codelist: 286 | return 287 | 288 | # Do the parsing 289 | with open(filename, 'rb') as f: 290 | if f.read(8) == globalstuff.gctmagic: # Check for the magic 291 | f.seek(-8, 2) # Go to the end of the file 292 | 293 | # If the "Codelist End" is at the end of the file, we have a regular GCT 294 | if f.read() == globalstuff.gctend: 295 | ParseGCT(os.path.splitext(os.path.basename(filename))[0], f, codelist) 296 | 297 | # Otherwise we have an extended GCT 298 | else: 299 | ParseExtendedGCT(f, codelist) 300 | else: 301 | # This ain't it, chief 302 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Invalid file', 'This file is invalid') 303 | 304 | 305 | def ParseExtendedGCT(f: BinaryIO, codelist: CodeList): 306 | """ 307 | BrawlBox allows you to store code names and offsets in the GCT. So, this is for GCTs using that feature. 308 | """ 309 | # Initialize vars 310 | backupoffset = 0 311 | 312 | # Set the tree widget 313 | listwidget = codelist.TreeWidget 314 | 315 | # First, let's get the file's length 316 | filelen = f.tell() 317 | f.seek(0) 318 | 319 | # Now, let's find the codelist end 320 | while f.tell() < filelen: 321 | if f.read(8) == globalstuff.gctend: 322 | f.seek(4, 1) 323 | backupoffset = f.tell() # Saving this for when i need to go back 324 | break 325 | 326 | # Failsafe time 327 | if f.tell() == filelen: 328 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Invalid file', 'This file is invalid') 329 | return 330 | 331 | # Now let's find the game id. Why -8 ? 332 | # First, the offset is according to the entry's beginning (aka the game name which was skipped) 333 | # Second, the seek needs to be re-adjusted due to the read operation 334 | f.seek(unpack('I', f.read(4))-8, 1) 335 | 336 | # Get the string 337 | gameid = '' 338 | while f.tell() < filelen: 339 | char = f.read(1) 340 | if char == b'\0': 341 | break 342 | gameid += char.decode('utf-8', 'ignore') 343 | 344 | # Verify the gameid's validity 345 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist): 346 | return 347 | 348 | # Read the amount of codes 349 | f.seek(backupoffset) # Go back 350 | f.seek(4, 1) 351 | amount = unpack('I', f.read(4)) 352 | 353 | # Begin reading codes! 354 | while amount > 0: 355 | # Read the offsets 356 | codeoffs = unpack('I', f.read(4)) 357 | codelen = unpack('I', f.read(4)) 358 | nameoffs = f.tell() + unpack('I', f.read(4)) - 8 # Offset starts at beginning of entry 359 | commentoffs = f.tell() + unpack('I', f.read(4)) - 12 # Same here 360 | if commentoffs < f.tell(): # If there's no comment the value is 0, so if we subtract 12 we'll be at a smaller offset 361 | commentoffs = 0 362 | backupoffset = f.tell() 363 | 364 | # Go to the code and read it 365 | f.seek(codeoffs) 366 | code = AssembleCode(f.read(codelen * 8).hex()) # Convert to hex string and add spaces and newlines 367 | 368 | # Go to the code name and read it 369 | codename = '' 370 | f.seek(nameoffs) 371 | while f.tell() < filelen: 372 | char = f.read(1) 373 | if char == b'\0': 374 | break 375 | codename += char.decode('utf-8', 'ignore') 376 | 377 | # Find the author inside the name 378 | lspl = codename.split(' [') 379 | codename = lspl[0] 380 | author = '' 381 | if len(lspl) > 1: 382 | author = lspl[1].rstrip(']') # Remove the last character 383 | 384 | # Go the comment and read it 385 | comment = '' 386 | if commentoffs: 387 | f.seek(commentoffs) 388 | while f.tell() < filelen: 389 | char = f.read(1) 390 | if char == b'\0': 391 | break 392 | comment += char.decode('utf-8', 'ignore') 393 | 394 | # Create the tree widget 395 | newitem = ModdedTreeWidgetItem(codename, False, True) 396 | newitem.setText(1, code) 397 | newitem.setText(2, comment) 398 | newitem.setText(4, author) 399 | listwidget.addTopLevelItem(newitem) 400 | 401 | # Go back to the offset we backed up earlier 402 | f.seek(backupoffset) 403 | amount -= 1 404 | 405 | 406 | def ParseGCT(filename: str, f: BinaryIO, codelist: CodeList): 407 | """ 408 | This GCT parser is for the normal format. It tries to split codes according to the codetypes. 409 | """ 410 | # Initialize vars 411 | currentcode = False 412 | amount = 0 413 | unkcount = 1 414 | finalist = [] 415 | 416 | # Set the tree widget 417 | listwidget = codelist.TreeWidget 418 | 419 | # First, let's get the file's length 420 | filelen = f.tell() - 8 # Ignore the F0 line 421 | f.seek(8) # Go back to the beginning and skip the GCT magic 422 | 423 | # Verify the gameid's validity 424 | gameid = os.path.splitext(os.path.basename(filename))[0] 425 | if 4 <= len(gameid) <= 6 and not GameIDCheck(gameid, codelist): 426 | return 427 | 428 | # Begin reading the GCT! 429 | while f.tell() < filelen: 430 | # Read the next line and get its first byte 431 | line = f.read(8) 432 | c = int(hex(line[0]), 16) 433 | 434 | # If we are currently in a code 435 | if currentcode: 436 | # If we have exhausted the amount of lines specified or we meet an "E0" line, don't add anymore lines 437 | if amount == 0 or (amount == -1 and c == 0xE0): 438 | currentcode = False 439 | elif amount > 0: 440 | amount -= 1 441 | 442 | # Add the line. Yes PyCharm, i know newitem could be referenced before assignment, but currentcode is never 443 | # true when the loop begins, so shut the fuck up. 444 | newitem.setText(1, newitem.text(1) + line.hex()) 445 | 446 | # It's a new code! 447 | else: 448 | # Set name 449 | name = 'Unknown Code ' 450 | while listwidget.findItems(name + str(unkcount), Qt.MatchExactly): 451 | unkcount += 1 452 | name += str(unkcount) 453 | unkcount += 1 454 | 455 | # Create the tree widget item 456 | newitem = ModdedTreeWidgetItem(name, False, True) 457 | newitem.setText(1, line.hex()) 458 | finalist.append(newitem) 459 | 460 | # Check the codetype. If the line isn't listed here, it will be added as a single line if found standalone. 461 | # Type 06 (length specified by code, in bytes) 462 | if c == 6 or c == 7: 463 | lines = int(line[7:].hex(), 16) 464 | amount = (lines + 7) // 8 - 1 # Add 7 to approximate up 465 | currentcode = True 466 | 467 | # Type 08 (fixed length) 468 | elif c == 8 or c == 9: 469 | currentcode = True 470 | 471 | # Type 20-2F, 40, 42, 48, 4A, A8-AE, F6 (add lines until we find an E0 line) 472 | elif 0x20 <= c <= 0x2F or c == 0x40 or c == 0x42 or c == 0x48 or c == 0x4A or 0xA8 <= c <= 0xAE or c == 0xF6: 473 | amount = -1 474 | currentcode = True 475 | 476 | # Type C0, C2, C4, F2/F4 (length specified by code, in lines) 477 | elif c == 0xC0 or 0xC2 <= c <= 0xC5 or 0xF2 <= c <= 0xF5: 478 | amount = int(line[7:].hex(), 16) - 1 479 | currentcode = True 480 | 481 | # Add spaces and newlines to the codes, then add the items to the tree 482 | for item in finalist: 483 | item.setText(1, AssembleCode(item.text(1))) 484 | globalstuff.mainWindow.CodeLookup(item, listwidget, filename) 485 | listwidget.addTopLevelItem(item) 486 | 487 | 488 | def ImportDOL(filename: str, codelist: CodeList): 489 | """ 490 | The ImportGCT twins' older sister. 491 | """ 492 | # Perform the initial operations. If they fail, abort everything. 493 | codelist = DoPreliminaryOperations(filename, codelist) 494 | if not codelist: 495 | return 496 | 497 | # Initialize vars 498 | sections = [] 499 | 500 | # Do the parsing 501 | with open(filename, 'rb') as f: 502 | # Get the entrypoint 503 | f.seek(0xE0) 504 | entrypoint = int(f.read(4).hex(), 16) 505 | 506 | # Go to the text sections' loading address. The one with the same address as the entrypoint usually contains the 507 | # codehandler+gct. But other custom code might override this, so as an additional check for 0x80001800 is made 508 | f.seek(0x48) 509 | for i in range(7): 510 | secmem = int(f.read(4).hex(), 16) 511 | if secmem == entrypoint or secmem == 0x80001800: 512 | sections.append(i) 513 | 514 | # If there are no matches, it means there's no codes here for us to find 515 | if not sections: 516 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Empty DOL', 'No GCTs were found in this file') 517 | return 518 | 519 | for section in sections: 520 | # Get the section offset and length 521 | f.seek(section * 4) 522 | sectionoffset = int(f.read(4).hex(), 16) 523 | f.seek(0x90 + section * 4) 524 | sectionend = sectionoffset + int(f.read(4).hex(), 16) 525 | 526 | # Initialize vars 527 | shouldadd = False 528 | buffer = globalstuff.gctmagic 529 | 530 | # Read the section 531 | f.seek(sectionoffset) 532 | while f.tell() < sectionend: 533 | # Read file 534 | bytez = f.read(8) 535 | 536 | # Check for the gct EOF, else add to the buffer if we found the magic 537 | if shouldadd: 538 | buffer += bytez 539 | if bytez == globalstuff.gctend: 540 | break 541 | 542 | # Found the GCT magic, start reading from here 543 | elif bytez == globalstuff.gctmagic: 544 | shouldadd = True 545 | 546 | # Skip the parsing if we didn't find anything 547 | if len(buffer) == 8: 548 | continue 549 | 550 | # Write the buffer to a temporary file, then feed it to the GCT parser 551 | with open('tmp.gct', 'wb+') as g: 552 | g.write(buffer) 553 | ParseGCT('tmp.gct', g, codelist) 554 | 555 | # Remove the file 556 | os.remove('tmp.gct') 557 | return # We're assuming there is only one GCT here. Who in their right mind would add more than one?! 558 | 559 | # This is only shown if nothing is found, as otherwise the function would have already returned 560 | QtWidgets.QMessageBox.critical(globalstuff.mainWindow, 'Empty DOL', 'No GCTs were found in this file') 561 | --------------------------------------------------------------------------------