class SpinnerPanel(QWidget): object_change: SignalInstance = Signal(int) zoom_in_triggered: SignalInstance = Signal() zoom_out_triggered: SignalInstance = Signal() def __init__(self, parent: Optional[QWidget], level_ref: LevelRef): super(SpinnerPanel, self).__init__(parent) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.level_ref = level_ref self.level_ref.data_changed.connect(self.update) self.spin_domain = Spinner(self, maximum=MAX_DOMAIN) self.spin_domain.setEnabled(False) self.spin_domain.valueChanged.connect(self.object_change.emit) self.spin_type = Spinner(self, maximum=MAX_TYPE) self.spin_type.setEnabled(False) self.spin_type.valueChanged.connect(self.object_change.emit) self.spin_length = Spinner(self, maximum=MAX_LENGTH) self.spin_length.setEnabled(False) self.spin_length.valueChanged.connect(self.object_change.emit) spinner_layout = QFormLayout() spinner_layout.addRow("Bank/Domain:", self.spin_domain) spinner_layout.addRow("Index:", self.spin_type) spinner_layout.addRow("Length:", self.spin_length) self.setLayout(spinner_layout) self.setWhatsThis( "<b>Spinner Panel</b><br/>" "The Spinner Panel gives raw byte access to objects for advanced users. The values are shown " "in hexadecimal notation.<br/>" "Level objects and enemies/items are categorized using domains and indexes. Which domain an " "object is in, doesn't hold much information about the object, if at all.<br/>" "As for the index, the only important information is, that all objects from 0x00 - 0x0F can " "not be resized. " "They have fixed dimensions, like the background bushes in Level 1-1.<br/>" "All other objects have 16 different iterations, meaning 0x10 - 0x1F, for example, is one " "object, with 16 different sizes, going from smallest to largest. In what way these objects " "expand, depends on their particular expansion type.<br/>" "Some '4-byte' objects can expand in a second way, since they have an additional byte " "holding that information. For example a platform, which can be sized vertically using the " "index and horizontally using the 4th byte.") def update(self): if len(self.level_ref.selected_objects) == 1: selected_object = self.level_ref.selected_objects[0] if isinstance(selected_object, ObjectLike): self._populate_spinners(selected_object) else: self.disable_all() super(SpinnerPanel, self).update() def _populate_spinners(self, obj: ObjectLike): self.blockSignals(True) self.set_type(obj.obj_index) self.enable_domain(isinstance(obj, LevelObject), obj.domain) if isinstance(obj, LevelObject) and obj.is_4byte: self.set_length(obj.length) else: self.enable_length(False) self.blockSignals(False) def get_type(self): return self.spin_type.value() def set_type(self, object_type: int): self.spin_type.setValue(object_type) self.spin_type.setEnabled(True) def get_domain(self): return self.spin_domain.value() def set_domain(self, domain: int): self.spin_domain.setValue(domain) self.spin_domain.setEnabled(True) def get_length(self) -> int: return self.spin_length.value() def set_length(self, length: int): self.spin_length.setValue(length) self.spin_length.setEnabled(True) def enable_type(self, enable: bool, value: int = 0): self.spin_type.setValue(value) self.spin_type.setEnabled(enable) def enable_domain(self, enable: bool, value: int = 0): self.spin_domain.setValue(value) self.spin_domain.setEnabled(enable) def enable_length(self, enable: bool, value: int = 0): self.spin_length.setValue(value) self.spin_length.setEnabled(enable) def clear_spinners(self): self.set_type(0x00) self.set_domain(0x00) self.set_length(0x00) def disable_all(self): self.blockSignals(True) self.clear_spinners() self.enable_type(False) self.enable_domain(False) self.enable_length(False) self.blockSignals(False)
class SpinnerPanel(QWidget): object_change: SignalInstance = Signal(int) zoom_in_triggered: SignalInstance = Signal() zoom_out_triggered: SignalInstance = Signal() def __init__(self, parent: Optional[QWidget], level_ref: LevelRef): super(SpinnerPanel, self).__init__(parent) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.level_ref = level_ref self.level_ref.data_changed.connect(self.update) self.spin_domain = Spinner(self, maximum=MAX_DOMAIN) self.spin_domain.setEnabled(False) self.spin_domain.valueChanged.connect(self.object_change.emit) self.spin_type = Spinner(self, maximum=MAX_TYPE) self.spin_type.setEnabled(False) self.spin_type.valueChanged.connect(self.object_change.emit) self.spin_length = Spinner(self, maximum=MAX_LENGTH) self.spin_length.setEnabled(False) self.spin_length.valueChanged.connect(self.object_change.emit) spinner_layout = QFormLayout() spinner_layout.addRow("Bank/Domain:", self.spin_domain) spinner_layout.addRow("Type:", self.spin_type) spinner_layout.addRow("Length:", self.spin_length) self.setLayout(spinner_layout) def update(self): if len(self.level_ref.selected_objects) == 1: selected_object = self.level_ref.selected_objects[0] if isinstance(selected_object, ObjectLike): self._populate_spinners(selected_object) else: self.disable_all() super(SpinnerPanel, self).update() def _populate_spinners(self, obj: ObjectLike): self.blockSignals(True) self.set_type(obj.obj_index) self.enable_domain(isinstance(obj, LevelObject), obj.domain) if isinstance(obj, LevelObject) and obj.is_4byte: self.set_length(obj.length) else: self.enable_length(False) self.blockSignals(False) def get_type(self): return self.spin_type.value() def set_type(self, object_type: int): self.spin_type.setValue(object_type) self.spin_type.setEnabled(True) def get_domain(self): return self.spin_domain.value() def set_domain(self, domain: int): self.spin_domain.setValue(domain) self.spin_domain.setEnabled(True) def get_length(self) -> int: return self.spin_length.value() def set_length(self, length: int): self.spin_length.setValue(length) self.spin_length.setEnabled(True) def enable_type(self, enable: bool, value: int = 0): self.spin_type.setValue(value) self.spin_type.setEnabled(enable) def enable_domain(self, enable: bool, value: int = 0): self.spin_domain.setValue(value) self.spin_domain.setEnabled(enable) def enable_length(self, enable: bool, value: int = 0): self.spin_length.setValue(value) self.spin_length.setEnabled(enable) def clear_spinners(self): self.set_type(0x00) self.set_domain(0x00) self.set_length(0x00) def disable_all(self): self.blockSignals(True) self.clear_spinners() self.enable_type(False) self.enable_domain(False) self.enable_length(False) self.blockSignals(False)
class LevelSelector(QDialog): def __init__(self, parent): super(LevelSelector, self).__init__(parent) self.setWindowTitle("Level Selector") self.setModal(True) self.selected_world = 1 self.selected_level = 1 self.object_set = 0 self.object_data_offset = 0x0 self.enemy_data_offset = 0x0 self.world_label = QLabel(parent=self, text="World") self.world_list = QListWidget(parent=self) self.world_list.addItems(WORLD_ITEMS) self.world_list.itemDoubleClicked.connect(self.on_ok) self.world_list.itemSelectionChanged.connect(self.on_world_click) self.level_label = QLabel(parent=self, text="Level") self.level_list = QListWidget(parent=self) self.level_list.itemDoubleClicked.connect(self.on_ok) self.level_list.itemSelectionChanged.connect(self.on_level_click) self.enemy_data_label = QLabel(parent=self, text="Enemy Data") self.enemy_data_spinner = Spinner(parent=self) self.object_data_label = QLabel(parent=self, text="Object Data") self.object_data_spinner = Spinner(self) self.object_set_label = QLabel(parent=self, text="Object Set") self.object_set_dropdown = QComboBox(self) self.object_set_dropdown.addItems(OBJECT_SET_ITEMS) self.button_ok = QPushButton("Ok", self) self.button_ok.clicked.connect(self.on_ok) self.button_cancel = QPushButton("Cancel", self) self.button_cancel.clicked.connect(self.close) self.window_layout = QGridLayout(self) self.window_layout.addWidget(self.world_label, 0, 0) self.window_layout.addWidget(self.level_label, 0, 1) self.window_layout.addWidget(self.world_list, 1, 0) self.window_layout.addWidget(self.level_list, 1, 1) self.window_layout.addWidget(self.enemy_data_label, 2, 0) self.window_layout.addWidget(self.object_data_label, 2, 1) self.window_layout.addWidget(self.enemy_data_spinner, 3, 0) self.window_layout.addWidget(self.object_data_spinner, 3, 1) self.window_layout.addWidget(self.object_set_label, 4, 0) self.window_layout.addWidget(self.object_set_dropdown, 4, 1) self.window_layout.addWidget(self.button_ok, 5, 0) self.window_layout.addWidget(self.button_cancel, 5, 1) self.setLayout(self.window_layout) self.world_list.setCurrentRow(1) # select Level 1-1 self.on_world_click() def keyPressEvent(self, key_event: QKeyEvent): if key_event.key() == Qt.Key_Escape: self.reject() def on_world_click(self): index = self.world_list.currentRow() assert index >= 0 self.level_list.clear() # skip first meaningless item for level in Level.offsets[1:]: if level.game_world == index: if level.name: self.level_list.addItem(level.name) if self.level_list.count(): self.level_list.setCurrentRow(0) self.on_level_click() def on_level_click(self): index = self.level_list.currentRow() assert index >= 0 self.selected_world = self.world_list.currentRow() self.selected_level = index + 1 level_is_overworld = self.selected_world == OVERWORLD_MAPS_INDEX if level_is_overworld: level_array_offset = self.selected_level else: level_array_offset = Level.world_indexes[self.selected_world] + self.selected_level object_data_for_lvl = Level.offsets[level_array_offset].rom_level_offset if not level_is_overworld: object_data_for_lvl -= Level.HEADER_LENGTH self.object_data_spinner.setValue(object_data_for_lvl) if not level_is_overworld: enemy_data_for_lvl = Level.offsets[level_array_offset].enemy_offset else: enemy_data_for_lvl = 0 if enemy_data_for_lvl > 0: # data in look up table is off by one, since workshop ignores the first byte enemy_data_for_lvl -= 1 self.enemy_data_spinner.setValue(enemy_data_for_lvl) self.enemy_data_spinner.setEnabled(not level_is_overworld) # if self.selected_world >= WORLD_1_INDEX: object_set_index = Level.offsets[level_array_offset].real_obj_set self.object_set_dropdown.setCurrentIndex(object_set_index) self.button_ok.setDisabled(self.selected_world == 0) def on_ok(self, _): if self.selected_world == 0: return self.object_set = self.object_set_dropdown.currentIndex() self.object_data_offset = self.object_data_spinner.value() # skip the first byte, because it seems useless self.enemy_data_offset = self.enemy_data_spinner.value() + 1 self.accept() def closeEvent(self, _close_event: QCloseEvent): self.reject()
class ObjectViewer(CustomChildWindow): def __init__(self, parent): super(ObjectViewer, self).__init__(parent, title="Object Viewer") self.spin_domain = Spinner(self, MAX_DOMAIN) self.spin_domain.valueChanged.connect(self.on_spin) self.spin_type = Spinner(self, MAX_TYPE) self.spin_type.valueChanged.connect(self.on_spin) self.spin_length = Spinner(self, MAX_LENGTH) self.spin_length.setDisabled(True) self.spin_length.valueChanged.connect(self.on_spin) _toolbar = QToolBar(self) _toolbar.addWidget(self.spin_domain) _toolbar.addWidget(self.spin_type) _toolbar.addWidget(self.spin_length) self.object_set_dropdown = QComboBox(_toolbar) self.object_set_dropdown.addItems(OBJECT_SET_ITEMS[1:]) self.object_set_dropdown.setCurrentIndex(0) self.graphic_set_dropdown = QComboBox(_toolbar) self.graphic_set_dropdown.addItems(GRAPHIC_SET_NAMES) self.graphic_set_dropdown.setCurrentIndex(1) self.object_set_dropdown.currentIndexChanged.connect( self.on_object_set) self.graphic_set_dropdown.currentIndexChanged.connect( self.on_graphic_set) _toolbar.addWidget(self.object_set_dropdown) _toolbar.addWidget(self.graphic_set_dropdown) self.addToolBar(_toolbar) self.drawing_area = ObjectDrawArea(self, 1) self.status_bar = QStatusBar(parent=self) self.status_bar.showMessage(self.drawing_area.current_object.name) self.setStatusBar(self.status_bar) self.drawing_area.update() self.block_list = BlockArray(self, self.drawing_area.current_object) central_widget = QWidget() central_widget.setLayout(QVBoxLayout()) central_widget.layout().addWidget(self.drawing_area) central_widget.layout().addWidget(self.block_list) self.setCentralWidget(central_widget) self.layout().setSizeConstraint(QLayout.SetFixedSize) return def set_object_and_graphic_set(self, object_set: int, graphics_set: int): self.object_set_dropdown.setCurrentIndex(object_set - 1) self.graphic_set_dropdown.setCurrentIndex(graphics_set) self.drawing_area.change_object_set(object_set) self.drawing_area.change_graphic_set(graphics_set) self.block_list.update_object(self.drawing_area.current_object) self.status_bar.showMessage(self.drawing_area.current_object.name) def on_object_set(self): object_set = self.object_set_dropdown.currentIndex() + 1 graphics_set = object_set self.set_object_and_graphic_set(object_set, graphics_set) def on_graphic_set(self): object_set = self.object_set_dropdown.currentIndex() + 1 graphics_set = self.graphic_set_dropdown.currentIndex() self.set_object_and_graphic_set(object_set, graphics_set) def set_object(self, domain: int, obj_index: int, secondary_length: int): object_data = bytearray(4) object_data[0] = domain << 5 object_data[1] = 0 object_data[2] = obj_index object_data[3] = secondary_length self.spin_domain.setValue(domain) self.spin_type.setValue(obj_index) self.spin_length.setValue(secondary_length) self.drawing_area.update_object(object_data) self.block_list.update_object(self.drawing_area.current_object) if self.drawing_area.current_object.is_4byte: self.spin_length.setEnabled(True) else: self.spin_length.setValue(0) self.spin_length.setEnabled(False) self.drawing_area.update() self.status_bar.showMessage(self.drawing_area.current_object.name) def on_spin(self, _): domain = self.spin_domain.value() obj_index = self.spin_type.value() secondary_length = self.spin_length.value() self.set_object(domain, obj_index, secondary_length)
class ObjectViewer(CustomChildWindow): def __init__(self, parent): super(ObjectViewer, self).__init__(parent, title="Object Viewer") self.spin_domain = Spinner(self, MAX_DOMAIN) self.spin_domain.valueChanged.connect(self.on_spin) self.spin_type = Spinner(self, MAX_TYPE) self.spin_type.valueChanged.connect(self.on_spin) self.spin_length = Spinner(self, MAX_LENGTH) self.spin_length.setDisabled(True) self.spin_length.valueChanged.connect(self.on_spin) _toolbar = QToolBar(self) _toolbar.addWidget(self.spin_domain) _toolbar.addWidget(self.spin_type) _toolbar.addWidget(self.spin_length) self.object_set_dropdown = QComboBox(_toolbar) self.object_set_dropdown.addItems(OBJECT_SET_ITEMS[1:]) self.object_set_dropdown.setCurrentIndex(0) self.graphic_set_dropdown = QComboBox(_toolbar) self.graphic_set_dropdown.addItems( [f"Graphics Set {gfx_set}" for gfx_set in range(32)]) self.graphic_set_dropdown.setCurrentIndex(1) self.object_set_dropdown.currentIndexChanged.connect( self.on_object_set) self.graphic_set_dropdown.currentIndexChanged.connect( self.on_graphic_set) _toolbar.addWidget(self.object_set_dropdown) _toolbar.addWidget(self.graphic_set_dropdown) self.addToolBar(_toolbar) self.object_set = 1 self.drawing_area = ObjectDrawArea(self, self.object_set) self.status_bar = QStatusBar(parent=self) self.status_bar.showMessage( self.drawing_area.current_object.description) self.setStatusBar(self.status_bar) self.setCentralWidget(self.drawing_area) self.drawing_area.update() self.layout().setSizeConstraint(QLayout.SetFixedSize) return def on_object_set(self): self.object_set = self.object_set_dropdown.currentIndex() + 1 gfx_set = self.object_set self.graphic_set_dropdown.setCurrentIndex(gfx_set) self.drawing_area.change_object_set(self.object_set) self.drawing_area.change_graphic_set(gfx_set) self.status_bar.showMessage( self.drawing_area.current_object.description) def on_graphic_set(self): gfx_set = self.graphic_set_dropdown.currentIndex() self.drawing_area.change_graphic_set(gfx_set) self.status_bar.showMessage( self.drawing_area.current_object.description) def on_spin(self, _): object_data = bytearray(4) object_data[0] = self.spin_domain.value() << 5 object_data[1] = 0 object_data[2] = self.spin_type.value() object_data[3] = self.spin_length.value() self.drawing_area.update_object(object_data) if self.drawing_area.current_object.is_4byte: self.spin_length.setEnabled(True) else: self.spin_length.setValue(0) self.spin_length.setEnabled(False) self.drawing_area.update() self.status_bar.showMessage( self.drawing_area.current_object.description)
class LevelSelector(QDialog): def __init__(self, parent): super(LevelSelector, self).__init__(parent) self.setWindowTitle("Level Selector") self.setModal(True) self.level_name = "" self.object_set = 0 self.object_data_offset = 0x0 self.enemy_data_offset = 0x0 self.world_label = QLabel(parent=self, text="World") self.world_list = QListWidget(parent=self) self.world_list.addItems(WORLD_ITEMS) self.world_list.itemDoubleClicked.connect(self.on_ok) self.world_list.itemSelectionChanged.connect(self.on_world_click) self.level_label = QLabel(parent=self, text="Level") self.level_list = QListWidget(parent=self) self.level_list.itemDoubleClicked.connect(self.on_ok) self.level_list.itemSelectionChanged.connect(self.on_level_click) self.enemy_data_label = QLabel(parent=self, text="Enemy Data") self.enemy_data_spinner = Spinner(parent=self) self.object_data_label = QLabel(parent=self, text="Object Data") self.object_data_spinner = Spinner(self) self.object_set_label = QLabel(parent=self, text="Object Set") self.object_set_dropdown = QComboBox(self) self.object_set_dropdown.addItems(OBJECT_SET_ITEMS) self.button_ok = QPushButton("Ok", self) self.button_ok.clicked.connect(self.on_ok) self.button_cancel = QPushButton("Cancel", self) self.button_cancel.clicked.connect(self.close) stock_level_widget = QWidget() stock_level_layout = QGridLayout(stock_level_widget) stock_level_layout.addWidget(self.world_label, 0, 0) stock_level_layout.addWidget(self.level_label, 0, 1) stock_level_layout.addWidget(self.world_list, 1, 0) stock_level_layout.addWidget(self.level_list, 1, 1) self.source_selector = QTabWidget() self.source_selector.addTab(stock_level_widget, "Stock Levels") for world_number in range(WORLD_COUNT): world_number += 1 world_map_select = WorldMapLevelSelect(world_number) world_map_select.level_selected.connect( self._on_level_selected_via_world_map) self.source_selector.addTab(world_map_select, f"World {world_number}") data_layout = QGridLayout() data_layout.addWidget(self.enemy_data_label, 0, 0) data_layout.addWidget(self.object_data_label, 0, 1) data_layout.addWidget(self.enemy_data_spinner, 1, 0) data_layout.addWidget(self.object_data_spinner, 1, 1) data_layout.addWidget(self.object_set_label, 2, 0) data_layout.addWidget(self.object_set_dropdown, 2, 1) data_layout.addWidget(self.button_ok, 3, 0) data_layout.addWidget(self.button_cancel, 3, 1) main_layout = QVBoxLayout() main_layout.addWidget(self.source_selector) main_layout.addLayout(data_layout) self.setLayout(main_layout) self.world_list.setCurrentRow(1) # select Level 1-1 self.on_world_click() def keyPressEvent(self, key_event: QKeyEvent): if key_event.key() == Qt.Key_Escape: self.reject() def on_world_click(self): index = self.world_list.currentRow() assert index >= 0 self.level_list.clear() # skip first meaningless item for level in Level.offsets[1:]: if level.game_world == index: if level.name: self.level_list.addItem(level.name) if self.level_list.count(): self.level_list.setCurrentRow(0) self.on_level_click() def on_level_click(self): index = self.level_list.currentRow() assert index >= 0 level_is_overworld = self.world_list.currentRow( ) == OVERWORLD_MAPS_INDEX if level_is_overworld: level_array_offset = index + 1 self.level_name = "" else: level_array_offset = Level.world_indexes[ self.world_list.currentRow()] + index + 1 self.level_name = f"World {self.world_list.currentRow()}, " self.level_name += f"{Level.offsets[level_array_offset].name}" object_data_for_lvl = Level.offsets[ level_array_offset].rom_level_offset if not level_is_overworld: object_data_for_lvl -= Level.HEADER_LENGTH if not level_is_overworld: enemy_data_for_lvl = Level.offsets[level_array_offset].enemy_offset else: enemy_data_for_lvl = 0 if enemy_data_for_lvl > 0: # data in look up table is off by one, since workshop ignores the first byte enemy_data_for_lvl -= 1 self.enemy_data_spinner.setEnabled(not level_is_overworld) # if self.world_list.currentRow() >= WORLD_1_INDEX: object_set_index = Level.offsets[level_array_offset].real_obj_set self.button_ok.setDisabled(level_is_overworld) self._fill_in_data(object_set_index, object_data_for_lvl, enemy_data_for_lvl) def _fill_in_data(self, object_set: int, layout_address: int, enemy_address: int): self.object_set_dropdown.setCurrentIndex(object_set) self.object_data_spinner.setValue(layout_address) self.enemy_data_spinner.setValue(enemy_address) def _on_level_selected_via_world_map(self, level_name: str, object_set: int, layout_address: int, enemy_address: int): self.level_name = level_name self._fill_in_data(object_set, layout_address, enemy_address) self.on_ok() def on_ok(self, _=None): self.object_set = self.object_set_dropdown.currentIndex() self.object_data_offset = self.object_data_spinner.value() # skip the first byte, because it seems useless self.enemy_data_offset = self.enemy_data_spinner.value() + 1 self.accept() def closeEvent(self, _close_event: QCloseEvent): self.reject()
class AutoScrollEditor(CustomDialog): def __init__(self, parent, level_ref: LevelRef): super(AutoScrollEditor, self).__init__(parent, title="Autoscroll Editor") self.level_ref = level_ref self.original_autoscroll_item = _get_autoscroll(self.level_ref.enemies) QVBoxLayout(self) self.enabled_checkbox = QCheckBox("Enable Autoscroll in level", self) self.enabled_checkbox.toggled.connect(self._insert_autoscroll_object) self.y_position_spinner = Spinner(self, maximum=0x60 - 1) self.y_position_spinner.valueChanged.connect(self._update_y_position) self.auto_scroll_type_label = QLabel(self) self.layout().addWidget(self.enabled_checkbox) self.layout().addWidget(self.y_position_spinner) self.layout().addWidget(self.auto_scroll_type_label) self.update() def update(self): autoscroll_item = _get_autoscroll(self.level_ref.enemies) self.enabled_checkbox.setChecked(autoscroll_item is not None) self.y_position_spinner.setEnabled(autoscroll_item is not None) if autoscroll_item is None: self.auto_scroll_type_label.setText(AUTOSCROLL_LABELS[-1]) else: self.y_position_spinner.setValue(autoscroll_item.y_position) self.auto_scroll_type_label.setText( AUTOSCROLL_LABELS[autoscroll_item.y_position >> 4]) def _update_y_position(self, _): autoscroll_item = _get_autoscroll(self.level_ref.enemies) autoscroll_item.y_position = self.y_position_spinner.value() self.level_ref.data_changed.emit() self.update() def _insert_autoscroll_object(self, should_insert: bool): autoscroll_item = _get_autoscroll(self.level_ref.enemies) if autoscroll_item is not None: self.level_ref.enemies.remove(autoscroll_item) if should_insert: self.level_ref.enemies.insert(0, self._create_autoscroll_object()) self.level_ref.data_changed.emit() self.update() def _create_autoscroll_object(self): return self.level_ref.level.enemy_item_factory.from_properties( OBJ_AUTOSCROLL, 0, self.y_position_spinner.value()) def closeEvent(self, event): current_autoscroll_item = _get_autoscroll(self.level_ref.enemies) if not (self.original_autoscroll_item is None and current_autoscroll_item is None): added_or_removed_autoscroll = self.original_autoscroll_item is None or current_autoscroll_item is None if (added_or_removed_autoscroll or self.original_autoscroll_item.y_position != current_autoscroll_item.y_position): self.level_ref.save_level_state() super(AutoScrollEditor, self).closeEvent(event)