├── version.py ├── mcedit_ui ├── build_ui.bat ├── build_ui.py ├── entity_layer_item.py ├── clickable_graphics_scene.py ├── settings.py ├── entity_search.ui ├── settings.ui ├── text_editor_dialog.py ├── entity_search_dialog.py ├── uic │ ├── ui_settings.py │ ├── ui_entity_search.py │ ├── ui_text_editor.py │ ├── ui_save_editor.py │ ├── ui_sprite_editor.py │ └── ui_main.py ├── room_view.py ├── text_editor.ui ├── save_editor.ui ├── tileset_graphics_scene.py ├── sprite_editor.ui ├── sprite_editor_dialog.py ├── entity_properties.py ├── custom_graphics_items.py ├── save_editor_dialog.py ├── layer_item.py ├── main.ui └── main_window.py ├── asm ├── assemble.bat ├── custom_symbols.txt ├── linker.ld ├── test_room_diff.txt └── test_room.asm ├── profile.bat ├── requirements.txt ├── .gitmodules ├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── paths.py ├── LICENSE.txt ├── mcedit.py └── README.md /version.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = "0.5.0" 3 | -------------------------------------------------------------------------------- /mcedit_ui/build_ui.bat: -------------------------------------------------------------------------------- 1 | py build_ui.py 2 | -------------------------------------------------------------------------------- /asm/assemble.bat: -------------------------------------------------------------------------------- 1 | py ../mclib/assemble.py 2 | -------------------------------------------------------------------------------- /asm/custom_symbols.txt: -------------------------------------------------------------------------------- 1 | 080ad3bc test_room_data 2 | -------------------------------------------------------------------------------- /profile.bat: -------------------------------------------------------------------------------- 1 | py -m cProfile -s cumtime mcedit.py > profileresults.txt 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6~=6.6.0 2 | PyYAML==6.0.1 3 | Pillow>=10.2.0 4 | psutil==5.7.0 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mclib"] 2 | path = mclib 3 | url = https://github.com/LagoLunatic/mclib.git 4 | -------------------------------------------------------------------------------- /asm/linker.ld: -------------------------------------------------------------------------------- 1 | 2 | LoadSaveFile = 0x08050624; 3 | SetCurrentSaveFile = 0x0805041C; 4 | SetGameState = 0x08056010; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | settings.txt 3 | /build 4 | /dist 5 | profileresults.txt 6 | profileresults.prof 7 | /wip 8 | /logs 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "qtForPython.uic.options": [ 3 | "-o", "${resourceDirname}${pathSeparator}uic${pathSeparator}ui_${resourceBasenameNoExtension}.py", 4 | ], 5 | } -------------------------------------------------------------------------------- /paths.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | try: 5 | from sys import _MEIPASS # @IgnoreException 6 | ROOT_PATH = _MEIPASS 7 | except ImportError: 8 | ROOT_PATH = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | MCLIB_PATH = os.path.join(ROOT_PATH, "mclib") 11 | DATA_PATH = os.path.join(MCLIB_PATH, "data") 12 | ASM_PATH = os.path.join(ROOT_PATH, "asm") 13 | -------------------------------------------------------------------------------- /mcedit_ui/build_ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from subprocess import call 4 | import glob 5 | import os 6 | 7 | ui_dir = os.path.dirname(__file__) 8 | output_dir = os.path.join(ui_dir, "uic") 9 | os.makedirs(output_dir, exist_ok=True) 10 | 11 | for input_path in glob.glob(glob.escape(ui_dir) + "/*.ui"): 12 | base_name = os.path.splitext(os.path.basename(input_path))[0] 13 | 14 | command = [ 15 | "pyside6-uic", 16 | input_path, 17 | "-o", os.path.join(output_dir, "ui_%s.py" % base_name) 18 | ] 19 | result = call(command) 20 | -------------------------------------------------------------------------------- /asm/test_room_diff.txt: -------------------------------------------------------------------------------- 1 | 134927232: 2 | - 16 3 | - 181 4 | - 12 5 | - 76 6 | - 32 7 | - 120 8 | - 163 9 | - 247 10 | - 77 11 | - 249 12 | - 32 13 | - 120 14 | - 163 15 | - 247 16 | - 70 17 | - 248 18 | - 2 19 | - 32 20 | - 168 21 | - 247 22 | - 61 23 | - 254 24 | - 8 25 | - 72 26 | - 16 27 | - 33 28 | - 9 29 | - 1 30 | - 1 31 | - 128 32 | - 136 33 | - 48 34 | - 98 35 | - 120 36 | - 2 37 | - 112 38 | - 162 39 | - 120 40 | - 66 41 | - 112 42 | - 162 43 | - 136 44 | - 130 45 | - 128 46 | - 226 47 | - 136 48 | - 194 49 | - 128 50 | - 16 51 | - 189 52 | - 0 53 | - 0 54 | - 188 55 | - 211 56 | - 10 57 | - 8 58 | - 64 59 | - 42 60 | - 0 61 | - 2 62 | - 0 63 | - 1 64 | - 0 65 | - 0 66 | - 128 67 | - 0 68 | - 96 69 | - 0 70 | 134556676: 71 | - 20 72 | - 224 73 | -------------------------------------------------------------------------------- /asm/test_room.asm: -------------------------------------------------------------------------------- 1 | 2 | .org 0x080AD380 3 | push r4, r14 4 | 5 | ldr r4,=test_room_data 6 | 7 | ldrb r0, [r4] ; Save slot 8 | bl LoadSaveFile 9 | ldrb r0, [r4] ; Save slot 10 | bl SetCurrentSaveFile 11 | mov r0, 2h 12 | bl SetGameState 13 | 14 | ; Skip intro (in case this save file has not been started) 15 | ldr r0,=02002A40h 16 | mov r1, 10h 17 | lsl r1, r1, 4h 18 | strh r1, [r0] 19 | 20 | add r0, 88h 21 | ldrb r2, [r4,1h] ; Area index 22 | strb r2, [r0] 23 | ldrb r2, [r4,2h] ; Room index 24 | strb r2, [r0,1h] 25 | ldrh r2, [r4,4h] ; X pos 26 | strh r2, [r0,4h] 27 | ldrh r2, [r4,6h] ; Y pos 28 | strh r2, [r0,6h] 29 | 30 | pop r4, r15 31 | .pool 32 | .global test_room_data 33 | test_room_data: 34 | .byte 0 ; Save slot 35 | .byte 1 ; Area index 36 | .byte 0 ; Room index 37 | .byte 0 ; Padding so the following variables are halfword aligned 38 | .short 0x0080 ; X pos 39 | .short 0x0060 ; Y pos 40 | 41 | .org 0x08052C04 42 | ; Prevent Ezlo from giving a hint when you load in 43 | b 0x08052C30 44 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run MCEdit", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/mcedit.py", 12 | "console": "integratedTerminal", 13 | "justMyCode": true, 14 | "args": [] 15 | }, 16 | { 17 | "name": "Profile MCEdit", 18 | "type": "python", 19 | "request": "launch", 20 | "module": "cProfile", 21 | "console": "integratedTerminal", 22 | "justMyCode": true, 23 | "args": [ 24 | "-o", "profileresults.prof", "${workspaceFolder}/mcedit.py", 25 | ] 26 | }, 27 | { 28 | "name": "Python: Current File", 29 | "type": "python", 30 | "request": "launch", 31 | "program": "${file}", 32 | "console": "integratedTerminal", 33 | "justMyCode": true 34 | }, 35 | ] 36 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 LagoLunatic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mcedit_ui/entity_layer_item.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.custom_graphics_items import * 7 | 8 | class EntityLayerItem(QGraphicsRectItem): 9 | def __init__(self, entity_lists, renderer): 10 | super().__init__() 11 | 12 | self.entity_lists = entity_lists 13 | self.renderer = renderer 14 | 15 | self.entity_graphics_items_by_entity_list = [] 16 | for entity_list in self.entity_lists: 17 | graphics_items_for_list = [] 18 | self.entity_graphics_items_by_entity_list.append((entity_list, graphics_items_for_list)) 19 | 20 | for entity in entity_list.entities: 21 | entity_item = self.add_graphics_item_for_entity(entity) 22 | graphics_items_for_list.append(entity_item) 23 | 24 | def add_graphics_item_for_entity(self, entity): 25 | if entity.type in [3, 4, 6, 7]: 26 | entity_item = EntityImageItem(entity, "entity", self.renderer) 27 | else: 28 | entity_item = EntityRectItem(entity, "entity") 29 | 30 | entity_item.setParentItem(self) 31 | 32 | return entity_item 33 | -------------------------------------------------------------------------------- /mcedit_ui/clickable_graphics_scene.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | class ClickableGraphicsScene(QGraphicsScene): 7 | BACKGROUND_BRUSH = QBrush(QColor(240, 240, 240, 255)) 8 | 9 | clicked = Signal(int, int, object) 10 | moved = Signal(int, int, object) 11 | released = Signal(int, int, object) 12 | graphics_item_moved = Signal(object) 13 | 14 | def __init__(self): 15 | super().__init__() 16 | 17 | self.setBackgroundBrush(self.BACKGROUND_BRUSH) 18 | 19 | def mousePressEvent(self, event): 20 | x = int(event.scenePos().x()) 21 | y = int(event.scenePos().y()) 22 | self.clicked.emit(x, y, event.buttons()) 23 | 24 | super().mousePressEvent(event) 25 | 26 | def mouseMoveEvent(self, event): 27 | x = int(event.scenePos().x()) 28 | y = int(event.scenePos().y()) 29 | self.moved.emit(x, y, event.buttons()) 30 | 31 | super().mouseMoveEvent(event) 32 | 33 | def mouseReleaseEvent(self, event): 34 | x = int(event.scenePos().x()) 35 | y = int(event.scenePos().y()) 36 | self.released.emit(x, y, event.button()) 37 | 38 | super().mouseReleaseEvent(event) 39 | 40 | def itemAt(self, x, y): 41 | return super().itemAt(x, y, QTransform()) 42 | -------------------------------------------------------------------------------- /mcedit_ui/settings.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.uic.ui_settings import Ui_Settings 7 | 8 | class SettingsDialog(QDialog): 9 | def __init__(self, main_window): 10 | super().__init__(main_window, Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) 11 | self.ui = Ui_Settings() 12 | self.ui.setupUi(self) 13 | 14 | self.settings = main_window.settings 15 | 16 | for save_slot in range(3): 17 | self.ui.test_room_save_slot.addItem("Slot %d" % (save_slot+1)) 18 | 19 | self.ui.emulator_path.setText(self.settings.get("emulator_path")) 20 | self.ui.test_room_save_slot.setCurrentIndex(self.settings.get("test_room_save_slot_index", 0)) 21 | 22 | self.ui.emulator_path_browse_button.clicked.connect(self.browse_for_emulator_path) 23 | self.ui.buttonBox.clicked.connect(self.button_pressed) 24 | 25 | self.show() 26 | 27 | def browse_for_emulator_path(self): 28 | default_dir = None 29 | emu_path, selected_filter = QFileDialog.getOpenFileName() 30 | if not emu_path: 31 | return 32 | 33 | self.ui.emulator_path.setText(emu_path) 34 | 35 | def button_pressed(self, button): 36 | if self.ui.buttonBox.standardButton(button) == QDialogButtonBox.Ok: 37 | self.settings["emulator_path"] = self.ui.emulator_path.text() 38 | self.settings["test_room_save_slot_index"] = self.ui.test_room_save_slot.currentIndex() 39 | 40 | self.close() 41 | -------------------------------------------------------------------------------- /mcedit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import traceback 5 | 6 | from PySide6.QtGui import * 7 | from PySide6.QtCore import * 8 | from PySide6.QtWidgets import * 9 | 10 | from mcedit_ui.main_window import MCEditorWindow 11 | 12 | # Allow keyboard interrupts on the command line to instantly close the program. 13 | import signal 14 | signal.signal(signal.SIGINT, signal.SIG_DFL) 15 | 16 | try: 17 | from sys import _MEIPASS # @IgnoreException 18 | except ImportError: 19 | # Setting the app user model ID is necessary for Windows to display a custom taskbar icon when running from source. 20 | import ctypes 21 | app_id = "LagoLunatic.MinishCapEditor" 22 | try: 23 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) 24 | except AttributeError: 25 | # Versions of Windows before Windows 7 don't support SetCurrentProcessExplicitAppUserModelID, so just swallow the error. 26 | pass 27 | 28 | if __name__ == "__main__": 29 | qApp = QApplication(sys.argv) 30 | 31 | def show_unhandled_exception(excepttype, exception, tb): 32 | sys.__excepthook__(excepttype, exception, tb) 33 | error_message_title = "Encountered an unhandled error" 34 | stack_trace = traceback.format_exception(excepttype, exception, tb) 35 | error_message = "GCFT encountered an unhandled error.\n" 36 | error_message += "Please report this issue with a screenshot of this message.\n\n" 37 | error_message += f"{exception}\n\n" 38 | error_message += "\n".join(stack_trace) 39 | QMessageBox.critical(None, error_message_title, error_message) 40 | sys.excepthook = show_unhandled_exception 41 | 42 | window = MCEditorWindow() 43 | sys.exit(qApp.exec()) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### About 3 | 4 | MCEdit is an editor for The Legend of Zelda: The Minish Cap (US version only). 5 | It's currently a work in progress and has a number of bugs and half-finished features. 6 | 7 | Current features: 8 | * Viewing rooms as well as the entities within them 9 | * Editing the BG1 and BG2 layers of rooms 10 | * Editing entities, with a UI that lists each entity's unique parameters 11 | * Viewing the game's text 12 | * Viewing entity sprites and animations 13 | * Editing save files (partial support) 14 | * Searching for specific entity types across all rooms in the game 15 | * Testing rooms quickly by launching the game with you in the selected room 16 | 17 | Planned features: 18 | * Editing the game's text 19 | * More convenient UI for editing entities 20 | * Editing the BG3 layer of rooms 21 | * Changing the size of rooms 22 | * Adding and removing entities 23 | * Implement editing more variables in save files 24 | * Editing maps 25 | * Editing image data 26 | 27 | ### Running from source 28 | 29 | If you want to run the latest development version of MCEdit from source, follow the instructions below. 30 | 31 | Download and install git from here: https://git-scm.com/downloads 32 | Then clone this repository with git by running this in a command prompt: 33 | `git clone --recursive https://github.com/LagoLunatic/MCEdit.git` 34 | 35 | Download and install Python 3.12 from here: https://www.python.org/downloads/release/python-3121/ 36 | "Windows installer (64-bit)" is the one you want if you're on Windows. 37 | 38 | Open the MCEdit folder in a command prompt and install dependencies by running: 39 | `py -m pip install -r requirements.txt` 40 | 41 | Then launch the editor with: 42 | `py mcedit.py` 43 | -------------------------------------------------------------------------------- /mcedit_ui/entity_search.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EntitySearch 4 | 5 | 6 | 7 | 0 8 | 0 9 | 600 10 | 500 11 | 12 | 13 | 14 | Entity Search 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Type 23 | 24 | 25 | 26 | 27 | 28 | 29 | Subtype 30 | 31 | 32 | 33 | 34 | 35 | 36 | Form 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Find 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /mcedit_ui/settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 0 8 | 0 9 | 600 10 | 200 11 | 12 | 13 | 14 | Entity Search 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Test Room Emulator Path 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Browse 33 | 34 | 35 | 36 | 37 | 38 | 39 | Test Room Save Slot 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 80 48 | 16777215 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Qt::Vertical 59 | 60 | 61 | 62 | 20 63 | 40 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /mcedit_ui/text_editor_dialog.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.uic.ui_text_editor import Ui_TextEditor 7 | 8 | from mclib.message import Message, MessageGroup 9 | 10 | import re 11 | 12 | class TextEditorDialog(QDialog): 13 | def __init__(self, main_window): 14 | super().__init__(main_window) 15 | self.ui = Ui_TextEditor() 16 | self.ui.setupUi(self) 17 | 18 | self.rom = main_window.game.rom 19 | 20 | self.ui.message_group_index.activated.connect(self.message_group_changed) 21 | self.ui.message_list.currentRowChanged.connect(self.message_changed) 22 | 23 | self.message_groups = [] 24 | self.group_index = None 25 | self.message_index = None 26 | 27 | for group_index in range(0x50): 28 | self.ui.message_group_index.addItem("%02X" % group_index) 29 | 30 | message_group = MessageGroup(group_index, self.rom) 31 | self.message_groups.append(message_group) 32 | 33 | for message_index in range(message_group.num_messages): 34 | message_id = (group_index << 8) | message_index 35 | message = Message(message_id, self.rom) 36 | 37 | self.show() 38 | 39 | self.message_group_changed(0) 40 | 41 | def message_group_changed(self, group_index): 42 | self.ui.message_list.clear() 43 | 44 | self.group_index = group_index 45 | self.group_messages = [] 46 | self.group = self.message_groups[self.group_index] 47 | 48 | font_metrics = QFontMetrics(self.ui.message_list.font()) 49 | max_preview_str_width = self.ui.message_list.viewport().width() - self.ui.message_list.verticalScrollBar().width() 50 | 51 | for message_index in range(self.group.num_messages): 52 | message_id = (group_index << 8) | message_index 53 | message = Message(message_id, self.rom) 54 | self.group_messages.append(message) 55 | 56 | preview_string = "%02X " % message_index 57 | preview_string += message.string.replace("\\n\n", " ") 58 | preview_string = font_metrics.elidedText(preview_string, Qt.TextElideMode.ElideRight, max_preview_str_width) 59 | self.ui.message_list.addItem(preview_string) 60 | 61 | def message_changed(self, message_index): 62 | self.message_index = message_index 63 | 64 | message = self.group_messages[message_index] 65 | 66 | self.ui.message_text_edit.setText(message.string) 67 | self.ui.rom_location.setText("%08X" % message.string_ptr) 68 | -------------------------------------------------------------------------------- /mcedit_ui/entity_search_dialog.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.uic.ui_entity_search import Ui_EntitySearch 7 | 8 | from mclib.docs import Docs 9 | 10 | import re 11 | 12 | class EntitySearchDialog(QDialog): 13 | def __init__(self, main_window): 14 | super().__init__(main_window, Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) 15 | self.ui = Ui_EntitySearch() 16 | self.ui.setupUi(self) 17 | 18 | self.ui.find_button.clicked.connect(self.execute_search) 19 | self.ui.entity_list.currentRowChanged.connect(self.selected_entity_changed) 20 | self.ui.entity_list.clicked.connect(self.selected_entity_changed) 21 | 22 | self.show() 23 | 24 | def execute_search(self): 25 | game = self.parent().game 26 | 27 | self.ui.entity_list.clear() 28 | 29 | type = self.ui.type.text() 30 | if re.search(r"^[0-9a-f]+$", type, re.IGNORECASE): 31 | type = int(type, 16) 32 | else: 33 | type = None 34 | 35 | subtype = self.ui.subtype.text() 36 | if re.search(r"^[0-9a-f]+$", subtype, re.IGNORECASE): 37 | subtype = int(subtype, 16) 38 | else: 39 | subtype = None 40 | 41 | form = self.ui.form.text() 42 | if re.search(r"^[0-9a-f]+$", form, re.IGNORECASE): 43 | form = int(form, 16) 44 | else: 45 | form = None 46 | 47 | self.entities = [] 48 | for area in game.areas: 49 | for room in area.rooms: 50 | if room is None: 51 | continue 52 | 53 | for entity_list in room.entity_lists: 54 | for entity in entity_list.entities: 55 | if type is not None and type != entity.type: 56 | continue 57 | if subtype is not None and subtype != entity.subtype: 58 | continue 59 | if form is not None and form != entity.form: 60 | continue 61 | 62 | self.entities.append(entity) 63 | self.ui.entity_list.addItem( 64 | "In room %02X-%02X: Entity %02X-%02X-%02X %s" % ( 65 | room.area.area_index, room.room_index, 66 | entity.type, entity.subtype, entity.form, 67 | Docs.get_name_for_entity("entity", entity.type, entity.subtype, entity.form) 68 | )) 69 | 70 | def selected_entity_changed(self): 71 | index = self.ui.entity_list.currentRow() 72 | if index == -1: 73 | return 74 | entity = self.entities[index] 75 | 76 | self.parent().go_to_room_and_select_entity(entity) 77 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'settings.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QGridLayout, QLabel, QLineEdit, 20 | QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout, 21 | QWidget) 22 | 23 | class Ui_Settings(object): 24 | def setupUi(self, Settings): 25 | if not Settings.objectName(): 26 | Settings.setObjectName(u"Settings") 27 | Settings.resize(600, 200) 28 | self.verticalLayout = QVBoxLayout(Settings) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.gridLayout = QGridLayout() 31 | self.gridLayout.setObjectName(u"gridLayout") 32 | self.label = QLabel(Settings) 33 | self.label.setObjectName(u"label") 34 | 35 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 36 | 37 | self.emulator_path = QLineEdit(Settings) 38 | self.emulator_path.setObjectName(u"emulator_path") 39 | 40 | self.gridLayout.addWidget(self.emulator_path, 0, 1, 1, 1) 41 | 42 | self.emulator_path_browse_button = QPushButton(Settings) 43 | self.emulator_path_browse_button.setObjectName(u"emulator_path_browse_button") 44 | 45 | self.gridLayout.addWidget(self.emulator_path_browse_button, 0, 2, 1, 1) 46 | 47 | self.label_2 = QLabel(Settings) 48 | self.label_2.setObjectName(u"label_2") 49 | 50 | self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) 51 | 52 | self.test_room_save_slot = QComboBox(Settings) 53 | self.test_room_save_slot.setObjectName(u"test_room_save_slot") 54 | self.test_room_save_slot.setMaximumSize(QSize(80, 16777215)) 55 | 56 | self.gridLayout.addWidget(self.test_room_save_slot, 1, 1, 1, 1) 57 | 58 | 59 | self.verticalLayout.addLayout(self.gridLayout) 60 | 61 | self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 62 | 63 | self.verticalLayout.addItem(self.verticalSpacer) 64 | 65 | self.buttonBox = QDialogButtonBox(Settings) 66 | self.buttonBox.setObjectName(u"buttonBox") 67 | self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) 68 | 69 | self.verticalLayout.addWidget(self.buttonBox) 70 | 71 | 72 | self.retranslateUi(Settings) 73 | 74 | QMetaObject.connectSlotsByName(Settings) 75 | # setupUi 76 | 77 | def retranslateUi(self, Settings): 78 | Settings.setWindowTitle(QCoreApplication.translate("Settings", u"Entity Search", None)) 79 | self.label.setText(QCoreApplication.translate("Settings", u"Test Room Emulator Path", None)) 80 | self.emulator_path_browse_button.setText(QCoreApplication.translate("Settings", u"Browse", None)) 81 | self.label_2.setText(QCoreApplication.translate("Settings", u"Test Room Save Slot", None)) 82 | # retranslateUi 83 | 84 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_entity_search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'entity_search.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QLabel, 19 | QLineEdit, QListWidget, QListWidgetItem, QPushButton, 20 | QSizePolicy, QVBoxLayout, QWidget) 21 | 22 | class Ui_EntitySearch(object): 23 | def setupUi(self, EntitySearch): 24 | if not EntitySearch.objectName(): 25 | EntitySearch.setObjectName(u"EntitySearch") 26 | EntitySearch.resize(600, 500) 27 | self.verticalLayout = QVBoxLayout(EntitySearch) 28 | self.verticalLayout.setObjectName(u"verticalLayout") 29 | self.formLayout = QFormLayout() 30 | self.formLayout.setObjectName(u"formLayout") 31 | self.label = QLabel(EntitySearch) 32 | self.label.setObjectName(u"label") 33 | 34 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) 35 | 36 | self.label_2 = QLabel(EntitySearch) 37 | self.label_2.setObjectName(u"label_2") 38 | 39 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_2) 40 | 41 | self.label_3 = QLabel(EntitySearch) 42 | self.label_3.setObjectName(u"label_3") 43 | 44 | self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_3) 45 | 46 | self.type = QLineEdit(EntitySearch) 47 | self.type.setObjectName(u"type") 48 | 49 | self.formLayout.setWidget(0, QFormLayout.FieldRole, self.type) 50 | 51 | self.subtype = QLineEdit(EntitySearch) 52 | self.subtype.setObjectName(u"subtype") 53 | 54 | self.formLayout.setWidget(1, QFormLayout.FieldRole, self.subtype) 55 | 56 | self.form = QLineEdit(EntitySearch) 57 | self.form.setObjectName(u"form") 58 | 59 | self.formLayout.setWidget(2, QFormLayout.FieldRole, self.form) 60 | 61 | 62 | self.verticalLayout.addLayout(self.formLayout) 63 | 64 | self.find_button = QPushButton(EntitySearch) 65 | self.find_button.setObjectName(u"find_button") 66 | 67 | self.verticalLayout.addWidget(self.find_button) 68 | 69 | self.entity_list = QListWidget(EntitySearch) 70 | self.entity_list.setObjectName(u"entity_list") 71 | 72 | self.verticalLayout.addWidget(self.entity_list) 73 | 74 | 75 | self.retranslateUi(EntitySearch) 76 | 77 | QMetaObject.connectSlotsByName(EntitySearch) 78 | # setupUi 79 | 80 | def retranslateUi(self, EntitySearch): 81 | EntitySearch.setWindowTitle(QCoreApplication.translate("EntitySearch", u"Entity Search", None)) 82 | self.label.setText(QCoreApplication.translate("EntitySearch", u"Type", None)) 83 | self.label_2.setText(QCoreApplication.translate("EntitySearch", u"Subtype", None)) 84 | self.label_3.setText(QCoreApplication.translate("EntitySearch", u"Form", None)) 85 | self.find_button.setText(QCoreApplication.translate("EntitySearch", u"Find", None)) 86 | # retranslateUi 87 | 88 | -------------------------------------------------------------------------------- /mcedit_ui/room_view.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | class RoomView(QGraphicsView): 7 | ZOOM_SCALES = [ 8 | 0.25, 9 | 0.5, 10 | 0.75, 11 | 1.0, 12 | 1.5, 13 | 2.0, 14 | 2.5, 15 | 3.0, 16 | 4.0, 17 | ] 18 | 19 | def __init__(self, parent): 20 | super().__init__(parent) 21 | 22 | self.is_panning = False 23 | self.curr_zoom_index = self.ZOOM_SCALES.index(1.0) 24 | 25 | self.setMouseTracking(True) 26 | self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) 27 | 28 | def updateSceneRect(self, scene_rect, transform=QTransform()): 29 | max_size = self.maximumViewportSize() 30 | margin_w = max_size.width() * 0.9 31 | margin_h = max_size.height() * 0.9 32 | 33 | view_rect = transform.mapRect(scene_rect) 34 | view_rect.adjust(-margin_w, -margin_h, margin_w, margin_h) 35 | inverted_transform, _ = transform.inverted() 36 | expanded_scene_rect = inverted_transform.mapRect(view_rect) 37 | 38 | self.scene().setSceneRect(expanded_scene_rect) 39 | self.setSceneRect(expanded_scene_rect) 40 | 41 | def reset_zoom(self): 42 | self.resetTransform() 43 | self.curr_zoom_index = self.ZOOM_SCALES.index(1.0) 44 | 45 | def keyPressEvent(self, event): 46 | if event.key() == Qt.Key_Space and not event.isAutoRepeat(): 47 | self.set_panning(True) 48 | 49 | def keyReleaseEvent(self, event): 50 | if event.key() == Qt.Key_Space and not event.isAutoRepeat(): 51 | self.set_panning(False) 52 | 53 | def resizeEvent(self, event): 54 | self.window().center_room_view() 55 | 56 | def set_panning(self, is_panning): 57 | if is_panning == self.is_panning: 58 | return 59 | 60 | self.is_panning = is_panning 61 | 62 | self.setInteractive(not is_panning) 63 | 64 | if is_panning: 65 | self.orig_mouse_pos = QCursor.pos() 66 | QApplication.setOverrideCursor(QCursor(Qt.ClosedHandCursor)) 67 | else: 68 | QApplication.restoreOverrideCursor() 69 | 70 | def mouseMoveEvent(self, event): 71 | if self.is_panning: 72 | diff = event.globalPos() - self.orig_mouse_pos 73 | 74 | horizontalValue = self.horizontalScrollBar().value() - diff.x() 75 | verticalValue = self.verticalScrollBar().value() - diff.y() 76 | 77 | self.horizontalScrollBar().setValue(horizontalValue) 78 | self.verticalScrollBar().setValue(verticalValue) 79 | 80 | self.orig_mouse_pos = event.globalPos() 81 | else: 82 | super().mouseMoveEvent(event) 83 | self.orig_mouse_pos = event.globalPos() 84 | 85 | def mousePressEvent(self, event): 86 | if event.button() == Qt.MiddleButton: 87 | self.set_panning(True) 88 | return 89 | 90 | super().mousePressEvent(event) 91 | 92 | def mouseReleaseEvent(self, event): 93 | if event.button() == Qt.MiddleButton: 94 | self.set_panning(False) 95 | return 96 | 97 | super().mouseReleaseEvent(event) 98 | 99 | def wheelEvent(self, event): 100 | if not QApplication.keyboardModifiers() & Qt.ControlModifier: 101 | super().wheelEvent(event) 102 | return 103 | 104 | y_change = event.angleDelta().y() 105 | 106 | old_zoom_scale = self.ZOOM_SCALES[self.curr_zoom_index] 107 | if y_change > 0: 108 | if self.curr_zoom_index == len(self.ZOOM_SCALES) - 1: 109 | return 110 | self.curr_zoom_index += 1 111 | elif y_change < 0: 112 | if self.curr_zoom_index == 0: 113 | return 114 | self.curr_zoom_index -= 1 115 | else: 116 | return 117 | 118 | curr_zoom_scale = self.ZOOM_SCALES[self.curr_zoom_index] 119 | scale_mult = curr_zoom_scale / old_zoom_scale 120 | 121 | self.scale(scale_mult, scale_mult) 122 | 123 | self.updateSceneRect(self.scene().sceneRect()) 124 | -------------------------------------------------------------------------------- /mcedit_ui/text_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TextEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1024 10 | 720 11 | 12 | 13 | 14 | Text Editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Qt::Horizontal 26 | 27 | 28 | 29 | 40 30 | 20 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Qt::ScrollBarAlwaysOn 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ROM Location 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 80 62 | 16777215 63 | 64 | 65 | 66 | true 67 | 68 | 69 | 70 | 71 | 72 | 73 | Qt::Horizontal 74 | 75 | 76 | 77 | 40 78 | 20 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | Qt::Horizontal 96 | 97 | 98 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | buttonBox 108 | accepted() 109 | TextEditor 110 | accept() 111 | 112 | 113 | 248 114 | 254 115 | 116 | 117 | 157 118 | 274 119 | 120 | 121 | 122 | 123 | buttonBox 124 | rejected() 125 | TextEditor 126 | reject() 127 | 128 | 129 | 316 130 | 260 131 | 132 | 133 | 286 134 | 274 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /mcedit_ui/save_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SaveEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1024 10 | 720 11 | 12 | 13 | 14 | Save Editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Import Raw Save 23 | 24 | 25 | 26 | 27 | 28 | 29 | Import VBA/mGBA Save 30 | 31 | 32 | 33 | 34 | 35 | 36 | Import GameShark Save 37 | 38 | 39 | 40 | 41 | 42 | 43 | Export Raw Save 44 | 45 | 46 | 47 | 48 | 49 | 50 | Export VBA/mGBA Save 51 | 52 | 53 | 54 | 55 | 56 | 57 | Export GameShark Save 58 | 59 | 60 | 61 | 62 | 63 | 64 | Qt::Horizontal 65 | 66 | 67 | 68 | 40 69 | 20 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 0 84 | 85 | 86 | 87 | 88 | Save Slot 1 89 | 90 | 91 | 92 | 93 | Save Slot 2 94 | 95 | 96 | 97 | 98 | Save Slot 3 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Qt::Horizontal 107 | 108 | 109 | 110 | 40 111 | 20 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | 123 | 124 | 125 | 126 | 0 127 | 0 128 | 1000 129 | 628 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_text_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'text_editor.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, 20 | QListWidget, QListWidgetItem, QSizePolicy, QSpacerItem, 21 | QTextEdit, QVBoxLayout, QWidget) 22 | 23 | class Ui_TextEditor(object): 24 | def setupUi(self, TextEditor): 25 | if not TextEditor.objectName(): 26 | TextEditor.setObjectName(u"TextEditor") 27 | TextEditor.resize(1024, 720) 28 | self.verticalLayout = QVBoxLayout(TextEditor) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.horizontalLayout_2 = QHBoxLayout() 31 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 32 | self.message_group_index = QComboBox(TextEditor) 33 | self.message_group_index.setObjectName(u"message_group_index") 34 | 35 | self.horizontalLayout_2.addWidget(self.message_group_index) 36 | 37 | self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 38 | 39 | self.horizontalLayout_2.addItem(self.horizontalSpacer) 40 | 41 | 42 | self.verticalLayout.addLayout(self.horizontalLayout_2) 43 | 44 | self.horizontalLayout = QHBoxLayout() 45 | self.horizontalLayout.setObjectName(u"horizontalLayout") 46 | self.message_list = QListWidget(TextEditor) 47 | self.message_list.setObjectName(u"message_list") 48 | self.message_list.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 49 | 50 | self.horizontalLayout.addWidget(self.message_list) 51 | 52 | self.verticalLayout_2 = QVBoxLayout() 53 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 54 | self.horizontalLayout_3 = QHBoxLayout() 55 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") 56 | self.label = QLabel(TextEditor) 57 | self.label.setObjectName(u"label") 58 | 59 | self.horizontalLayout_3.addWidget(self.label) 60 | 61 | self.rom_location = QLineEdit(TextEditor) 62 | self.rom_location.setObjectName(u"rom_location") 63 | self.rom_location.setMaximumSize(QSize(80, 16777215)) 64 | self.rom_location.setReadOnly(True) 65 | 66 | self.horizontalLayout_3.addWidget(self.rom_location) 67 | 68 | self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 69 | 70 | self.horizontalLayout_3.addItem(self.horizontalSpacer_2) 71 | 72 | 73 | self.verticalLayout_2.addLayout(self.horizontalLayout_3) 74 | 75 | self.message_text_edit = QTextEdit(TextEditor) 76 | self.message_text_edit.setObjectName(u"message_text_edit") 77 | 78 | self.verticalLayout_2.addWidget(self.message_text_edit) 79 | 80 | 81 | self.horizontalLayout.addLayout(self.verticalLayout_2) 82 | 83 | 84 | self.verticalLayout.addLayout(self.horizontalLayout) 85 | 86 | self.buttonBox = QDialogButtonBox(TextEditor) 87 | self.buttonBox.setObjectName(u"buttonBox") 88 | self.buttonBox.setOrientation(Qt.Horizontal) 89 | self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) 90 | 91 | self.verticalLayout.addWidget(self.buttonBox) 92 | 93 | 94 | self.retranslateUi(TextEditor) 95 | self.buttonBox.accepted.connect(TextEditor.accept) 96 | self.buttonBox.rejected.connect(TextEditor.reject) 97 | 98 | QMetaObject.connectSlotsByName(TextEditor) 99 | # setupUi 100 | 101 | def retranslateUi(self, TextEditor): 102 | TextEditor.setWindowTitle(QCoreApplication.translate("TextEditor", u"Text Editor", None)) 103 | self.label.setText(QCoreApplication.translate("TextEditor", u"ROM Location", None)) 104 | # retranslateUi 105 | 106 | -------------------------------------------------------------------------------- /mcedit_ui/tileset_graphics_scene.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.clickable_graphics_scene import ClickableGraphicsScene 7 | from mcedit_ui.custom_graphics_items import GraphicsImageItem 8 | 9 | class TilesetGraphicsScene(ClickableGraphicsScene): 10 | def __init__(self, main_window): 11 | super().__init__() 12 | 13 | self.main_window = main_window 14 | 15 | self.selection_origin = None 16 | 17 | self.clicked.connect(self.mouse_clicked_on_tileset) 18 | self.moved.connect(self.mouse_moved_on_tileset) 19 | self.released.connect(self.mouse_released_on_tileset) 20 | 21 | self.tileset_graphics_item = GraphicsImageItem() 22 | self.addItem(self.tileset_graphics_item) 23 | 24 | self.selection_rect = QGraphicsRectItem() 25 | self.selection_rect.setPen(QPen(QColor(255, 0, 0, 255))) 26 | self.addItem(self.selection_rect) 27 | 28 | self.update_tileset_image(None) 29 | 30 | self.select_tile_by_index(0) 31 | self.update_selection_rect() 32 | 33 | def mouse_clicked_on_tileset(self, x, y, button): 34 | if x < 0 or y < 0 or x >= self.tileset_width or y >= self.tileset_height: 35 | return 36 | if button != Qt.LeftButton: 37 | return 38 | 39 | self.selection_origin = QPoint(x//0x10, y//0x10) 40 | self.update_selection_on_tileset(x, y) 41 | 42 | def mouse_moved_on_tileset(self, x, y, button): 43 | if button != Qt.LeftButton: 44 | return 45 | if self.selection_origin is None: 46 | return 47 | 48 | self.update_selection_on_tileset(x, y) 49 | 50 | def mouse_released_on_tileset(self, x, y, button): 51 | if button != Qt.LeftButton: 52 | return 53 | if self.selection_origin is None: 54 | return 55 | 56 | self.update_selection_on_tileset(x, y) 57 | self.stop_selecting_on_tileset() 58 | 59 | def update_selection_on_tileset(self, mouse_x, mouse_y): 60 | mouse_x = max(mouse_x, 0) 61 | mouse_y = max(mouse_y, 0) 62 | mouse_x = min(mouse_x, self.tileset_width-1) 63 | mouse_y = min(mouse_y, self.tileset_height-1) 64 | mouse_x = mouse_x//0x10 65 | mouse_y = mouse_y//0x10 66 | 67 | self.selection_x = min(mouse_x, self.selection_origin.x()) 68 | self.selection_y = min(mouse_y, self.selection_origin.y()) 69 | selection_right_x = max(mouse_x, self.selection_origin.x()) 70 | selection_bottom_y = max(mouse_y, self.selection_origin.y()) 71 | self.selection_w = selection_right_x - self.selection_x + 1 72 | self.selection_h = selection_bottom_y - self.selection_y + 1 73 | 74 | self.update_selection_rect() 75 | 76 | self.selected_tile_indexes = [] 77 | for y in range(self.selection_y, self.selection_y+self.selection_h): 78 | for x in range(self.selection_x, self.selection_x+self.selection_w): 79 | tile_index = x + y*0x10 80 | self.selected_tile_indexes.append(tile_index) 81 | 82 | def stop_selecting_on_tileset(self): 83 | self.selection_origin = None 84 | 85 | self.main_window.update_selected_tiles_cursor_image() 86 | 87 | def select_tile_by_index(self, tile_index_16x16): 88 | self.selected_tile_indexes = [tile_index_16x16] 89 | self.selection_x = tile_index_16x16 % 0x10 90 | self.selection_y = tile_index_16x16 // 0x10 91 | self.selection_w = 1 92 | self.selection_h = 1 93 | self.update_selection_rect() 94 | 95 | self.main_window.update_selected_tiles_cursor_image() 96 | 97 | def select_tile_by_pos(self, x, y): 98 | tile_index_16x16 = (y//0x10)*0x10 + (x//0x10) 99 | self.select_tile_by_index(tile_index_16x16) 100 | 101 | def update_tileset_image(self, new_image): 102 | self.tileset_graphics_item.set_image(new_image) 103 | 104 | if new_image is None: 105 | self.tileset_width = 16 106 | self.tileset_height = 16 107 | else: 108 | self.tileset_width = new_image.width 109 | self.tileset_height = new_image.height 110 | 111 | self.select_tile_by_index(0) 112 | 113 | def update_selection_rect(self): 114 | self.selection_rect.setPos(self.selection_x*0x10, self.selection_y*0x10) 115 | self.selection_rect.setRect(0, 0, self.selection_w*0x10, self.selection_h*0x10) 116 | 117 | if len(self.views()) > 0: 118 | self.views()[0].ensureVisible(self.selection_rect, xmargin=0, ymargin=0) 119 | 120 | def get_selection_as_pixmap(self): 121 | return self.tileset_graphics_item.pixmap().copy( 122 | self.selection_x*0x10, self.selection_y*0x10, 123 | self.selection_w*0x10, self.selection_h*0x10 124 | ) 125 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_save_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'save_editor.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QComboBox, QDialog, QFormLayout, 19 | QGridLayout, QHBoxLayout, QPushButton, QScrollArea, 20 | QSizePolicy, QSpacerItem, QVBoxLayout, QWidget) 21 | 22 | class Ui_SaveEditor(object): 23 | def setupUi(self, SaveEditor): 24 | if not SaveEditor.objectName(): 25 | SaveEditor.setObjectName(u"SaveEditor") 26 | SaveEditor.resize(1024, 720) 27 | self.verticalLayout = QVBoxLayout(SaveEditor) 28 | self.verticalLayout.setObjectName(u"verticalLayout") 29 | self.horizontalLayout = QHBoxLayout() 30 | self.horizontalLayout.setObjectName(u"horizontalLayout") 31 | self.import_raw_save = QPushButton(SaveEditor) 32 | self.import_raw_save.setObjectName(u"import_raw_save") 33 | 34 | self.horizontalLayout.addWidget(self.import_raw_save) 35 | 36 | self.import_vba_mgba_save = QPushButton(SaveEditor) 37 | self.import_vba_mgba_save.setObjectName(u"import_vba_mgba_save") 38 | 39 | self.horizontalLayout.addWidget(self.import_vba_mgba_save) 40 | 41 | self.import_gameshark_save = QPushButton(SaveEditor) 42 | self.import_gameshark_save.setObjectName(u"import_gameshark_save") 43 | 44 | self.horizontalLayout.addWidget(self.import_gameshark_save) 45 | 46 | self.export_raw_save = QPushButton(SaveEditor) 47 | self.export_raw_save.setObjectName(u"export_raw_save") 48 | 49 | self.horizontalLayout.addWidget(self.export_raw_save) 50 | 51 | self.export_vba_mgba_save = QPushButton(SaveEditor) 52 | self.export_vba_mgba_save.setObjectName(u"export_vba_mgba_save") 53 | 54 | self.horizontalLayout.addWidget(self.export_vba_mgba_save) 55 | 56 | self.export_gameshark_save = QPushButton(SaveEditor) 57 | self.export_gameshark_save.setObjectName(u"export_gameshark_save") 58 | 59 | self.horizontalLayout.addWidget(self.export_gameshark_save) 60 | 61 | self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 62 | 63 | self.horizontalLayout.addItem(self.horizontalSpacer) 64 | 65 | 66 | self.verticalLayout.addLayout(self.horizontalLayout) 67 | 68 | self.horizontalLayout_2 = QHBoxLayout() 69 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 70 | self.selected_slot_index = QComboBox(SaveEditor) 71 | self.selected_slot_index.addItem("") 72 | self.selected_slot_index.addItem("") 73 | self.selected_slot_index.addItem("") 74 | self.selected_slot_index.setObjectName(u"selected_slot_index") 75 | self.selected_slot_index.setMinimumSize(QSize(100, 0)) 76 | 77 | self.horizontalLayout_2.addWidget(self.selected_slot_index) 78 | 79 | self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 80 | 81 | self.horizontalLayout_2.addItem(self.horizontalSpacer_2) 82 | 83 | 84 | self.verticalLayout.addLayout(self.horizontalLayout_2) 85 | 86 | self.scrollArea = QScrollArea(SaveEditor) 87 | self.scrollArea.setObjectName(u"scrollArea") 88 | self.scrollArea.setWidgetResizable(True) 89 | self.scrollAreaWidgetContents = QWidget() 90 | self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") 91 | self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1000, 628)) 92 | self.horizontalLayout_3 = QHBoxLayout(self.scrollAreaWidgetContents) 93 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") 94 | self.owned_figurines_layout = QVBoxLayout() 95 | self.owned_figurines_layout.setObjectName(u"owned_figurines_layout") 96 | 97 | self.horizontalLayout_3.addLayout(self.owned_figurines_layout) 98 | 99 | self.owned_items_layout = QFormLayout() 100 | self.owned_items_layout.setObjectName(u"owned_items_layout") 101 | 102 | self.horizontalLayout_3.addLayout(self.owned_items_layout) 103 | 104 | self.flags_layout = QGridLayout() 105 | self.flags_layout.setObjectName(u"flags_layout") 106 | 107 | self.horizontalLayout_3.addLayout(self.flags_layout) 108 | 109 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 110 | 111 | self.verticalLayout.addWidget(self.scrollArea) 112 | 113 | 114 | self.retranslateUi(SaveEditor) 115 | 116 | QMetaObject.connectSlotsByName(SaveEditor) 117 | # setupUi 118 | 119 | def retranslateUi(self, SaveEditor): 120 | SaveEditor.setWindowTitle(QCoreApplication.translate("SaveEditor", u"Save Editor", None)) 121 | self.import_raw_save.setText(QCoreApplication.translate("SaveEditor", u"Import Raw Save", None)) 122 | self.import_vba_mgba_save.setText(QCoreApplication.translate("SaveEditor", u"Import VBA/mGBA Save", None)) 123 | self.import_gameshark_save.setText(QCoreApplication.translate("SaveEditor", u"Import GameShark Save", None)) 124 | self.export_raw_save.setText(QCoreApplication.translate("SaveEditor", u"Export Raw Save", None)) 125 | self.export_vba_mgba_save.setText(QCoreApplication.translate("SaveEditor", u"Export VBA/mGBA Save", None)) 126 | self.export_gameshark_save.setText(QCoreApplication.translate("SaveEditor", u"Export GameShark Save", None)) 127 | self.selected_slot_index.setItemText(0, QCoreApplication.translate("SaveEditor", u"Save Slot 1", None)) 128 | self.selected_slot_index.setItemText(1, QCoreApplication.translate("SaveEditor", u"Save Slot 2", None)) 129 | self.selected_slot_index.setItemText(2, QCoreApplication.translate("SaveEditor", u"Save Slot 3", None)) 130 | 131 | # retranslateUi 132 | 133 | -------------------------------------------------------------------------------- /mcedit_ui/sprite_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SpriteEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 832 10 | 478 11 | 12 | 13 | 14 | Text Editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0 23 | 24 | 25 | 26 | Enemies 27 | 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Objects 40 | 41 | 42 | 43 | 0 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | NPCs 53 | 54 | 55 | 56 | 0 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Player 66 | 67 | 68 | 69 | 0 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Type 4s 79 | 80 | 81 | 82 | 0 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Player Items 92 | 93 | 94 | 95 | 0 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Form 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Animation 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Frame 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Qt::Horizontal 159 | 160 | 161 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | buttonBox 171 | accepted() 172 | SpriteEditor 173 | accept() 174 | 175 | 176 | 248 177 | 254 178 | 179 | 180 | 157 181 | 274 182 | 183 | 184 | 185 | 186 | buttonBox 187 | rejected() 188 | SpriteEditor 189 | reject() 190 | 191 | 192 | 316 193 | 260 194 | 195 | 196 | 286 197 | 274 198 | 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /mcedit_ui/sprite_editor_dialog.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | import traceback 7 | 8 | from mcedit_ui.uic.ui_sprite_editor import Ui_SpriteEditor 9 | 10 | from mcedit_ui.clickable_graphics_scene import * 11 | from mcedit_ui.custom_graphics_items import * 12 | 13 | from mclib.sprite_loading import SpriteLoadingData 14 | from mclib.sprite import Sprite 15 | from mclib.docs import Docs 16 | 17 | class SpriteEditorDialog(QDialog): 18 | def __init__(self, main_window): 19 | super().__init__(main_window) 20 | self.ui = Ui_SpriteEditor() 21 | self.ui.setupUi(self) 22 | 23 | self.game = main_window.game 24 | self.rom = self.game.rom 25 | self.renderer = main_window.renderer 26 | 27 | self.type = -1 28 | self.subtype = -1 29 | self.form = -1 30 | 31 | self.sprite_graphics_scene = ClickableGraphicsScene() 32 | self.ui.sprite_graphics_view.setScene(self.sprite_graphics_scene) 33 | 34 | self.ui.enemy_list.currentRowChanged.connect(self.enemy_changed) 35 | self.ui.object_list.currentRowChanged.connect(self.object_changed) 36 | self.ui.npc_list.currentRowChanged.connect(self.npc_changed) 37 | self.ui.player_list.currentRowChanged.connect(self.player_changed) 38 | self.ui.type_4s_list.currentRowChanged.connect(self.type_4_changed) 39 | self.ui.player_items_list.currentRowChanged.connect(self.player_item_changed) 40 | 41 | self.ui.form_index.activated.connect(self.form_changed) 42 | self.ui.anim_index.activated.connect(self.anim_changed) 43 | self.ui.frame_index.activated.connect(self.frame_changed) 44 | 45 | self.type_to_list_widget = { 46 | 3: self.ui.enemy_list, 47 | 6: self.ui.object_list, 48 | 7: self.ui.npc_list, 49 | 1: self.ui.player_list, 50 | 4: self.ui.type_4s_list, 51 | 8: self.ui.player_items_list, 52 | } 53 | self.type_and_row_index_to_subtype = {} 54 | for type, list_widget in self.type_to_list_widget.items(): 55 | self.type_and_row_index_to_subtype[type] = [] 56 | 57 | subtypes = Docs.get_all_subtypes_for_type("entity", type) 58 | for subtype in subtypes: 59 | self.type_and_row_index_to_subtype[type].append(subtype) 60 | 61 | form = -1 # TODO kinda hacky to do it this way 62 | text = "%02X-%02X %s" % ( 63 | type, subtype, 64 | Docs.get_name_for_entity("entity", type, subtype, form) 65 | ) 66 | list_widget.addItem(text) 67 | 68 | self.show() 69 | 70 | def enemy_changed(self, row_index): 71 | type = 3 72 | subtype = self.type_and_row_index_to_subtype[type][row_index] 73 | form = 0 74 | self.sprite_changed(type, subtype, form) 75 | 76 | def object_changed(self, row_index): 77 | type = 6 78 | subtype = self.type_and_row_index_to_subtype[type][row_index] 79 | form = 0 80 | self.sprite_changed(type, subtype, form) 81 | 82 | def npc_changed(self, row_index): 83 | type = 7 84 | subtype = self.type_and_row_index_to_subtype[type][row_index] 85 | form = 0 86 | self.sprite_changed(type, subtype, form) 87 | 88 | def player_changed(self, row_index): 89 | type = 1 90 | subtype = self.type_and_row_index_to_subtype[type][row_index] 91 | form = 0 92 | self.sprite_changed(type, subtype, form) 93 | 94 | def type_4_changed(self, row_index): 95 | type = 4 96 | subtype = self.type_and_row_index_to_subtype[type][row_index] 97 | form = 0 98 | self.sprite_changed(type, subtype, form) 99 | 100 | def player_item_changed(self, row_index): 101 | type = 8 102 | subtype = self.type_and_row_index_to_subtype[type][row_index] 103 | form = 0 104 | self.sprite_changed(type, subtype, form) 105 | 106 | def form_changed(self, form): 107 | self.sprite_changed(self.type, self.subtype, form) 108 | 109 | def sprite_changed(self, type, subtype, form): 110 | #print(type, subtype, form) 111 | 112 | if self.type == type and self.subtype == subtype: 113 | only_form_changed = True 114 | else: 115 | only_form_changed = False 116 | 117 | self.type = type 118 | self.subtype = subtype 119 | self.form = form 120 | 121 | self.sprite_graphics_scene.clear() 122 | self.ui.anim_index.clear() 123 | self.ui.frame_index.clear() 124 | if not only_form_changed: 125 | self.ui.form_index.clear() 126 | forms = Docs.get_all_forms_for_subtype("entity", self.type, self.subtype) 127 | for other_form in forms: 128 | form_name = Docs.get_name_for_entity_form("entity", self.type, self.subtype, other_form) 129 | self.ui.form_index.addItem("%02X %s" % (other_form, form_name)) 130 | 131 | self.loading_data = SpriteLoadingData(type, subtype, form, self.rom) 132 | if self.loading_data.has_no_sprite: 133 | self.sprite = None 134 | return 135 | 136 | self.sprite = Sprite(self.loading_data.sprite_index, self.rom) 137 | 138 | # TODO: how to determine number of anims and frames? 139 | num_frames = 0xFF 140 | num_anims = 0xFF 141 | 142 | for i in range(num_frames): 143 | self.ui.frame_index.addItem("%02X" % i) 144 | 145 | if self.sprite.animation_list_ptr == 0: 146 | self.frame_changed(0) 147 | else: 148 | for i in range(num_anims): 149 | self.ui.anim_index.addItem("%02X" % i) 150 | 151 | self.anim_changed(0) 152 | 153 | def anim_changed(self, anim_index): 154 | self.ui.anim_index.setCurrentIndex(anim_index) 155 | 156 | try: 157 | anim = self.sprite.get_animation(anim_index) 158 | except Exception as e: 159 | stack_trace = traceback.format_exc() 160 | error_message = "Error getting animation:\n" + str(e) + "\n\n" + stack_trace 161 | QMessageBox.warning(self, 162 | "Error getting animation", 163 | error_message 164 | ) 165 | 166 | keyframe = anim.keyframes[0] 167 | # TODO: how to handle the keyframe's h and v flip? 168 | frame_index = keyframe.frame_index 169 | 170 | self.frame_changed(frame_index) 171 | 172 | def frame_changed(self, frame_index): 173 | self.ui.frame_index.setCurrentIndex(frame_index) 174 | 175 | self.sprite_graphics_scene.clear() 176 | 177 | try: 178 | offsets = (0, 0) 179 | extra_frame_indexes = [] 180 | frame_image, x_off, y_off = self.renderer.render_entity_frame(self.loading_data, frame_index, offsets, extra_frame_indexes) 181 | except Exception as e: 182 | stack_trace = traceback.format_exc() 183 | error_message = "Error rendering frame:\n" + str(e) + "\n\n" + stack_trace 184 | QMessageBox.warning(self, 185 | "Error rendering frame", 186 | error_message 187 | ) 188 | 189 | if frame_image == None: 190 | return 191 | 192 | item = GraphicsImageItem(frame_image, x_off, y_off, draw_border=False) 193 | item.setPos(x_off, y_off) 194 | self.sprite_graphics_scene.addItem(item) 195 | -------------------------------------------------------------------------------- /mcedit_ui/entity_properties.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mclib.docs import Docs 7 | from mclib.entity import ParamEntity 8 | 9 | from collections import OrderedDict 10 | import string 11 | 12 | # TODO: tree widget isn't that great here after all... gotta make a fully custom thing so that comboboxes can be scrolled through easier 13 | 14 | class EntityProperties(QWidget): 15 | def __init__(self, parent): 16 | super().__init__(parent) 17 | 18 | v_layout = QVBoxLayout(self) 19 | 20 | self.entity_label = QLabel(self) 21 | self.entity_label.setText("(No entity selected.)") 22 | v_layout.addWidget(self.entity_label) 23 | 24 | self.properties_tree = QTreeView(self) 25 | delegate = CustomItemDelegate() 26 | self.properties_tree.setItemDelegate(delegate) 27 | self.entity_model = EntityModel() 28 | self.properties_tree.setModel(self.entity_model) 29 | self.properties_tree.hide() 30 | v_layout.addWidget(self.properties_tree) 31 | 32 | # Make it so when the properties tree is hidden the text at the top still stays at the top. 33 | size_policy = self.properties_tree.sizePolicy() 34 | size_policy.setRetainSizeWhenHidden(True) 35 | self.properties_tree.setSizePolicy(size_policy) 36 | 37 | def select_entity_graphics_item(self, entity_graphics_item): 38 | if entity_graphics_item is None: 39 | self.entity_label.setText("(No entity selected.)") 40 | self.entity_model.set_entity(None) 41 | self.properties_tree.hide() 42 | return 43 | 44 | self.properties_tree.show() 45 | 46 | self.entity_model.set_entity(entity_graphics_item) 47 | 48 | self.entity_model.layoutChanged.emit() 49 | 50 | entity_class = entity_graphics_item.entity_class 51 | if entity_class == "entity": 52 | self.entity_label.setText("Entity properties:") 53 | elif entity_class == "tile_entity": 54 | self.entity_label.setText("Tile entity properties:") 55 | elif entity_class == "exit": 56 | self.entity_label.setText("Exit properties:") 57 | elif entity_class == "exit_region": 58 | self.entity_label.setText("Exit region properties:") 59 | else: 60 | raise Exception("Unknown entity class: %s" % entity_class) 61 | 62 | class CustomItemDelegate(QItemDelegate): 63 | def createEditor(self, parent, option, index): 64 | model = index.model() 65 | entity = model.entity 66 | prop = model.get_property_by_row(index.row()) 67 | 68 | if prop.attribute_name in ["type", "subtype", "form", "item_id"]: 69 | editor = QComboBox(parent) 70 | 71 | num_possible_values = 2**prop.num_bits 72 | for i in range(num_possible_values): 73 | option_name = Docs.prettify_prop_value(prop, i, entity) 74 | editor.addItem(option_name) 75 | else: 76 | editor = QLineEdit(parent) 77 | return editor 78 | 79 | def setEditorData(self, editor, index): 80 | prop = index.model().get_property_by_row(index.row()) 81 | if prop.attribute_name in ["type", "subtype", "form", "item_id"]: 82 | value_index = editor.findText(index.data()) 83 | editor.setCurrentIndex(value_index) 84 | else: 85 | editor.setText(index.data()) 86 | 87 | def setModelData(self, editor, model, index): 88 | prop = index.model().get_property_by_row(index.row()) 89 | if prop.attribute_name in ["type", "subtype", "form", "item_id"]: 90 | value = editor.currentIndex() 91 | else: 92 | value = editor.text() 93 | if not all(char in string.hexdigits for char in value): 94 | return 95 | value = int(value, 16) 96 | 97 | max_val = (2 << prop.num_bits) - 1 98 | if value > max_val: 99 | value = max_val 100 | if value < 0: 101 | value = 0 102 | 103 | setattr(model.entity, prop.attribute_name, value) 104 | # Update the params in case the type of entity changed. 105 | # Also ensure the X and Y pos remain correct, even if the internal representation of them has changed. 106 | x, y = model.entity.x, model.entity.y 107 | model.entity.update_params() 108 | model.entity.x, model.entity.y = x, y 109 | model.entity.save() 110 | model.entity_graphics_item.update_from_entity() 111 | 112 | class EntityModel(QAbstractItemModel): 113 | def __init__(self, parent=None): 114 | super().__init__(parent) 115 | 116 | self.scene_graphics_item_moved_signal_connected = False 117 | 118 | self.set_entity(None) 119 | 120 | def set_entity(self, entity_graphics_item): 121 | if entity_graphics_item is None: 122 | self.entity_graphics_item = None 123 | self.entity = None 124 | self.entity_class = None 125 | self.properties = OrderedDict() 126 | return 127 | 128 | if not self.scene_graphics_item_moved_signal_connected: 129 | # Connect the scene's signal for updating the entity's position when it's moved. 130 | # This can't be connected in __init__ because we don't know what the scene is yet. 131 | # So instead we do it the first time an entity is selected. 132 | entity_graphics_item.scene().graphics_item_moved.connect(self.entity_moved) 133 | 134 | self.entity_graphics_item = entity_graphics_item 135 | self.entity = entity_graphics_item.entity 136 | self.entity_class = entity_graphics_item.entity_class 137 | 138 | self.properties = self.entity.properties 139 | 140 | def entity_moved(self, moved_graphics_item): 141 | if moved_graphics_item == self.entity_graphics_item: 142 | x_and_y_pos_row_indexes = [ 143 | i for i, prop in enumerate(self.properties.values()) 144 | if prop.pretty_name in ["X Pos", "Y Pos", "Tile X", "Tile Y", "Center X", "Center Y"] 145 | ] 146 | 147 | if x_and_y_pos_row_indexes: 148 | first_row_index = min(x_and_y_pos_row_indexes) 149 | last_row_index = max(x_and_y_pos_row_indexes) 150 | top_left_model_index = self.index(first_row_index, 1) 151 | bottom_right_model_index = self.index(last_row_index, 1) 152 | 153 | self.dataChanged.emit(top_left_model_index, bottom_right_model_index) 154 | 155 | def columnCount(self, parent=QModelIndex()): 156 | # Property name, property value 157 | return 2 158 | 159 | def rowCount(self, parent=QModelIndex()): 160 | if parent.isValid(): 161 | # The properties don't have any children of their own. 162 | return 0 163 | else: 164 | # The root node. Return the number of properties. 165 | if self.entity is None: 166 | return 0 167 | 168 | return len(self.properties) 169 | 170 | def headerData(self, section, orientation, role): 171 | if role == Qt.DisplayRole: 172 | if section == 0: 173 | return "Property" 174 | else: 175 | return "Value" 176 | else: 177 | return None 178 | 179 | def data(self, index, role): 180 | if not index.isValid(): 181 | return None 182 | 183 | if role == Qt.DisplayRole: 184 | if self.entity is None: 185 | return None 186 | 187 | prop = self.get_property_by_row(index.row()) 188 | 189 | if index.column() == 0: 190 | return prop.pretty_name 191 | else: 192 | value = getattr(self.entity, prop.attribute_name) 193 | return Docs.prettify_prop_value(prop, value, self.entity) 194 | else: 195 | return None 196 | 197 | def get_property_by_row(self, row): 198 | if self.entity is None: 199 | return (None, None, None) 200 | 201 | return list(self.properties.values())[row] 202 | 203 | def index(self, row, column, parent=QModelIndex()): 204 | if self.hasIndex(row, column, parent): 205 | return self.createIndex(row, column) 206 | else: 207 | return QModelIndex() 208 | 209 | def parent(self, index): 210 | return QModelIndex() 211 | 212 | def flags(self, index): 213 | prop = self.get_property_by_row(index.row()) 214 | if index.column() == 0 or prop.pretty_name == "ROM Location": 215 | return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren 216 | else: 217 | return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren | Qt.ItemIsEditable 218 | -------------------------------------------------------------------------------- /mcedit_ui/custom_graphics_items.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | import traceback 7 | 8 | class GenericEntityGraphicsItem(QGraphicsItem): # Mixin 9 | def __init__(self, *args, **kwargs): 10 | # Need to avoid calling QGraphicsItem.__init__ here so we don't get a double initialization 11 | pass 12 | 13 | def initialize_entity_item(self): 14 | self.setFlag(QGraphicsItem.ItemIsMovable) 15 | self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) 16 | self.setFlag(QGraphicsItem.ItemIsSelectable) 17 | 18 | self.setCursor(QCursor(Qt.SizeAllCursor)) 19 | 20 | self.update_from_entity() 21 | 22 | self.setZValue(self.pos().y()) 23 | 24 | def update_from_entity(self): 25 | raise NotImplementedError() 26 | 27 | def itemChange(self, change, value): 28 | if change == QGraphicsItem.ItemPositionChange and self.scene(): 29 | new_pos = value 30 | x = new_pos.x() 31 | y = new_pos.y() 32 | 33 | if not QApplication.keyboardModifiers() & Qt.ControlModifier: 34 | # Lock to 8x8 grid unless Ctrl is held down 35 | x = round(x // 8) * 8 36 | y = round(y // 8) * 8 37 | 38 | x = int(x) 39 | y = int(y) 40 | 41 | # TODO: handle negatives better 42 | self.entity.x = x 43 | self.entity.y = y 44 | self.entity.save() 45 | 46 | new_pos.setX(self.entity.x) 47 | new_pos.setY(self.entity.y) 48 | 49 | self.setZValue(self.entity.y) 50 | 51 | self.scene().graphics_item_moved.emit(self) 52 | 53 | return super().itemChange(change, new_pos) 54 | 55 | return super().itemChange(change, value) 56 | 57 | class GraphicsImageItem(QGraphicsPixmapItem): 58 | def __init__(self, pil_image=None, x_off=0, y_off=0, draw_border=True): 59 | super().__init__() 60 | 61 | self.draw_border = draw_border 62 | 63 | self.set_image(pil_image, x_off, y_off) 64 | 65 | def set_image(self, pil_image, x_off=0, y_off=0): 66 | if pil_image is None: 67 | width = 16 68 | height = 16 69 | pixmap = QPixmap(width, height) 70 | pixmap.fill(QColor(200, 0, 200, 150)) 71 | self.setPixmap(pixmap) 72 | 73 | self.setOffset(-width//2, -height//2) 74 | else: 75 | width, height = pil_image.size 76 | data = pil_image.tobytes('raw', 'BGRA') 77 | qimage = QImage(data, width, height, QImage.Format_ARGB32) 78 | pixmap = QPixmap.fromImage(qimage) 79 | self.setPixmap(pixmap) 80 | 81 | self.setOffset(x_off, y_off) 82 | 83 | def boundingRect(self): 84 | # If the sprite is smaller than 16x16, make sure the bounding rectangle is at least 16x16. 85 | 86 | orig_rect = super().boundingRect() 87 | top = orig_rect.top() 88 | bottom = orig_rect.bottom() 89 | left = orig_rect.left() 90 | right = orig_rect.right() 91 | 92 | width = bottom - top 93 | height = right - left 94 | if width < 8: 95 | left -= (8-width)//2 96 | right += (8-width)//2 97 | if height < 8: 98 | top -= (8-height)//2 99 | bottom += (8-height)//2 100 | 101 | return QRectF(left, top, right-left, bottom-top) 102 | 103 | def shape(self): 104 | # Make the whole bounding rectangle clickable, instead of just the sprite's pixels. 105 | path = QPainterPath() 106 | path.addRect(self.boundingRect()) 107 | return path 108 | 109 | def paint(self, painter, option, widget): 110 | if self.draw_border: 111 | # Draw a border around the sprite. 112 | pen = painter.pen() 113 | rect = self.boundingRect() 114 | painter.drawRect(QRect(rect.x(), rect.y(), rect.width()-pen.width(), rect.height()-pen.width())) 115 | 116 | super().paint(painter, option, widget) 117 | 118 | class EntityImageItem(GraphicsImageItem, GenericEntityGraphicsItem): 119 | def __init__(self, entity, entity_class, renderer): 120 | super().__init__() 121 | self.entity = entity 122 | self.entity_class = entity_class 123 | self.renderer = renderer 124 | self.initialize_entity_item() 125 | 126 | def update_from_entity(self): 127 | try: 128 | image, x_off, y_off = self.renderer.render_entity_pretty_frame(self.entity) 129 | except Exception as e: 130 | stack_trace = traceback.format_exc() 131 | error_message = "Error rendering entity sprite in room %02X-%02X:\n" % ( 132 | self.entity.room.area.area_index, self.entity.room.room_index 133 | ) 134 | error_message += str(e) + "\n\n" + stack_trace 135 | error_log_file_name = "./logs/entity render errors/entity render error %02X-%02X-%02X.txt" % ( 136 | self.entity.type, self.entity.subtype, self.entity.form 137 | ) 138 | with open(error_log_file_name, "w") as f: 139 | f.write(error_message) 140 | #print(error_message) 141 | 142 | image, x_off, y_off = (None, None, None) 143 | 144 | self.set_image(image, x_off, y_off) 145 | 146 | self.setPos(self.entity.x, self.entity.y) 147 | 148 | class EntityRectItem(QGraphicsRectItem, GenericEntityGraphicsItem): 149 | ENTITY_BRUSH = QBrush(QColor(200, 0, 200, 150)) 150 | TILE_ENTITY_BRUSH = QBrush(QColor(0, 0, 200, 150)) 151 | EXIT_BRUSH = QBrush(QColor(200, 200, 200, 150)) 152 | ENEMY_BRUSH = QBrush(QColor(200, 0, 0, 150)) 153 | 154 | def __init__(self, entity, entity_class): 155 | super().__init__(-8, -8, 16, 16) 156 | self.entity = entity 157 | self.entity_class = entity_class 158 | self.initialize_entity_item() 159 | 160 | def update_from_entity(self): 161 | if self.entity_class == "entity": 162 | self.init_entity() 163 | elif self.entity_class == "tile_entity": 164 | self.init_tile_entity() 165 | elif self.entity_class == "exit": 166 | self.init_exit() 167 | elif self.entity_class == "exit_region": 168 | self.init_exit_region() 169 | 170 | def init_entity(self): 171 | self.setPos(self.entity.x, self.entity.y) 172 | 173 | if self.entity.type == 3: 174 | self.setBrush(self.ENEMY_BRUSH) 175 | else: 176 | self.setBrush(self.ENTITY_BRUSH) 177 | 178 | def init_tile_entity(self): 179 | self.setPos(self.entity.x, self.entity.y) 180 | 181 | self.setRect(0, 0, 16, 16) 182 | 183 | self.setBrush(self.TILE_ENTITY_BRUSH) 184 | 185 | def init_exit(self): 186 | ext = self.entity 187 | room = self.entity.room 188 | 189 | self.setPos(ext.x, ext.y) 190 | 191 | self.setBrush(self.EXIT_BRUSH) 192 | 193 | self.setFlag(QGraphicsItem.ItemIsMovable) 194 | if ext.transition_type == 0: 195 | # Screen edge based exit transition. 196 | 197 | # Should not be draggable. 198 | self.setFlag(QGraphicsItem.ItemIsMovable, enabled=False) 199 | 200 | dir = None 201 | bits = None 202 | for i in range(4): 203 | bit_shift = i*2 204 | bit_mask = (3 << bit_shift) 205 | if (ext.screen_edge & bit_mask) != 0: 206 | dir = i 207 | bits = (ext.screen_edge & bit_mask) >> bit_shift 208 | break 209 | # TODO: what to do when more than one direction is set? 210 | 211 | if dir in [0, 2]: 212 | if dir == 0: 213 | # Up 214 | y = -16 215 | elif dir == 2: 216 | # Down 217 | y = room.height 218 | 219 | x = 0 220 | w = room.width 221 | 222 | if bits == 1: 223 | w //= 2 224 | elif bits == 2: 225 | w //= 2 226 | x += w 227 | 228 | self.setPos(x, y) 229 | self.setRect(0, 0, w, 16) 230 | elif dir in [1, 3]: 231 | if dir == 1: 232 | # Right 233 | x = room.width 234 | elif dir == 3: 235 | # Left 236 | x = -16 237 | 238 | y = 0 239 | h = room.height 240 | 241 | if bits == 1: 242 | h //= 2 243 | elif bits == 2: 244 | h //= 2 245 | y += h 246 | 247 | self.setPos(x, y) 248 | self.setRect(0, 0, 16, h) 249 | else: 250 | # No direction bits are set. 251 | # TODO: How to represent this invalid state visually? 252 | pass 253 | 254 | def init_exit_region(self): 255 | self.setBrush(self.EXIT_BRUSH) 256 | 257 | self.setPos(self.entity.center_x, self.entity.center_y) 258 | self.setRect(-self.entity.half_width, -self.entity.half_height, self.entity.half_width*2, self.entity.half_height*2) 259 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_sprite_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'sprite_editor.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QGraphicsView, QHBoxLayout, QLabel, 20 | QListWidget, QListWidgetItem, QSizePolicy, QTabWidget, 21 | QVBoxLayout, QWidget) 22 | 23 | class Ui_SpriteEditor(object): 24 | def setupUi(self, SpriteEditor): 25 | if not SpriteEditor.objectName(): 26 | SpriteEditor.setObjectName(u"SpriteEditor") 27 | SpriteEditor.resize(832, 478) 28 | self.verticalLayout = QVBoxLayout(SpriteEditor) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.horizontalLayout = QHBoxLayout() 31 | self.horizontalLayout.setObjectName(u"horizontalLayout") 32 | self.tabWidget = QTabWidget(SpriteEditor) 33 | self.tabWidget.setObjectName(u"tabWidget") 34 | self.tab = QWidget() 35 | self.tab.setObjectName(u"tab") 36 | self.verticalLayout_3 = QVBoxLayout(self.tab) 37 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 38 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 39 | self.enemy_list = QListWidget(self.tab) 40 | self.enemy_list.setObjectName(u"enemy_list") 41 | 42 | self.verticalLayout_3.addWidget(self.enemy_list) 43 | 44 | self.tabWidget.addTab(self.tab, "") 45 | self.tab_2 = QWidget() 46 | self.tab_2.setObjectName(u"tab_2") 47 | self.verticalLayout_4 = QVBoxLayout(self.tab_2) 48 | self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) 49 | self.verticalLayout_4.setObjectName(u"verticalLayout_4") 50 | self.object_list = QListWidget(self.tab_2) 51 | self.object_list.setObjectName(u"object_list") 52 | 53 | self.verticalLayout_4.addWidget(self.object_list) 54 | 55 | self.tabWidget.addTab(self.tab_2, "") 56 | self.tab_3 = QWidget() 57 | self.tab_3.setObjectName(u"tab_3") 58 | self.verticalLayout_5 = QVBoxLayout(self.tab_3) 59 | self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) 60 | self.verticalLayout_5.setObjectName(u"verticalLayout_5") 61 | self.npc_list = QListWidget(self.tab_3) 62 | self.npc_list.setObjectName(u"npc_list") 63 | 64 | self.verticalLayout_5.addWidget(self.npc_list) 65 | 66 | self.tabWidget.addTab(self.tab_3, "") 67 | self.tab_4 = QWidget() 68 | self.tab_4.setObjectName(u"tab_4") 69 | self.verticalLayout_6 = QVBoxLayout(self.tab_4) 70 | self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) 71 | self.verticalLayout_6.setObjectName(u"verticalLayout_6") 72 | self.player_list = QListWidget(self.tab_4) 73 | self.player_list.setObjectName(u"player_list") 74 | 75 | self.verticalLayout_6.addWidget(self.player_list) 76 | 77 | self.tabWidget.addTab(self.tab_4, "") 78 | self.tab_5 = QWidget() 79 | self.tab_5.setObjectName(u"tab_5") 80 | self.verticalLayout_7 = QVBoxLayout(self.tab_5) 81 | self.verticalLayout_7.setContentsMargins(0, 0, 0, 0) 82 | self.verticalLayout_7.setObjectName(u"verticalLayout_7") 83 | self.type_4s_list = QListWidget(self.tab_5) 84 | self.type_4s_list.setObjectName(u"type_4s_list") 85 | 86 | self.verticalLayout_7.addWidget(self.type_4s_list) 87 | 88 | self.tabWidget.addTab(self.tab_5, "") 89 | self.tab_6 = QWidget() 90 | self.tab_6.setObjectName(u"tab_6") 91 | self.verticalLayout_8 = QVBoxLayout(self.tab_6) 92 | self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) 93 | self.verticalLayout_8.setObjectName(u"verticalLayout_8") 94 | self.player_items_list = QListWidget(self.tab_6) 95 | self.player_items_list.setObjectName(u"player_items_list") 96 | 97 | self.verticalLayout_8.addWidget(self.player_items_list) 98 | 99 | self.tabWidget.addTab(self.tab_6, "") 100 | 101 | self.horizontalLayout.addWidget(self.tabWidget) 102 | 103 | self.verticalLayout_2 = QVBoxLayout() 104 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 105 | self.verticalLayout_9 = QVBoxLayout() 106 | self.verticalLayout_9.setObjectName(u"verticalLayout_9") 107 | self.horizontalLayout_2 = QHBoxLayout() 108 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 109 | self.label_3 = QLabel(SpriteEditor) 110 | self.label_3.setObjectName(u"label_3") 111 | 112 | self.horizontalLayout_2.addWidget(self.label_3) 113 | 114 | self.form_index = QComboBox(SpriteEditor) 115 | self.form_index.setObjectName(u"form_index") 116 | 117 | self.horizontalLayout_2.addWidget(self.form_index) 118 | 119 | 120 | self.verticalLayout_9.addLayout(self.horizontalLayout_2) 121 | 122 | self.horizontalLayout_3 = QHBoxLayout() 123 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") 124 | self.label = QLabel(SpriteEditor) 125 | self.label.setObjectName(u"label") 126 | 127 | self.horizontalLayout_3.addWidget(self.label) 128 | 129 | self.anim_index = QComboBox(SpriteEditor) 130 | self.anim_index.setObjectName(u"anim_index") 131 | 132 | self.horizontalLayout_3.addWidget(self.anim_index) 133 | 134 | self.label_2 = QLabel(SpriteEditor) 135 | self.label_2.setObjectName(u"label_2") 136 | 137 | self.horizontalLayout_3.addWidget(self.label_2) 138 | 139 | self.frame_index = QComboBox(SpriteEditor) 140 | self.frame_index.setObjectName(u"frame_index") 141 | 142 | self.horizontalLayout_3.addWidget(self.frame_index) 143 | 144 | 145 | self.verticalLayout_9.addLayout(self.horizontalLayout_3) 146 | 147 | 148 | self.verticalLayout_2.addLayout(self.verticalLayout_9) 149 | 150 | self.sprite_graphics_view = QGraphicsView(SpriteEditor) 151 | self.sprite_graphics_view.setObjectName(u"sprite_graphics_view") 152 | 153 | self.verticalLayout_2.addWidget(self.sprite_graphics_view) 154 | 155 | 156 | self.horizontalLayout.addLayout(self.verticalLayout_2) 157 | 158 | 159 | self.verticalLayout.addLayout(self.horizontalLayout) 160 | 161 | self.buttonBox = QDialogButtonBox(SpriteEditor) 162 | self.buttonBox.setObjectName(u"buttonBox") 163 | self.buttonBox.setOrientation(Qt.Horizontal) 164 | self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) 165 | 166 | self.verticalLayout.addWidget(self.buttonBox) 167 | 168 | 169 | self.retranslateUi(SpriteEditor) 170 | self.buttonBox.accepted.connect(SpriteEditor.accept) 171 | self.buttonBox.rejected.connect(SpriteEditor.reject) 172 | 173 | self.tabWidget.setCurrentIndex(0) 174 | 175 | 176 | QMetaObject.connectSlotsByName(SpriteEditor) 177 | # setupUi 178 | 179 | def retranslateUi(self, SpriteEditor): 180 | SpriteEditor.setWindowTitle(QCoreApplication.translate("SpriteEditor", u"Text Editor", None)) 181 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QCoreApplication.translate("SpriteEditor", u"Enemies", None)) 182 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("SpriteEditor", u"Objects", None)) 183 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QCoreApplication.translate("SpriteEditor", u"NPCs", None)) 184 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QCoreApplication.translate("SpriteEditor", u"Player", None)) 185 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), QCoreApplication.translate("SpriteEditor", u"Type 4s", None)) 186 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_6), QCoreApplication.translate("SpriteEditor", u"Player Items", None)) 187 | self.label_3.setText(QCoreApplication.translate("SpriteEditor", u"Form", None)) 188 | self.label.setText(QCoreApplication.translate("SpriteEditor", u"Animation", None)) 189 | self.label_2.setText(QCoreApplication.translate("SpriteEditor", u"Frame", None)) 190 | # retranslateUi 191 | 192 | -------------------------------------------------------------------------------- /mcedit_ui/save_editor_dialog.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | import traceback 7 | 8 | from mcedit_ui.uic.ui_save_editor import Ui_SaveEditor 9 | 10 | from mclib.data_interface import DataInterface 11 | from mclib.save import Save, SaveSlot 12 | from mclib.docs import ITEM_ID_TO_NAME 13 | 14 | class SaveEditorDialog(QDialog): 15 | def __init__(self, main_window): 16 | super().__init__(main_window) 17 | self.ui = Ui_SaveEditor() 18 | self.ui.setupUi(self) 19 | 20 | self.rom = main_window.game.rom 21 | 22 | self.save = None 23 | 24 | self.ui.import_raw_save.clicked.connect(self.import_raw_save) 25 | self.ui.import_vba_mgba_save.clicked.connect(self.import_vba_mgba_save) 26 | self.ui.import_gameshark_save.clicked.connect(self.import_gameshark_save) 27 | self.ui.export_raw_save.clicked.connect(self.export_raw_save) 28 | self.ui.export_vba_mgba_save.clicked.connect(self.export_vba_mgba_save) 29 | self.ui.export_gameshark_save.clicked.connect(self.export_gameshark_save) 30 | 31 | self.selected_slot_index = 0 32 | self.ui.selected_slot_index.activated.connect(self.selected_slot_index_changed) 33 | 34 | self.initialize_ui_lists() 35 | 36 | self.show() 37 | 38 | def selected_slot_index_changed(self, new_slot_index): 39 | self.update_save_from_ui() # Save changes to the previously slot UI to the save. 40 | self.selected_slot_index = new_slot_index 41 | self.update_ui_from_save() # Load the new slot's data from the save to the UI. 42 | 43 | def initialize_ui_lists(self): 44 | figs_layout = self.ui.owned_figurines_layout 45 | for fig_id in range(SaveSlot.NUM_FIGURINES): 46 | checkbox = QCheckBox(self) 47 | checkbox.setText("Figurine %d" % (fig_id)) 48 | figs_layout.addWidget(checkbox) 49 | spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 50 | figs_layout.addItem(spacer) 51 | 52 | items_layout = self.ui.owned_items_layout 53 | for item_id in range(SaveSlot.NUM_ITEMS): 54 | item_name = ITEM_ID_TO_NAME.get(item_id, "") 55 | 56 | label = QLabel(self) 57 | label.setText("%02X %s" % (item_id, item_name)) 58 | items_layout.setWidget(item_id, QFormLayout.LabelRole, label) 59 | 60 | dropdown = QComboBox(self) 61 | dropdown.addItem("Not owned") 62 | dropdown.addItem("Owned") 63 | dropdown.addItem("Formerly owned") 64 | dropdown.addItem("(Unused)") 65 | items_layout.setWidget(item_id, QFormLayout.FieldRole, dropdown) 66 | 67 | flags_layout = self.ui.flags_layout 68 | self.flag_checkboxes = [] 69 | all_local_flag_offsets = [ 70 | (0x0000, "Global"), 71 | (0x0100, "Outdoors"), 72 | (0x0200, "Indoors"), 73 | (0x0300, "Caves & Passages"), 74 | (0x0400, "Caves"), 75 | (0x0500, "Deepwood Shrine"), 76 | (0x05C0, "Cave of Flames"), 77 | (0x0680, "Fortress of Winds"), 78 | (0x0740, "Temple of Droplets"), 79 | (0x0800, "Palace of Winds"), 80 | (0x08C0, "Dark Hyrule Castle"), 81 | (0x09C0, "(Unused 1)"), 82 | (0x0A80, "(Unused 2)"), 83 | (0x1000, ""), 84 | ] 85 | col = 0 86 | for i in range(len(all_local_flag_offsets)-1): 87 | local_flag_offset, flag_region_name = all_local_flag_offsets[i] 88 | next_local_flag_offset, next_flag_region_name = all_local_flag_offsets[i+1] 89 | 90 | label = QLabel() 91 | label.setText(flag_region_name) 92 | flags_layout.addWidget(label, 0, col) 93 | 94 | row = 1 95 | for flag_index in range(local_flag_offset, next_local_flag_offset): 96 | checkbox = QCheckBox(self) 97 | checkbox.setText("Flag %03X" % (flag_index)) 98 | flags_layout.addWidget(checkbox, row, col) 99 | self.flag_checkboxes.append(checkbox) 100 | row += 1 101 | col += 1 102 | 103 | def update_ui_from_save(self): 104 | if self.save is None: 105 | return 106 | 107 | slot = self.save.slots[self.selected_slot_index] 108 | 109 | figs_layout = self.ui.owned_figurines_layout 110 | for fig_id, fig_is_owned in enumerate(slot.owned_figurines): 111 | checkbox = figs_layout.itemAt(fig_id).widget() 112 | checkbox.setChecked(fig_is_owned) 113 | 114 | items_layout = self.ui.owned_items_layout 115 | for item_id, owned_item_value in enumerate(slot.owned_item_info): 116 | dropdown = items_layout.itemAt(item_id, QFormLayout.FieldRole).widget() 117 | dropdown.setCurrentIndex(owned_item_value) 118 | 119 | for flag_index, flag_is_set in enumerate(slot.flags): 120 | checkbox = self.flag_checkboxes[flag_index] 121 | checkbox.setChecked(flag_is_set) 122 | 123 | def update_save_from_ui(self): 124 | if self.save is None: 125 | return 126 | 127 | slot = self.save.slots[self.selected_slot_index] 128 | 129 | figs_layout = self.ui.owned_figurines_layout 130 | for fig_id, fig_is_owned in enumerate(slot.owned_figurines): 131 | checkbox = figs_layout.itemAt(fig_id).widget() 132 | slot.owned_figurines[fig_id] = checkbox.isChecked() 133 | 134 | items_layout = self.ui.owned_items_layout 135 | for item_id, owned_item_value in enumerate(slot.owned_item_info): 136 | dropdown = items_layout.itemAt(item_id, QFormLayout.FieldRole).widget() 137 | slot.owned_item_info[item_id] = dropdown.currentIndex() 138 | 139 | for flag_index, flag_is_set in enumerate(slot.flags): 140 | checkbox = self.flag_checkboxes[flag_index] 141 | slot.flags[flag_index] = checkbox.isChecked() 142 | 143 | 144 | def get_import_path(self): 145 | default_dir = None 146 | file_path, selected_filter = QFileDialog.getOpenFileName(self, "Select save to open", default_dir, "Save Files (*.sav)") 147 | return file_path 148 | 149 | def get_export_path(self): 150 | if self.save is None: 151 | QMessageBox.warning(self, 152 | "No save data loaded", 153 | "Must import a save before you can export it." 154 | ) 155 | return None 156 | 157 | default_dir = None 158 | file_path, selected_filter = QFileDialog.getSaveFileName(self, "Select where to export save", default_dir, "Save Files (*.sav)") 159 | return file_path 160 | 161 | def import_raw_save(self): 162 | file_path = self.get_import_path() 163 | if not file_path: 164 | return 165 | 166 | try: 167 | with open(file_path, "rb") as f: 168 | data = DataInterface(f.read()) 169 | self.save = Save.from_raw_format(data) 170 | except Exception as e: 171 | stack_trace = traceback.format_exc() 172 | error_message = "Error importing save:\n" + str(e) + "\n\n" + stack_trace 173 | QMessageBox.critical(self, 174 | "Error importing save", 175 | error_message 176 | ) 177 | return 178 | 179 | self.update_ui_from_save() 180 | 181 | def import_vba_mgba_save(self): 182 | file_path = self.get_import_path() 183 | if not file_path: 184 | return 185 | 186 | try: 187 | with open(file_path, "rb") as f: 188 | data = DataInterface(f.read()) 189 | self.save = Save.from_vba_mgba_format(data) 190 | except Exception as e: 191 | stack_trace = traceback.format_exc() 192 | error_message = "Error importing save:\n" + str(e) + "\n\n" + stack_trace 193 | QMessageBox.critical(self, 194 | "Error importing save", 195 | error_message 196 | ) 197 | return 198 | 199 | self.update_ui_from_save() 200 | 201 | def import_gameshark_save(self): 202 | file_path = self.get_import_path() 203 | if not file_path: 204 | return 205 | 206 | try: 207 | with open(file_path, "rb") as f: 208 | data = DataInterface(f.read()) 209 | self.save = Save.from_gameshark_format(data) 210 | except Exception as e: 211 | stack_trace = traceback.format_exc() 212 | error_message = "Error importing save:\n" + str(e) + "\n\n" + stack_trace 213 | QMessageBox.critical(self, 214 | "Error importing save", 215 | error_message 216 | ) 217 | return 218 | 219 | self.update_ui_from_save() 220 | 221 | def export_raw_save(self): 222 | file_path = self.get_export_path() 223 | if not file_path: 224 | return 225 | 226 | try: 227 | self.update_save_from_ui() 228 | self.save.write() 229 | raw_bytes = self.save.to_raw_format().read_all_bytes() 230 | with open(file_path, "wb") as f: 231 | f.write(raw_bytes) 232 | except Exception as e: 233 | stack_trace = traceback.format_exc() 234 | error_message = "Error exporting save:\n" + str(e) + "\n\n" + stack_trace 235 | QMessageBox.critical(self, 236 | "Error exporting save", 237 | error_message 238 | ) 239 | return 240 | 241 | def export_vba_mgba_save(self): 242 | file_path = self.get_export_path() 243 | if not file_path: 244 | return 245 | 246 | try: 247 | self.update_save_from_ui() 248 | self.save.write() 249 | raw_bytes = self.save.to_vba_mgba_format().read_all_bytes() 250 | with open(file_path, "wb") as f: 251 | f.write(raw_bytes) 252 | except Exception as e: 253 | stack_trace = traceback.format_exc() 254 | error_message = "Error exporting save:\n" + str(e) + "\n\n" + stack_trace 255 | QMessageBox.critical(self, 256 | "Error exporting save", 257 | error_message 258 | ) 259 | return 260 | 261 | def export_gameshark_save(self): 262 | file_path = self.get_export_path() 263 | if not file_path: 264 | return 265 | 266 | try: 267 | self.update_save_from_ui() 268 | self.save.write() 269 | raw_bytes = self.save.to_gameshark_format().read_all_bytes() 270 | with open(file_path, "wb") as f: 271 | f.write(raw_bytes) 272 | except Exception as e: 273 | stack_trace = traceback.format_exc() 274 | error_message = "Error exporting save:\n" + str(e) + "\n\n" + stack_trace 275 | QMessageBox.critical(self, 276 | "Error exporting save", 277 | error_message 278 | ) 279 | return 280 | -------------------------------------------------------------------------------- /mcedit_ui/layer_item.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | import traceback 7 | 8 | from mclib.visual_zone import VisualZone 9 | 10 | class LayerItem(QGraphicsRectItem): 11 | def __init__(self, room, layer_index, renderer, main_window): 12 | super().__init__() 13 | 14 | self.room = room 15 | self.layer_index = layer_index 16 | self.renderer = renderer 17 | self.rom = room.rom 18 | self.main_window = main_window 19 | 20 | try: 21 | self.render_layer() 22 | except Exception as e: 23 | stack_trace = traceback.format_exc() 24 | error_message = "Error rendering layer in room %02X-%02X:\n" % (room.area.area_index, room.room_index) 25 | error_message += str(e) + "\n\n" + stack_trace 26 | print(error_message) 27 | 28 | def layer_clicked(self, x, y, button): 29 | if x < 0 or y < 0 or x >= self.room.width or y >= self.room.height: 30 | return 31 | 32 | tile_x = x//0x10 33 | tile_y = y//0x10 34 | x = tile_x*0x10 35 | y = tile_y*0x10 36 | 37 | curr_tileset_scene = self.main_window.selected_tileset_graphics_scene 38 | 39 | if button == Qt.LeftButton: 40 | for x_off in range(curr_tileset_scene.selection_w): 41 | for y_off in range(curr_tileset_scene.selection_h): 42 | curr_tile_x_on_layer = tile_x + x_off 43 | curr_tile_y_on_layer = tile_y + y_off 44 | curr_x_on_layer = curr_tile_x_on_layer*0x10 45 | curr_y_on_layer = curr_tile_y_on_layer*0x10 46 | 47 | if curr_x_on_layer >= self.room.width: 48 | continue 49 | if curr_y_on_layer >= self.room.height: 50 | continue 51 | 52 | tile_index_16x16 = curr_tileset_scene.selected_tile_indexes[x_off + y_off*curr_tileset_scene.selection_w] 53 | 54 | tile_pixmap = self.get_tile_pixmap_by_16x16_index(tile_index_16x16, curr_x_on_layer, curr_y_on_layer) 55 | tile_item = self.tile_graphics_items_by_pos[curr_tile_x_on_layer][curr_tile_y_on_layer] 56 | tile_item.setPixmap(tile_pixmap) 57 | 58 | room_width_in_16x16_tiles = self.room.width//16 59 | tile_index_on_layer = curr_tile_y_on_layer*room_width_in_16x16_tiles + curr_tile_x_on_layer 60 | self.layer.data[tile_index_on_layer] = tile_index_16x16 61 | 62 | self.layer.has_unsaved_changes = True 63 | elif button == Qt.RightButton: 64 | room_width_in_16x16_tiles = self.room.width//16 65 | tile_index_on_layer = tile_y*room_width_in_16x16_tiles + tile_x 66 | tile_index_on_tileset = self.layer.data[tile_index_on_layer] 67 | curr_tileset_scene.select_tile_by_index(tile_index_on_tileset) 68 | 69 | def render_layer(self): 70 | room = self.room 71 | area = room.area 72 | layer_index = self.layer_index 73 | 74 | if room.area.uses_256_color_bg1s: 75 | if layer_index == 2: 76 | self.render_layer_mapped(color_mode=256) 77 | else: 78 | # Their BG1s may be unused? They seem to error out when trying to render them. TODO figure them out 79 | pass 80 | else: 81 | if layer_index == 3: 82 | if area.get_gfx_asset_list(room.gfx_index).tile_mappings_8x8[layer_index] is None: 83 | return 84 | self.render_layer_mapped(color_mode=16) 85 | elif room.layers_asset_list.tile_mappings_8x8[layer_index] is not None: 86 | self.render_layer_mapped(color_mode=16) 87 | else: 88 | self.render_layer_16_color() 89 | 90 | def render_layer_16_color(self): 91 | room = self.room 92 | area = room.area 93 | layer_index = self.layer_index 94 | 95 | self.tile_graphics_items_by_pos = [] 96 | for tile_x in range(room.width//0x10): 97 | self.tile_graphics_items_by_pos.append([]) 98 | for tile_y in range(room.height//0x10): 99 | self.tile_graphics_items_by_pos[tile_x].append(None) 100 | 101 | gfx_asset_list = area.get_gfx_asset_list(room.gfx_index) 102 | orig_gfx_data = gfx_asset_list.gfx_data 103 | if layer_index in [1, 3]: 104 | self.gfx_data = orig_gfx_data.read_raw(0x4000, len(orig_gfx_data)-0x4000) 105 | else: 106 | self.gfx_data = orig_gfx_data 107 | self.palettes = self.renderer.generate_palettes_for_area_by_gfx_index(room.area, room.gfx_index) 108 | self.tileset_data = room.area.tilesets_asset_list.tileset_datas[layer_index] 109 | if self.tileset_data is None: 110 | return 111 | 112 | self.layer = room.layers_asset_list.layers[layer_index] 113 | if self.layer is None: 114 | raise Exception("Layer BG%d has no layer data" % layer_index) 115 | if len(self.layer.data) == 0: 116 | raise Exception("Layer BG%d has zero-length layer data" % layer_index) 117 | if self.layer.data[0] == 0xFFFF: 118 | # No real layer data here 119 | return 120 | 121 | self.cached_8x8_tile_images_by_tile_attrs_and_zone_ids = {} 122 | 123 | room_width_in_16x16_tiles = room.width//16 124 | 125 | self.cached_tile_pixmaps_by_16x16_index = {} 126 | for i in range(len(self.layer.data)): 127 | tile_index_16x16 = self.layer.data[i] 128 | 129 | x = (i % room_width_in_16x16_tiles)*16 130 | y = (i // room_width_in_16x16_tiles)*16 131 | 132 | tile_pixmap = self.get_tile_pixmap_by_16x16_index(tile_index_16x16, x, y) 133 | 134 | tile_item = QGraphicsPixmapItem(tile_pixmap, self) 135 | tile_item.setPos(x, y) 136 | self.tile_graphics_items_by_pos[x//0x10][y//0x10] = tile_item 137 | 138 | def get_tile_pixmap_by_16x16_index(self, tile_index_16x16, x, y): 139 | if tile_index_16x16 in self.cached_tile_pixmaps_by_16x16_index: 140 | tile_pixmap = self.cached_tile_pixmaps_by_16x16_index[tile_index_16x16] 141 | else: 142 | tile_pixmap = self.render_tile_pixmap_by_16x16_tile_index(tile_index_16x16, x, y) 143 | 144 | self.cached_tile_pixmaps_by_16x16_index[tile_index_16x16] = tile_pixmap 145 | 146 | return tile_pixmap 147 | 148 | def render_tile_pixmap_by_16x16_tile_index(self, tile_index_16x16, x, y): 149 | room = self.room 150 | layer_index = self.layer_index 151 | gfx_data = self.gfx_data 152 | palettes = self.palettes 153 | zone_ids = [] 154 | 155 | if self.room.zone_lists: 156 | zone_ids = VisualZone.get_zone_ids_overlapping_point(self.room.zone_lists, x, y) 157 | 158 | if zone_ids: 159 | gfx_data = gfx_data.copy() 160 | 161 | for zone_id in zone_ids: 162 | zone_data = room.visual_zone_datas[zone_id] 163 | 164 | if zone_data.palette_group_index is not None: 165 | palettes = self.renderer.generate_palettes_from_palette_group_by_index(zone_data.palette_group_index) 166 | 167 | for zone_gfx_data_ptr, zone_gfx_load_offset in zone_data.gfx_load_datas: 168 | if layer_index in [1, 3]: 169 | zone_gfx_load_offset -= 0x4000 170 | if zone_gfx_load_offset < 0: 171 | continue 172 | 173 | zone_gfx_data = self.rom.read_raw(zone_gfx_data_ptr, 0x1000) 174 | gfx_data.write_raw(zone_gfx_load_offset, zone_gfx_data) 175 | 176 | tile_image_16x16 = QImage(16, 16, QImage.Format_ARGB32) 177 | tile_image_16x16.fill(0) 178 | painter = QPainter(tile_image_16x16) 179 | 180 | zone_ids_tuple = tuple(zone_ids) 181 | if zone_ids_tuple not in self.cached_8x8_tile_images_by_tile_attrs_and_zone_ids: 182 | self.cached_8x8_tile_images_by_tile_attrs_and_zone_ids[zone_ids_tuple] = {} 183 | cached_8x8_tile_images_by_tile_attrs = self.cached_8x8_tile_images_by_tile_attrs_and_zone_ids[zone_ids_tuple] 184 | 185 | try: 186 | for tile_8x8_i in range(4): 187 | tile_attrs = self.tileset_data[tile_index_16x16*4 + tile_8x8_i] 188 | 189 | horizontal_flip = (tile_attrs & 0x0400) > 0 190 | vertical_flip = (tile_attrs & 0x0800) > 0 191 | 192 | # Remove flip bits so all 4 orientations can be cached together as one. 193 | tile_attrs &= (~0x0C00) 194 | 195 | if tile_attrs in cached_8x8_tile_images_by_tile_attrs: 196 | data = cached_8x8_tile_images_by_tile_attrs[tile_attrs] 197 | else: 198 | pil_image = self.renderer.render_tile_by_tile_attrs(tile_attrs, gfx_data, palettes) 199 | data = pil_image.tobytes('raw', 'BGRA') 200 | cached_8x8_tile_images_by_tile_attrs[tile_attrs] = data 201 | 202 | # For some reason, QImages can't be cached safely, they would become corrupted looking. 203 | # So cache just the image data instead. 204 | tile_image_8x8 = QImage(data, 8, 8, QImage.Format_ARGB32) 205 | 206 | if horizontal_flip and vertical_flip: 207 | tile_image_8x8 = tile_image_8x8.transformed(QTransform.fromScale(-1, -1)) 208 | elif horizontal_flip: 209 | tile_image_8x8 = tile_image_8x8.transformed(QTransform.fromScale(-1, 1)) 210 | elif vertical_flip: 211 | tile_image_8x8 = tile_image_8x8.transformed(QTransform.fromScale(1, -1)) 212 | 213 | x_on_16x16_tile = (tile_8x8_i % 2)*8 214 | y_on_16x16_tile = (tile_8x8_i // 2)*8 215 | 216 | painter.drawImage(x_on_16x16_tile, y_on_16x16_tile, tile_image_8x8) 217 | except: 218 | # Need to properly end the painter or the program will crash 219 | painter.end() 220 | raise 221 | 222 | painter.end() 223 | 224 | tile_pixmap = QPixmap.fromImage(tile_image_16x16) 225 | 226 | return tile_pixmap 227 | 228 | def render_layer_mapped(self, color_mode=256): 229 | room = self.room 230 | layer_index = self.layer_index 231 | 232 | palettes = self.renderer.generate_palettes_for_area_by_gfx_index(room.area, room.gfx_index) 233 | 234 | layer_image = self.renderer.render_layer_mapped(self.room, palettes, layer_index, color_mode=color_mode) 235 | 236 | data = layer_image.tobytes('raw', 'BGRA') 237 | qimage = QImage(data, layer_image.size[0], layer_image.size[1], QImage.Format_ARGB32) 238 | layer_pixmap = QPixmap.fromImage(qimage) 239 | 240 | graphics_item = QGraphicsPixmapItem(layer_pixmap, self) 241 | -------------------------------------------------------------------------------- /mcedit_ui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1280 10 | 768 11 | 12 | 13 | 14 | Minish Cap Editor 15 | 16 | 17 | 18 | 19 | 20 | 21 | Qt::Horizontal 22 | 23 | 24 | 10 25 | 26 | 27 | 28 | 29 | 0 30 | 31 | 32 | 33 | 34 | 35 | 36 | Area 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Room 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Qt::Vertical 65 | 66 | 67 | 68 | 20 69 | 40 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 2 80 | 0 81 | 82 | 83 | 84 | true 85 | 86 | 87 | 88 | 89 | 0 90 | 0 91 | 684 92 | 707 93 | 94 | 95 | 96 | 97 | 0 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 300 109 | 0 110 | 111 | 112 | 113 | 0 114 | 115 | 116 | 117 | Entities 118 | 119 | 120 | 121 | 0 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | BG2 131 | 132 | 133 | 134 | 0 135 | 136 | 137 | 138 | 139 | 140 | 0 141 | 0 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | BG1 151 | 152 | 153 | 154 | 0 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 0 170 | 0 171 | 1280 172 | 21 173 | 174 | 175 | 176 | 177 | File 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | View 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | Tools 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | Build 207 | 208 | 209 | 210 | 211 | 212 | Edit 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | true 226 | 227 | 228 | true 229 | 230 | 231 | Layer BG1 232 | 233 | 234 | 235 | 236 | true 237 | 238 | 239 | true 240 | 241 | 242 | Layer BG2 243 | 244 | 245 | 246 | 247 | true 248 | 249 | 250 | true 251 | 252 | 253 | Entities 254 | 255 | 256 | 257 | 258 | true 259 | 260 | 261 | true 262 | 263 | 264 | Tile Entities 265 | 266 | 267 | 268 | 269 | true 270 | 271 | 272 | true 273 | 274 | 275 | Exits 276 | 277 | 278 | 279 | 280 | Entity Search 281 | 282 | 283 | Ctrl+F 284 | 285 | 286 | 287 | 288 | Test Room 289 | 290 | 291 | F7 292 | 293 | 294 | 295 | 296 | true 297 | 298 | 299 | true 300 | 301 | 302 | Layer BG3 303 | 304 | 305 | 306 | 307 | Save Editor 308 | 309 | 310 | V 311 | 312 | 313 | 314 | 315 | Text Editor 316 | 317 | 318 | T 319 | 320 | 321 | 322 | 323 | New Project 324 | 325 | 326 | 327 | 328 | Open Project 329 | 330 | 331 | 332 | 333 | Save Project 334 | 335 | 336 | 337 | 338 | Save Project As 339 | 340 | 341 | 342 | 343 | Sprite Editor 344 | 345 | 346 | P 347 | 348 | 349 | 350 | 351 | Preferences 352 | 353 | 354 | 355 | 356 | 357 | EntityProperties 358 | QWidget 359 |
mcedit_ui.entity_properties
360 | 1 361 |
362 | 363 | RoomView 364 | QGraphicsView 365 |
mcedit_ui.room_view
366 |
367 |
368 | 369 | 370 |
371 | -------------------------------------------------------------------------------- /mcedit_ui/uic/ui_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'main.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.6.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, 15 | QCursor, QFont, QFontDatabase, QGradient, 16 | QIcon, QImage, QKeySequence, QLinearGradient, 17 | QPainter, QPalette, QPixmap, QRadialGradient, 18 | QTransform) 19 | from PySide6.QtWidgets import (QApplication, QComboBox, QFormLayout, QGraphicsView, 20 | QHBoxLayout, QLabel, QListWidget, QListWidgetItem, 21 | QMainWindow, QMenu, QMenuBar, QScrollArea, 22 | QSizePolicy, QSpacerItem, QSplitter, QStatusBar, 23 | QTabWidget, QVBoxLayout, QWidget) 24 | 25 | from mcedit_ui.entity_properties import EntityProperties 26 | from mcedit_ui.room_view import RoomView 27 | 28 | class Ui_MainWindow(object): 29 | def setupUi(self, MainWindow): 30 | if not MainWindow.objectName(): 31 | MainWindow.setObjectName(u"MainWindow") 32 | MainWindow.resize(1280, 768) 33 | self.actionLayer_BG1 = QAction(MainWindow) 34 | self.actionLayer_BG1.setObjectName(u"actionLayer_BG1") 35 | self.actionLayer_BG1.setCheckable(True) 36 | self.actionLayer_BG1.setChecked(True) 37 | self.actionLayer_BG2 = QAction(MainWindow) 38 | self.actionLayer_BG2.setObjectName(u"actionLayer_BG2") 39 | self.actionLayer_BG2.setCheckable(True) 40 | self.actionLayer_BG2.setChecked(True) 41 | self.actionEntities = QAction(MainWindow) 42 | self.actionEntities.setObjectName(u"actionEntities") 43 | self.actionEntities.setCheckable(True) 44 | self.actionEntities.setChecked(True) 45 | self.actionTile_Entities = QAction(MainWindow) 46 | self.actionTile_Entities.setObjectName(u"actionTile_Entities") 47 | self.actionTile_Entities.setCheckable(True) 48 | self.actionTile_Entities.setChecked(True) 49 | self.actionExits = QAction(MainWindow) 50 | self.actionExits.setObjectName(u"actionExits") 51 | self.actionExits.setCheckable(True) 52 | self.actionExits.setChecked(True) 53 | self.actionEntity_Search = QAction(MainWindow) 54 | self.actionEntity_Search.setObjectName(u"actionEntity_Search") 55 | self.actionTest_Room = QAction(MainWindow) 56 | self.actionTest_Room.setObjectName(u"actionTest_Room") 57 | self.actionLayer_BG3 = QAction(MainWindow) 58 | self.actionLayer_BG3.setObjectName(u"actionLayer_BG3") 59 | self.actionLayer_BG3.setCheckable(True) 60 | self.actionLayer_BG3.setChecked(True) 61 | self.actionSave_Editor = QAction(MainWindow) 62 | self.actionSave_Editor.setObjectName(u"actionSave_Editor") 63 | self.actionText_Editor = QAction(MainWindow) 64 | self.actionText_Editor.setObjectName(u"actionText_Editor") 65 | self.actionNew_Project = QAction(MainWindow) 66 | self.actionNew_Project.setObjectName(u"actionNew_Project") 67 | self.actionOpen_Project = QAction(MainWindow) 68 | self.actionOpen_Project.setObjectName(u"actionOpen_Project") 69 | self.actionSave_Project = QAction(MainWindow) 70 | self.actionSave_Project.setObjectName(u"actionSave_Project") 71 | self.actionSave_Project_As = QAction(MainWindow) 72 | self.actionSave_Project_As.setObjectName(u"actionSave_Project_As") 73 | self.actionSprite_Editor = QAction(MainWindow) 74 | self.actionSprite_Editor.setObjectName(u"actionSprite_Editor") 75 | self.actionPreferences = QAction(MainWindow) 76 | self.actionPreferences.setObjectName(u"actionPreferences") 77 | self.centralwidget = QWidget(MainWindow) 78 | self.centralwidget.setObjectName(u"centralwidget") 79 | self.horizontalLayout = QHBoxLayout(self.centralwidget) 80 | self.horizontalLayout.setObjectName(u"horizontalLayout") 81 | self.splitter = QSplitter(self.centralwidget) 82 | self.splitter.setObjectName(u"splitter") 83 | self.splitter.setOrientation(Qt.Horizontal) 84 | self.splitter.setHandleWidth(10) 85 | self.left_sidebar = QWidget(self.splitter) 86 | self.left_sidebar.setObjectName(u"left_sidebar") 87 | self.verticalLayout_2 = QVBoxLayout(self.left_sidebar) 88 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 89 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 90 | self.formLayout = QFormLayout() 91 | self.formLayout.setObjectName(u"formLayout") 92 | self.label = QLabel(self.left_sidebar) 93 | self.label.setObjectName(u"label") 94 | 95 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label) 96 | 97 | self.area_index = QComboBox(self.left_sidebar) 98 | self.area_index.setObjectName(u"area_index") 99 | 100 | self.formLayout.setWidget(0, QFormLayout.FieldRole, self.area_index) 101 | 102 | self.label_2 = QLabel(self.left_sidebar) 103 | self.label_2.setObjectName(u"label_2") 104 | 105 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label_2) 106 | 107 | self.room_index = QComboBox(self.left_sidebar) 108 | self.room_index.setObjectName(u"room_index") 109 | 110 | self.formLayout.setWidget(1, QFormLayout.FieldRole, self.room_index) 111 | 112 | 113 | self.verticalLayout_2.addLayout(self.formLayout) 114 | 115 | self.map_graphics_view = QGraphicsView(self.left_sidebar) 116 | self.map_graphics_view.setObjectName(u"map_graphics_view") 117 | 118 | self.verticalLayout_2.addWidget(self.map_graphics_view) 119 | 120 | self.entity_lists_list = QListWidget(self.left_sidebar) 121 | self.entity_lists_list.setObjectName(u"entity_lists_list") 122 | 123 | self.verticalLayout_2.addWidget(self.entity_lists_list) 124 | 125 | self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 126 | 127 | self.verticalLayout_2.addItem(self.verticalSpacer) 128 | 129 | self.splitter.addWidget(self.left_sidebar) 130 | self.scrollArea = QScrollArea(self.splitter) 131 | self.scrollArea.setObjectName(u"scrollArea") 132 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 133 | sizePolicy.setHorizontalStretch(2) 134 | sizePolicy.setVerticalStretch(0) 135 | sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) 136 | self.scrollArea.setSizePolicy(sizePolicy) 137 | self.scrollArea.setWidgetResizable(True) 138 | self.scrollAreaWidgetContents = QWidget() 139 | self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") 140 | self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 684, 707)) 141 | self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents) 142 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 143 | self.verticalLayout.setObjectName(u"verticalLayout") 144 | self.room_graphics_view = RoomView(self.scrollAreaWidgetContents) 145 | self.room_graphics_view.setObjectName(u"room_graphics_view") 146 | 147 | self.verticalLayout.addWidget(self.room_graphics_view) 148 | 149 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 150 | self.splitter.addWidget(self.scrollArea) 151 | self.right_sidebar = QTabWidget(self.splitter) 152 | self.right_sidebar.setObjectName(u"right_sidebar") 153 | self.right_sidebar.setMinimumSize(QSize(300, 0)) 154 | self.tab = QWidget() 155 | self.tab.setObjectName(u"tab") 156 | self.verticalLayout_6 = QVBoxLayout(self.tab) 157 | self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) 158 | self.verticalLayout_6.setObjectName(u"verticalLayout_6") 159 | self.entity_properies = EntityProperties(self.tab) 160 | self.entity_properies.setObjectName(u"entity_properies") 161 | 162 | self.verticalLayout_6.addWidget(self.entity_properies) 163 | 164 | self.right_sidebar.addTab(self.tab, "") 165 | self.tab_2 = QWidget() 166 | self.tab_2.setObjectName(u"tab_2") 167 | self.verticalLayout_7 = QVBoxLayout(self.tab_2) 168 | self.verticalLayout_7.setContentsMargins(0, 0, 0, 0) 169 | self.verticalLayout_7.setObjectName(u"verticalLayout_7") 170 | self.bg2_tileset_graphics_view = QGraphicsView(self.tab_2) 171 | self.bg2_tileset_graphics_view.setObjectName(u"bg2_tileset_graphics_view") 172 | sizePolicy1 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) 173 | sizePolicy1.setHorizontalStretch(0) 174 | sizePolicy1.setVerticalStretch(0) 175 | sizePolicy1.setHeightForWidth(self.bg2_tileset_graphics_view.sizePolicy().hasHeightForWidth()) 176 | self.bg2_tileset_graphics_view.setSizePolicy(sizePolicy1) 177 | 178 | self.verticalLayout_7.addWidget(self.bg2_tileset_graphics_view) 179 | 180 | self.right_sidebar.addTab(self.tab_2, "") 181 | self.tab_3 = QWidget() 182 | self.tab_3.setObjectName(u"tab_3") 183 | self.verticalLayout_8 = QVBoxLayout(self.tab_3) 184 | self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) 185 | self.verticalLayout_8.setObjectName(u"verticalLayout_8") 186 | self.bg1_tileset_graphics_view = QGraphicsView(self.tab_3) 187 | self.bg1_tileset_graphics_view.setObjectName(u"bg1_tileset_graphics_view") 188 | 189 | self.verticalLayout_8.addWidget(self.bg1_tileset_graphics_view) 190 | 191 | self.right_sidebar.addTab(self.tab_3, "") 192 | self.splitter.addWidget(self.right_sidebar) 193 | 194 | self.horizontalLayout.addWidget(self.splitter) 195 | 196 | MainWindow.setCentralWidget(self.centralwidget) 197 | self.menubar = QMenuBar(MainWindow) 198 | self.menubar.setObjectName(u"menubar") 199 | self.menubar.setGeometry(QRect(0, 0, 1280, 21)) 200 | self.menuFile = QMenu(self.menubar) 201 | self.menuFile.setObjectName(u"menuFile") 202 | self.menuView = QMenu(self.menubar) 203 | self.menuView.setObjectName(u"menuView") 204 | self.menuTools = QMenu(self.menubar) 205 | self.menuTools.setObjectName(u"menuTools") 206 | self.menuBuild = QMenu(self.menubar) 207 | self.menuBuild.setObjectName(u"menuBuild") 208 | self.menuEdit = QMenu(self.menubar) 209 | self.menuEdit.setObjectName(u"menuEdit") 210 | MainWindow.setMenuBar(self.menubar) 211 | self.statusbar = QStatusBar(MainWindow) 212 | self.statusbar.setObjectName(u"statusbar") 213 | MainWindow.setStatusBar(self.statusbar) 214 | 215 | self.menubar.addAction(self.menuFile.menuAction()) 216 | self.menubar.addAction(self.menuEdit.menuAction()) 217 | self.menubar.addAction(self.menuView.menuAction()) 218 | self.menubar.addAction(self.menuTools.menuAction()) 219 | self.menubar.addAction(self.menuBuild.menuAction()) 220 | self.menuFile.addAction(self.actionNew_Project) 221 | self.menuFile.addAction(self.actionOpen_Project) 222 | self.menuFile.addAction(self.actionSave_Project) 223 | self.menuFile.addAction(self.actionSave_Project_As) 224 | self.menuView.addAction(self.actionLayer_BG1) 225 | self.menuView.addAction(self.actionLayer_BG2) 226 | self.menuView.addAction(self.actionLayer_BG3) 227 | self.menuView.addAction(self.actionEntities) 228 | self.menuView.addAction(self.actionTile_Entities) 229 | self.menuView.addAction(self.actionExits) 230 | self.menuTools.addAction(self.actionEntity_Search) 231 | self.menuTools.addAction(self.actionSave_Editor) 232 | self.menuTools.addAction(self.actionText_Editor) 233 | self.menuTools.addAction(self.actionSprite_Editor) 234 | self.menuBuild.addAction(self.actionTest_Room) 235 | self.menuEdit.addAction(self.actionPreferences) 236 | 237 | self.retranslateUi(MainWindow) 238 | 239 | self.right_sidebar.setCurrentIndex(0) 240 | 241 | 242 | QMetaObject.connectSlotsByName(MainWindow) 243 | # setupUi 244 | 245 | def retranslateUi(self, MainWindow): 246 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Minish Cap Editor", None)) 247 | self.actionLayer_BG1.setText(QCoreApplication.translate("MainWindow", u"Layer BG1", None)) 248 | self.actionLayer_BG2.setText(QCoreApplication.translate("MainWindow", u"Layer BG2", None)) 249 | self.actionEntities.setText(QCoreApplication.translate("MainWindow", u"Entities", None)) 250 | self.actionTile_Entities.setText(QCoreApplication.translate("MainWindow", u"Tile Entities", None)) 251 | self.actionExits.setText(QCoreApplication.translate("MainWindow", u"Exits", None)) 252 | self.actionEntity_Search.setText(QCoreApplication.translate("MainWindow", u"Entity Search", None)) 253 | #if QT_CONFIG(shortcut) 254 | self.actionEntity_Search.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+F", None)) 255 | #endif // QT_CONFIG(shortcut) 256 | self.actionTest_Room.setText(QCoreApplication.translate("MainWindow", u"Test Room", None)) 257 | #if QT_CONFIG(shortcut) 258 | self.actionTest_Room.setShortcut(QCoreApplication.translate("MainWindow", u"F7", None)) 259 | #endif // QT_CONFIG(shortcut) 260 | self.actionLayer_BG3.setText(QCoreApplication.translate("MainWindow", u"Layer BG3", None)) 261 | self.actionSave_Editor.setText(QCoreApplication.translate("MainWindow", u"Save Editor", None)) 262 | #if QT_CONFIG(shortcut) 263 | self.actionSave_Editor.setShortcut(QCoreApplication.translate("MainWindow", u"V", None)) 264 | #endif // QT_CONFIG(shortcut) 265 | self.actionText_Editor.setText(QCoreApplication.translate("MainWindow", u"Text Editor", None)) 266 | #if QT_CONFIG(shortcut) 267 | self.actionText_Editor.setShortcut(QCoreApplication.translate("MainWindow", u"T", None)) 268 | #endif // QT_CONFIG(shortcut) 269 | self.actionNew_Project.setText(QCoreApplication.translate("MainWindow", u"New Project", None)) 270 | self.actionOpen_Project.setText(QCoreApplication.translate("MainWindow", u"Open Project", None)) 271 | self.actionSave_Project.setText(QCoreApplication.translate("MainWindow", u"Save Project", None)) 272 | self.actionSave_Project_As.setText(QCoreApplication.translate("MainWindow", u"Save Project As", None)) 273 | self.actionSprite_Editor.setText(QCoreApplication.translate("MainWindow", u"Sprite Editor", None)) 274 | #if QT_CONFIG(shortcut) 275 | self.actionSprite_Editor.setShortcut(QCoreApplication.translate("MainWindow", u"P", None)) 276 | #endif // QT_CONFIG(shortcut) 277 | self.actionPreferences.setText(QCoreApplication.translate("MainWindow", u"Preferences", None)) 278 | self.label.setText(QCoreApplication.translate("MainWindow", u"Area", None)) 279 | self.label_2.setText(QCoreApplication.translate("MainWindow", u"Room", None)) 280 | self.right_sidebar.setTabText(self.right_sidebar.indexOf(self.tab), QCoreApplication.translate("MainWindow", u"Entities", None)) 281 | self.right_sidebar.setTabText(self.right_sidebar.indexOf(self.tab_2), QCoreApplication.translate("MainWindow", u"BG2", None)) 282 | self.right_sidebar.setTabText(self.right_sidebar.indexOf(self.tab_3), QCoreApplication.translate("MainWindow", u"BG1", None)) 283 | self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) 284 | self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"View", None)) 285 | self.menuTools.setTitle(QCoreApplication.translate("MainWindow", u"Tools", None)) 286 | self.menuBuild.setTitle(QCoreApplication.translate("MainWindow", u"Build", None)) 287 | self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) 288 | # retranslateUi 289 | 290 | -------------------------------------------------------------------------------- /mcedit_ui/main_window.py: -------------------------------------------------------------------------------- 1 | 2 | from PySide6.QtGui import * 3 | from PySide6.QtCore import * 4 | from PySide6.QtWidgets import * 5 | 6 | from mcedit_ui.uic.ui_main import Ui_MainWindow 7 | from mcedit_ui.clickable_graphics_scene import * 8 | from mcedit_ui.custom_graphics_items import * 9 | from mcedit_ui.entity_layer_item import * 10 | from mcedit_ui.layer_item import * 11 | from mcedit_ui.tileset_graphics_scene import * 12 | 13 | from mcedit_ui.settings import * 14 | from mcedit_ui.entity_search_dialog import * 15 | from mcedit_ui.save_editor_dialog import * 16 | from mcedit_ui.text_editor_dialog import * 17 | from mcedit_ui.sprite_editor_dialog import * 18 | 19 | from mclib.game import Game 20 | from mclib.data_interface import RomInterface 21 | from mclib.renderer import Renderer 22 | from mclib.docs import AREA_INDEX_TO_NAME 23 | 24 | from version import VERSION 25 | 26 | import os 27 | from collections import OrderedDict 28 | from PIL import Image 29 | import traceback 30 | import subprocess 31 | import psutil 32 | from zipfile import ZipFile 33 | 34 | import yaml 35 | try: 36 | from yaml import CDumper as Dumper 37 | except ImportError: 38 | from yaml import Dumper 39 | 40 | # Allow yaml to load and dump OrderedDicts. 41 | yaml.SafeLoader.add_constructor( 42 | yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, 43 | lambda loader, node: OrderedDict(loader.construct_pairs(node)) 44 | ) 45 | yaml.Dumper.add_representer( 46 | OrderedDict, 47 | lambda dumper, data: dumper.represent_dict(data.items()) 48 | ) 49 | 50 | from paths import DATA_PATH 51 | 52 | class MCEditorWindow(QMainWindow): 53 | def __init__(self): 54 | super().__init__() 55 | self.ui = Ui_MainWindow() 56 | self.ui.setupUi(self) 57 | 58 | self.current_project_path = None 59 | 60 | self.open_dialogs = [] 61 | 62 | self.area_index = None 63 | self.room_index = None 64 | self.area = None 65 | self.room = None 66 | 67 | self.layer_items = [] 68 | self.selected_layer_index = None 69 | self.selected_tileset_graphics_scene = None 70 | self.selected_tiles_cursor = None 71 | 72 | self.last_emulator_process = None 73 | 74 | self.ui.scrollArea.setFrameShape(QFrame.NoFrame) 75 | 76 | self.room_graphics_scene = ClickableGraphicsScene() 77 | self.ui.room_graphics_view.setScene(self.room_graphics_scene) 78 | self.ui.room_graphics_view.setFocus() 79 | self.room_graphics_scene.clicked.connect(self.room_clicked) 80 | self.room_graphics_scene.clicked.connect(self.layer_clicked) 81 | self.room_graphics_scene.moved.connect(self.mouse_moved_on_room) 82 | 83 | self.map_graphics_scene = ClickableGraphicsScene() 84 | self.ui.map_graphics_view.setScene(self.map_graphics_scene) 85 | self.map_graphics_scene.clicked.connect(self.map_clicked) 86 | 87 | self.bg2_tileset_graphics_scene = TilesetGraphicsScene(self) 88 | self.ui.bg2_tileset_graphics_view.setScene(self.bg2_tileset_graphics_scene) 89 | self.bg1_tileset_graphics_scene = TilesetGraphicsScene(self) 90 | self.ui.bg1_tileset_graphics_view.setScene(self.bg1_tileset_graphics_scene) 91 | self.selected_tileset_graphics_scene = None 92 | 93 | self.ui.right_sidebar.currentChanged.connect(self.update_edit_mode_by_current_tab) 94 | QShortcut(QKeySequence(Qt.Key_F1), self, self.enter_entity_edit_mode) 95 | QShortcut(QKeySequence(Qt.Key_F2), self, self.enter_bg2_layer_edit_mode) 96 | QShortcut(QKeySequence(Qt.Key_F3), self, self.enter_bg1_layer_edit_mode) 97 | 98 | self.ui.actionNew_Project.triggered.connect(self.start_new_project) 99 | self.ui.actionOpen_Project.triggered.connect(self.open_project) 100 | self.ui.actionSave_Project.triggered.connect(self.save_project) 101 | self.ui.actionSave_Project_As.triggered.connect(self.save_project_as) 102 | 103 | self.ui.actionLayer_BG1.triggered.connect(self.update_visible_view_items) 104 | self.ui.actionLayer_BG2.triggered.connect(self.update_visible_view_items) 105 | self.ui.actionLayer_BG3.triggered.connect(self.update_visible_view_items) 106 | self.ui.actionEntities.triggered.connect(self.update_visible_view_items) 107 | self.ui.actionTile_Entities.triggered.connect(self.update_visible_view_items) 108 | self.ui.actionExits.triggered.connect(self.update_visible_view_items) 109 | 110 | self.ui.actionPreferences.triggered.connect(self.open_settings) 111 | self.ui.actionEntity_Search.triggered.connect(self.open_entity_search) 112 | self.ui.actionSave_Editor.triggered.connect(self.open_save_editor) 113 | self.ui.actionText_Editor.triggered.connect(self.open_text_editor) 114 | self.ui.actionSprite_Editor.triggered.connect(self.open_sprite_editor) 115 | 116 | self.ui.actionTest_Room.triggered.connect(self.test_room) 117 | 118 | self.ui.area_index.activated.connect(self.area_index_changed) 119 | self.ui.room_index.activated.connect(self.room_index_changed) 120 | 121 | self.ui.entity_lists_list.setSelectionMode(QAbstractItemView.ExtendedSelection) 122 | self.ui.entity_lists_list.itemSelectionChanged.connect(self.entity_list_visibility_toggled) 123 | 124 | self.setWindowTitle("MCEdit %s" % VERSION) 125 | 126 | #icon_path = os.path.join(ASSETS_PATH, "icon.ico") 127 | #self.setWindowIcon(QIcon(icon_path)) 128 | 129 | self.load_settings() 130 | 131 | self.setWindowState(Qt.WindowMaximized) 132 | 133 | self.disable_menu_actions_when_no_project_loaded() 134 | 135 | self.show() 136 | 137 | if "last_used_project" in self.settings and os.path.isfile(self.settings["last_used_project"]): 138 | try: 139 | self.open_project_by_path(self.settings["last_used_project"]) 140 | except Exception as e: 141 | stack_trace = traceback.format_exc() 142 | error_message = "Error opening last used project:\n" + str(e) + "\n\n" + stack_trace 143 | QMessageBox.warning(self, 144 | "Error opening last used project", 145 | error_message 146 | ) 147 | 148 | def load_settings(self): 149 | self.settings_path = "settings.txt" 150 | if os.path.isfile(self.settings_path): 151 | with open(self.settings_path) as f: 152 | self.settings = yaml.safe_load(f) 153 | if self.settings is None: 154 | self.settings = OrderedDict() 155 | else: 156 | self.settings = OrderedDict() 157 | 158 | def save_settings(self): 159 | with open(self.settings_path, "w") as f: 160 | yaml.dump(self.settings, f, default_flow_style=False, Dumper=yaml.Dumper) 161 | 162 | MENU_ACTIONS_THAT_REQUIRE_PROJECT_TO_BE_LOADED = [ 163 | "actionSave_Project", 164 | "actionSave_Project_As", 165 | "actionSave_Editor", 166 | "actionText_Editor", 167 | "actionSprite_Editor", 168 | "actionLayer_BG1", 169 | "actionLayer_BG2", 170 | "actionLayer_BG3", 171 | "actionEntities", 172 | "actionTile_Entities", 173 | "actionExits", 174 | "actionEntity_Search", 175 | "actionTest_Room", 176 | ] 177 | 178 | def disable_menu_actions_when_no_project_loaded(self): 179 | for action_name in self.MENU_ACTIONS_THAT_REQUIRE_PROJECT_TO_BE_LOADED: 180 | getattr(self.ui, action_name).setEnabled(False) 181 | 182 | def enable_menu_actions_when_project_loaded(self): 183 | for action_name in self.MENU_ACTIONS_THAT_REQUIRE_PROJECT_TO_BE_LOADED: 184 | getattr(self.ui, action_name).setEnabled(True) 185 | 186 | def start_new_project(self): 187 | default_dir = None 188 | 189 | rom_path, selected_filter = QFileDialog.getOpenFileName(self, "Select Minish Cap ROM to use as a base", default_dir, "GBA ROM Files (*.gba)") 190 | if not rom_path: 191 | return 192 | 193 | # TODO: verify that the chosen file is actually a USA minish cap rom 194 | 195 | # TODO: if the rom is USA minish cap, but md5 is not vanilla, give a warning (disableable in settings) 196 | 197 | with open(rom_path, "rb") as file: 198 | rom_interface = RomInterface(file.read()) 199 | 200 | self.load_project(rom_interface) 201 | 202 | self.current_project_path = None 203 | 204 | def open_project(self): 205 | default_dir = None 206 | 207 | project_path, selected_filter = QFileDialog.getOpenFileName(self, "Select project", default_dir, "MCEdit Project Files (*.mcproj)") 208 | if not project_path: 209 | return 210 | 211 | self.open_project_by_path(project_path) 212 | 213 | def open_project_by_path(self, project_path): 214 | # TODO: ensure project_path is a real project 215 | 216 | zip = ZipFile(project_path) 217 | 218 | rom_interface = RomInterface(zip.read("rom.gba")) 219 | 220 | self.load_project(rom_interface) 221 | 222 | self.current_project_path = project_path 223 | self.settings["last_used_project"] = self.current_project_path 224 | 225 | def load_project(self, rom_interface): 226 | self.close_open_dialogs() 227 | 228 | self.game = Game(rom_interface) 229 | self.renderer = Renderer(self.game) 230 | 231 | self.initialize_dropdowns() 232 | 233 | self.enable_menu_actions_when_project_loaded() 234 | 235 | def save_project(self): 236 | if self.current_project_path is None: 237 | self.save_project_as() 238 | return 239 | 240 | self.save_any_unsaved_changes_for_all_layers() 241 | 242 | with ZipFile(self.current_project_path, "w") as zip: 243 | zip.writestr("rom.gba", self.game.rom.read_all_bytes()) 244 | 245 | def save_project_as(self): 246 | default_dir = None 247 | 248 | project_path, selected_filter = QFileDialog.getSaveFileName(self, "Save as", default_dir, "MCEdit Project Files (*.mcproj)") 249 | if not project_path: 250 | return 251 | 252 | self.current_project_path = project_path 253 | self.settings["last_used_project"] = self.current_project_path 254 | 255 | self.save_project() 256 | 257 | def initialize_dropdowns(self): 258 | self.ui.area_index.clear() 259 | self.ui.room_index.clear() 260 | for area in self.game.areas: 261 | area_name = AREA_INDEX_TO_NAME[area.area_index] 262 | self.ui.area_index.addItem("%02X %s" % (area.area_index, area_name)) 263 | 264 | try: 265 | if "last_area_index" in self.settings: 266 | area_index = self.settings["last_area_index"] 267 | room_index = self.settings["last_room_index"] 268 | else: 269 | area_index = 0 270 | room_index = 0 271 | self.area_index_changed(area_index, default_room_index=room_index) 272 | except Exception as e: 273 | stack_trace = traceback.format_exc() 274 | error_message = "Error loading map:\n" + str(e) + "\n\n" + stack_trace 275 | print(error_message) 276 | return 277 | 278 | def area_index_changed(self, area_index, skip_loading_room=False, default_room_index=0): 279 | self.area_index = area_index 280 | self.ui.area_index.setCurrentIndex(area_index) 281 | self.ui.room_index.clear() 282 | 283 | self.area = self.game.areas[self.area_index] 284 | 285 | for room_index, room in enumerate(self.area.rooms): 286 | if room is None: 287 | room_text = "%02X INVALID" % room_index 288 | else: 289 | room_text = "%02X %08X %08X" % (room.room_index, room.gfx_metadata_ptr, room.property_list_ptr) 290 | self.ui.room_index.addItem(room_text) 291 | 292 | try: 293 | self.load_map() 294 | except Exception as e: 295 | stack_trace = traceback.format_exc() 296 | error_message = "Error loading map:\n" + str(e) + "\n\n" + stack_trace 297 | print(error_message) 298 | return 299 | 300 | if not skip_loading_room: 301 | self.room_index_changed(default_room_index) 302 | 303 | def room_index_changed(self, room_index): 304 | self.save_any_unsaved_changes_for_all_layers() 305 | 306 | self.room_index = room_index 307 | self.ui.room_index.setCurrentIndex(room_index) 308 | 309 | if room_index >= 0 and room_index < len(self.area.rooms): 310 | self.room = self.area.rooms[room_index] 311 | else: 312 | self.room = None 313 | 314 | self.load_room() 315 | 316 | self.settings["last_area_index"] = self.area_index 317 | self.settings["last_room_index"] = self.room_index 318 | 319 | def change_area_and_room(self, area_index, room_index): 320 | if self.area_index != area_index: 321 | self.area_index_changed(area_index, skip_loading_room=True) 322 | 323 | self.room_index_changed(room_index) 324 | 325 | def change_area_and_room_by_exit(self, ext): 326 | self.change_area_and_room(ext.dest_area, ext.dest_room) 327 | dest_x = ext.dest_x 328 | dest_y = ext.dest_y 329 | if dest_x >= 0x400: 330 | dest_x = self.room.width/2 331 | if dest_y >= 0x400: 332 | dest_y = self.room.height/2 333 | self.ui.room_graphics_view.centerOn(dest_x, dest_y) 334 | 335 | def go_to_room_and_select_entity(self, entity): 336 | if entity.room.area.area_index != self.area.area_index or entity.room.room_index != self.room.room_index: 337 | self.change_area_and_room(entity.room.area.area_index, entity.room.room_index) 338 | self.select_entity(entity) 339 | 340 | def load_room(self): 341 | self.ui.entity_lists_list.clear() 342 | self.room_graphics_scene.clear() 343 | 344 | room_boundaries_item = QGraphicsRectItem(0, 0, self.room.width, self.room.height) 345 | room_boundaries_item.setBrush(QBrush(QColor(200, 200, 200, 255))) 346 | room_boundaries_item.setPen(QPen(QColor(255, 255, 255, 0))) 347 | self.room_graphics_scene.addItem(room_boundaries_item) 348 | 349 | self.selected_tiles_cursor = QGraphicsPixmapItem() 350 | self.selected_tiles_cursor.setZValue(99999999) 351 | self.room_graphics_scene.addItem(self.selected_tiles_cursor) 352 | self.update_selected_tiles_cursor_image() 353 | 354 | self.update_selected_room_on_map() 355 | 356 | self.center_room_view() 357 | 358 | try: 359 | self.renderer.update_curr_room_palettes_and_tilesets(self.room) 360 | 361 | self.bg2_tileset_graphics_scene.update_tileset_image(self.renderer.curr_room_tileset_images[2]) 362 | self.bg1_tileset_graphics_scene.update_tileset_image(self.renderer.curr_room_tileset_images[1]) 363 | except Exception as e: 364 | stack_trace = traceback.format_exc() 365 | error_message = "Error loading room:\n" + str(e) + "\n\n" + stack_trace 366 | print(error_message) 367 | # QMessageBox.warning(self, 368 | # "Error loading room", 369 | # error_message 370 | # ) 371 | 372 | if self.room is None: 373 | return 374 | 375 | try: 376 | self.load_room_layers() 377 | except Exception as e: 378 | stack_trace = traceback.format_exc() 379 | error_message = "Error loading room:\n" + str(e) + "\n\n" + stack_trace 380 | print(error_message) 381 | # QMessageBox.warning(self, 382 | # "Error loading room", 383 | # error_message 384 | # ) 385 | 386 | 387 | try: 388 | self.load_room_entities() 389 | except Exception as e: 390 | stack_trace = traceback.format_exc() 391 | error_message = "Error loading room:\n" + str(e) + "\n\n" + stack_trace 392 | print(error_message) 393 | # QMessageBox.warning(self, 394 | # "Error loading room", 395 | # error_message 396 | # ) 397 | 398 | 399 | self.ui.room_graphics_view.updateSceneRect(self.room_graphics_scene.itemsBoundingRect()) 400 | 401 | self.update_visible_view_items() 402 | 403 | self.update_edit_mode_by_current_tab() 404 | 405 | def load_room_layers(self): 406 | self.layer_bg3_view_item = LayerItem(self.room, 3, self.renderer, self) 407 | self.room_graphics_scene.addItem(self.layer_bg3_view_item) 408 | self.layer_bg2_view_item = LayerItem(self.room, 2, self.renderer, self) 409 | self.room_graphics_scene.addItem(self.layer_bg2_view_item) 410 | self.layer_bg1_view_item = LayerItem(self.room, 1, self.renderer, self) 411 | self.room_graphics_scene.addItem(self.layer_bg1_view_item) 412 | 413 | self.layer_items = [ 414 | None, 415 | self.layer_bg1_view_item, 416 | self.layer_bg2_view_item, 417 | self.layer_bg3_view_item, 418 | ] 419 | 420 | def load_room_entities(self): 421 | self.entities_view_item = EntityLayerItem(self.room.entity_lists, self.renderer) 422 | self.room_graphics_scene.addItem(self.entities_view_item) 423 | 424 | i = 0 425 | self.ui.entity_lists_list.blockSignals(True) 426 | for entity_list, graphics_items in self.entities_view_item.entity_graphics_items_by_entity_list: 427 | list_widget_item = QListWidgetItem("%02X %08X %s" % (i, entity_list.entity_list_ptr, entity_list.name)) 428 | self.ui.entity_lists_list.addItem(list_widget_item) 429 | list_widget_item.setSelected(True) 430 | i += 1 431 | self.ui.entity_lists_list.blockSignals(False) 432 | 433 | self.tile_entities_view_item = QGraphicsRectItem() 434 | self.room_graphics_scene.addItem(self.tile_entities_view_item) 435 | for tile_entity in self.room.tile_entities: 436 | entity_item = EntityRectItem(tile_entity, "tile_entity") 437 | entity_item.setParentItem(self.tile_entities_view_item) 438 | 439 | self.exits_view_item = QGraphicsRectItem() 440 | self.room_graphics_scene.addItem(self.exits_view_item) 441 | for ext in self.room.exits: 442 | entity_item = EntityRectItem(ext, "exit") 443 | entity_item.setParentItem(self.exits_view_item) 444 | 445 | for regions in self.room.exit_region_lists: 446 | for region in regions: 447 | entity_item = EntityRectItem(region, "exit_region") 448 | entity_item.setParentItem(self.exits_view_item) 449 | 450 | self.select_entity_graphics_item(None) 451 | 452 | def show_all_entities(self): 453 | self.entities_view_item.show() 454 | self.tile_entities_view_item.show() 455 | self.exits_view_item.show() 456 | 457 | def hide_all_entities(self): 458 | self.entities_view_item.hide() 459 | self.tile_entities_view_item.hide() 460 | self.exits_view_item.hide() 461 | 462 | def center_room_view(self): 463 | if self.room is not None: 464 | self.ui.room_graphics_view.centerOn(self.room.width/2, self.room.height/2) 465 | 466 | def room_clicked(self, x, y, button): 467 | graphics_item = self.room_graphics_scene.itemAt(x, y) 468 | if graphics_item is None: 469 | self.select_entity_graphics_item(None) 470 | return 471 | 472 | if isinstance(graphics_item, EntityRectItem) or isinstance(graphics_item, EntityImageItem): 473 | if button == Qt.LeftButton: 474 | self.select_entity_graphics_item(graphics_item) 475 | elif button == Qt.RightButton and graphics_item.entity_class == "exit": 476 | # Go through the exit into the destination room. 477 | self.change_area_and_room_by_exit(graphics_item.entity) 478 | elif button == Qt.RightButton and graphics_item.entity_class == "exit_region": 479 | # Go through the exit into the destination room. 480 | self.change_area_and_room_by_exit(graphics_item.entity.exit) 481 | else: 482 | self.select_entity_graphics_item(None) 483 | 484 | def mouse_moved_on_room(self, x, y, button): 485 | if button == Qt.LeftButton: 486 | self.layer_clicked(x, y, button) 487 | 488 | self.update_selected_tiles_cursor_position(x, y, button) 489 | 490 | 491 | def update_edit_mode_by_current_tab(self): 492 | if self.room is None: 493 | return 494 | 495 | if self.ui.right_sidebar.currentIndex() == 0: 496 | self.enter_entity_edit_mode() 497 | elif self.ui.right_sidebar.currentIndex() == 1: 498 | self.enter_bg2_layer_edit_mode() 499 | elif self.ui.right_sidebar.currentIndex() == 2: 500 | self.enter_bg1_layer_edit_mode() 501 | 502 | def enter_entity_edit_mode(self): 503 | self.ui.right_sidebar.setCurrentIndex(0) 504 | 505 | self.selected_tileset_graphics_scene = None 506 | self.selected_layer_index = None 507 | self.update_selected_tiles_cursor_image() 508 | self.show_all_entities() 509 | 510 | for layer_item in self.layer_items: 511 | if layer_item is None: 512 | continue 513 | layer_item.setOpacity(1.0) 514 | 515 | def enter_bg2_layer_edit_mode(self): 516 | self.enter_layer_edit_mode_by_layer_index(2) 517 | 518 | def enter_bg1_layer_edit_mode(self): 519 | self.enter_layer_edit_mode_by_layer_index(1) 520 | 521 | def enter_layer_edit_mode_by_layer_index(self, layer_index): 522 | if layer_index == 2: 523 | self.selected_tileset_graphics_scene = self.bg2_tileset_graphics_scene 524 | self.ui.right_sidebar.setCurrentIndex(1) 525 | elif layer_index == 1: 526 | self.selected_tileset_graphics_scene = self.bg1_tileset_graphics_scene 527 | self.ui.right_sidebar.setCurrentIndex(2) 528 | 529 | self.update_selected_tiles_cursor_image() 530 | 531 | self.selected_layer_index = layer_index 532 | self.hide_all_entities() 533 | 534 | for layer_item in self.layer_items: 535 | if layer_item is None: 536 | continue 537 | if layer_item.layer_index == layer_index: 538 | layer_item.setOpacity(1.0) 539 | else: 540 | layer_item.setOpacity(0.5) 541 | 542 | def update_selected_tiles_cursor_image(self): 543 | if self.selected_tiles_cursor is None: 544 | return 545 | 546 | if self.selected_tileset_graphics_scene is None: 547 | self.selected_tiles_cursor.hide() 548 | else: 549 | pixmap = self.selected_tileset_graphics_scene.get_selection_as_pixmap() 550 | self.selected_tiles_cursor.setPixmap(pixmap) 551 | self.selected_tiles_cursor.show() 552 | 553 | def update_selected_tiles_cursor_position(self, x, y, button): 554 | if self.selected_tiles_cursor is None: 555 | return 556 | 557 | if self.selected_tileset_graphics_scene is None: 558 | self.selected_tiles_cursor.hide() 559 | elif x < 0 or y < 0 or x >= self.room.width or y >= self.room.height: 560 | self.selected_tiles_cursor.hide() 561 | else: 562 | self.selected_tiles_cursor.setPos(x//0x10*0x10, y//0x10*0x10) 563 | self.selected_tiles_cursor.show() 564 | 565 | def layer_clicked(self, x, y, button): 566 | if self.selected_layer_index is not None: 567 | layer_item = self.layer_items[self.selected_layer_index] 568 | layer_item.layer_clicked(x, y, button) 569 | return 570 | 571 | def save_any_unsaved_changes_for_all_layers(self): 572 | if self.room is None: 573 | return 574 | 575 | try: 576 | for layer_index in range(4): 577 | print(layer_index) 578 | layer = self.room.layers_asset_list.layers[layer_index] 579 | if layer is not None: 580 | layer.save_any_unsaved_changes() 581 | except Exception as e: 582 | QMessageBox.warning(self, 583 | "Error saving layer changes", 584 | str(e) 585 | ) 586 | 587 | 588 | def load_map(self): 589 | self.map_graphics_scene.clear() 590 | 591 | self.selected_room_graphics_item = None 592 | 593 | if self.area.is_dungeon: 594 | dungeon = self.game.dungeons[self.area.dungeon_index] 595 | map_image = self.renderer.render_dungeon_map(dungeon) 596 | elif self.area.is_overworld: 597 | map_image = self.renderer.render_world_map() 598 | else: 599 | map_image = self.renderer.render_dummy_map(self.area) 600 | 601 | data = map_image.tobytes('raw', 'BGRA') 602 | qimage = QImage(data, map_image.size[0], map_image.size[1], QImage.Format_ARGB32) 603 | pixmap = QPixmap.fromImage(qimage) 604 | 605 | map_graphics_item = QGraphicsPixmapItem(pixmap) 606 | self.map_graphics_scene.addItem(map_graphics_item) 607 | 608 | self.selected_room_graphics_item = QGraphicsRectItem() 609 | self.selected_room_graphics_item.setPen(QPen(QColor(220, 0, 0, 255))) 610 | self.selected_room_graphics_item.setRect(0, 0, 0, 0) 611 | self.map_graphics_scene.addItem(self.selected_room_graphics_item) 612 | 613 | self.map_graphics_scene.setSceneRect(self.map_graphics_scene.itemsBoundingRect()) 614 | 615 | def map_clicked(self, x, y, button): 616 | if button == Qt.LeftButton: 617 | if self.area.is_overworld: 618 | areas_to_check = [ 619 | area for area in self.game.areas 620 | if area.is_overworld 621 | and not area.area_index in [0x15] 622 | ] 623 | elif self.area.is_dungeon: 624 | areas_to_check = [ 625 | area for area in self.game.areas 626 | if area.is_dungeon and area.dungeon_index == self.area.dungeon_index 627 | and not area.area_index in [0x5F, 0x71, 0x77] 628 | ] 629 | else: 630 | areas_to_check = [self.area] 631 | 632 | for area in areas_to_check: 633 | for room in area.rooms: 634 | if room is None: 635 | continue 636 | 637 | if self.area.is_overworld: 638 | room_x = room.x_pos / 0x19 639 | room_y = room.y_pos / 0x19 640 | room_w = room.width / 0x19 641 | room_h = room.height / 0x19 642 | else: 643 | room_x = room.x_pos / 0x10 644 | room_y = room.y_pos / 0x10 645 | room_w = room.width / 0x10 646 | room_h = room.height / 0x10 647 | 648 | if x >= room_x and y >= room_y and x < room_x+room_w and y < room_y+room_h: 649 | # Go into the clicked room. 650 | self.change_area_and_room(area.area_index, room.room_index) 651 | break 652 | 653 | def update_selected_room_on_map(self): 654 | if self.selected_room_graphics_item is None: 655 | return 656 | 657 | old_rect = self.selected_room_graphics_item.rect() 658 | 659 | if self.room is None: 660 | x = 0 661 | y = 0 662 | w = 0 663 | h = 0 664 | elif self.area.is_overworld: 665 | x = self.room.x_pos/0x19 666 | y = self.room.y_pos/0x19 667 | w = self.room.width/0x19-1 668 | h = self.room.height/0x19-1 669 | else: 670 | x = self.room.x_pos/0x10 671 | y = self.room.y_pos/0x10 672 | w = self.room.width/0x10-1 673 | h = self.room.height/0x10-1 674 | 675 | self.selected_room_graphics_item.setRect( 676 | x, y, w, h 677 | ) 678 | 679 | self.map_graphics_scene.setSceneRect(self.map_graphics_scene.itemsBoundingRect()) 680 | 681 | if w != 0: 682 | center_x = x + w/2 683 | center_y = y + h/2 684 | self.ui.map_graphics_view.centerOn(center_x, center_y) 685 | self.map_graphics_scene.invalidate(old_rect) 686 | 687 | def update_visible_view_items(self): 688 | self.layer_bg1_view_item.setVisible(self.ui.actionLayer_BG1.isChecked()) 689 | self.layer_bg2_view_item.setVisible(self.ui.actionLayer_BG2.isChecked()) 690 | self.layer_bg3_view_item.setVisible(self.ui.actionLayer_BG3.isChecked()) 691 | self.entities_view_item.setVisible(self.ui.actionEntities.isChecked()) 692 | self.tile_entities_view_item.setVisible(self.ui.actionTile_Entities.isChecked()) 693 | self.exits_view_item.setVisible(self.ui.actionExits.isChecked()) 694 | 695 | def entity_list_visibility_toggled(self): 696 | for entity_list_index in range(self.ui.entity_lists_list.count()): 697 | list_widget_item = self.ui.entity_lists_list.item(entity_list_index) 698 | entity_list, graphics_items = self.entities_view_item.entity_graphics_items_by_entity_list[entity_list_index] 699 | for entity_item in graphics_items: 700 | entity_item.setVisible(list_widget_item.isSelected()) 701 | 702 | def select_entity_graphics_item(self, entity_graphics_item): 703 | if entity_graphics_item: 704 | for other_entity_graphics_item in self.entities_view_item.childItems(): 705 | other_entity_graphics_item.setSelected(False) 706 | 707 | entity_graphics_item.setSelected(True) 708 | 709 | self.ui.entity_properies.select_entity_graphics_item(entity_graphics_item) 710 | 711 | def select_entity(self, entity): 712 | entity_graphics_item = next(( 713 | egi for egi in self.entities_view_item.childItems() 714 | if egi.entity == entity 715 | ), None) 716 | if entity_graphics_item is None: 717 | return 718 | self.select_entity_graphics_item(entity_graphics_item) 719 | self.ui.room_graphics_view.centerOn(entity_graphics_item) 720 | 721 | 722 | def close_open_dialogs(self): 723 | for dialog in self.open_dialogs: 724 | dialog.close() 725 | self.open_dialogs = [] 726 | 727 | def open_settings(self): 728 | dialog = SettingsDialog(self) 729 | self.open_dialogs.append(dialog) 730 | 731 | def open_entity_search(self): 732 | entity_search_dialog = EntitySearchDialog(self) 733 | self.open_dialogs.append(entity_search_dialog) 734 | 735 | def open_save_editor(self): 736 | dialog = SaveEditorDialog(self) 737 | self.open_dialogs.append(dialog) 738 | 739 | def open_text_editor(self): 740 | dialog = TextEditorDialog(self) 741 | self.open_dialogs.append(dialog) 742 | 743 | def open_sprite_editor(self): 744 | dialog = SpriteEditorDialog(self) 745 | self.open_dialogs.append(dialog) 746 | 747 | 748 | def test_room(self): 749 | if self.current_project_path is None: 750 | QMessageBox.warning(self, 751 | "Project not saved", 752 | "Cannot test run on an unsaved project. Save the project and try again." 753 | ) 754 | # TODO: figure out where to put the test room rom when project is unsaved to fix this limitation 755 | return 756 | 757 | if self.settings.get("emulator_path") is None: 758 | QMessageBox.warning(self, 759 | "Emulator path not set", 760 | "Must set emulator path in the settings before running test room." 761 | ) 762 | return 763 | emulator_path = self.settings["emulator_path"] 764 | 765 | if self.settings.get("test_room_save_slot_index", None) not in [0, 1, 2]: 766 | self.settings["test_room_save_slot_index"] = 0 767 | 768 | self.save_any_unsaved_changes_for_all_layers() 769 | 770 | # Kill the last running emulator process if the user didn't do it manually. 771 | if self.last_emulator_process is not None: 772 | if self.last_emulator_process.is_running(): 773 | self.last_emulator_process.kill() 774 | self.last_emulator_process = None 775 | 776 | # Apply the test room patch and set where to load the player at. 777 | scene_pos = self.ui.room_graphics_view.mapToScene(self.ui.room_graphics_view.mapFromGlobal(QCursor.pos())) 778 | test_rom = self.game.rom.copy() 779 | self.game.apply_patch("test_room", rom=test_rom) 780 | sym = self.game.custom_symbols["test_room_data"] 781 | test_rom.write_u8(sym+0, self.settings["test_room_save_slot_index"]) 782 | test_rom.write_u8(sym+1, self.area_index) 783 | test_rom.write_u8(sym+2, self.room_index) 784 | test_rom.write_u16(sym+4, scene_pos.x()) 785 | test_rom.write_u16(sym+6, scene_pos.y()) 786 | 787 | # Write the test ROM. 788 | output_dir = os.path.dirname(self.current_project_path) 789 | project_basename, file_ext = os.path.splitext(os.path.basename(self.current_project_path)) 790 | output_rom_basename = project_basename + " Test" 791 | output_rom_path = os.path.join(output_dir, output_rom_basename + ".gba") 792 | output_rom_path = os.path.abspath(output_rom_path) 793 | output_rom_path = output_rom_path.replace("/", "\\") 794 | with open(output_rom_path, "wb") as f: 795 | f.write(test_rom.read_all_bytes()) 796 | 797 | # Copy the symbol map file to be next to the test room ROM so that No$GBA can load it. 798 | input_map_path = os.path.join(DATA_PATH, "symbol_map.sym") 799 | with open(input_map_path) as f: 800 | symbol_map = f.read() 801 | output_map_path = os.path.join(output_dir, output_rom_basename + ".sym") 802 | with open(output_map_path, "w") as f: 803 | f.write(symbol_map) 804 | 805 | # Launch the emulator. 806 | popen_process = subprocess.Popen([emulator_path, output_rom_path]) 807 | self.last_emulator_process = psutil.Process(popen_process.pid) 808 | 809 | 810 | def keyPressEvent(self, event): 811 | if event.key() == Qt.Key_Escape: 812 | self.close() 813 | 814 | def closeEvent(self, event): 815 | #cancelled = self.confirm_discard_changes() 816 | #if cancelled: 817 | # event.ignore() 818 | # return 819 | 820 | for dialog in self.open_dialogs: 821 | dialog.close() 822 | 823 | self.save_settings() 824 | --------------------------------------------------------------------------------