├── 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 |
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 |
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 |
--------------------------------------------------------------------------------