def __init__(self, window: Ui_MainWindow, window_manager: WindowManager, options: Options): super().__init__() self.window = window self._window_manager = window_manager self.failure_handler = GenerationFailureHandler(self) self._options = options
class GenerateSeedTab(QtWidgets.QWidget, BackgroundTaskMixin): _logic_settings_window: Optional[CustomizePresetDialog] = None _has_set_from_last_selected: bool = False _preset_menu: PresetMenu _action_delete: QtGui.QAction _original_show_event: Callable[[QtGui.QShowEvent], None] def __init__(self, window: Ui_MainWindow, window_manager: WindowManager, options: Options): super().__init__() self.window = window self._window_manager = window_manager self.failure_handler = GenerationFailureHandler(self) self._options = options def setup_ui(self): window = self.window window.create_preset_tree.window_manager = self._window_manager window.create_preset_tree.options = self._options self._original_show_event = window.tab_create_seed.showEvent window.tab_create_seed.showEvent = self._tab_show_event # Progress self.background_tasks_button_lock_signal.connect(self.enable_buttons_with_background_tasks) self.progress_update_signal.connect(self.update_progress) self.window.stop_background_process_button.clicked.connect(self.stop_background_process) self.window.num_players_spin_box.setVisible(self._window_manager.is_preview_mode) self.window.create_generate_no_retry_button.setVisible(self._window_manager.is_preview_mode) # Menu self._preset_menu = PresetMenu(window) # Signals window.create_generate_button.clicked.connect(partial(self._generate_new_seed, True)) window.create_generate_no_retry_button.clicked.connect(partial(self._generate_new_seed, True, retries=0)) window.create_generate_race_button.clicked.connect(partial(self._generate_new_seed, False)) window.create_preset_tree.itemSelectionChanged.connect(self._on_select_preset) window.create_preset_tree.customContextMenuRequested.connect(self._on_tree_context_menu) self._preset_menu.action_customize.triggered.connect(self._on_customize_preset) self._preset_menu.action_delete.triggered.connect(self._on_delete_preset) self._preset_menu.action_history.triggered.connect(self._on_view_preset_history) self._preset_menu.action_export.triggered.connect(self._on_export_preset) self._preset_menu.action_duplicate.triggered.connect(self._on_duplicate_preset) self._preset_menu.action_map_tracker.triggered.connect(self._on_open_map_tracker_for_preset) self._preset_menu.action_required_tricks.triggered.connect(self._on_open_required_tricks_for_preset) self._preset_menu.action_import.triggered.connect(self._on_import_preset) self._update_preset_tree_items() def _update_preset_tree_items(self): self.window.create_preset_tree.update_items() @asyncSlot() async def _do_migration(self): dialog = QtWidgets.QProgressDialog( ("Randovania changed where your presets are saved and a one-time migration is being performed.\n" "Further changes in old versions won't be migrated."), None, 0, 1, self, ) common_qt_lib.set_default_window_icon(dialog) dialog.setWindowTitle("Preset Migration") dialog.setAutoReset(False) dialog.setAutoClose(False) dialog.show() def on_update(current, target): dialog.setValue(current) dialog.setMaximum(target) await self._window_manager.preset_manager.migrate_from_old_path(on_update) self._update_preset_tree_items() dialog.setCancelButtonText("Ok") def _tab_show_event(self, event: QtGui.QShowEvent): if self._window_manager.preset_manager.should_do_migration(): QTimer.singleShot(0, self._do_migration) return self._original_show_event(event) @property def _current_preset_data(self) -> Optional[VersionedPreset]: return self.window.create_preset_tree.current_preset_data def enable_buttons_with_background_tasks(self, value: bool): self.window.stop_background_process_button.setEnabled(not value) self.window.create_generate_button.setEnabled(value) self.window.create_generate_race_button.setEnabled(value) def _add_new_preset(self, preset: VersionedPreset): with self._options as options: options.selected_preset_uuid = preset.uuid self._window_manager.preset_manager.add_new_preset(preset) self._update_preset_tree_items() self.window.create_preset_tree.select_preset(preset) @asyncSlot() async def _on_customize_preset(self): if self._logic_settings_window is not None: self._logic_settings_window.raise_() return old_preset = self._current_preset_data.get_preset() if old_preset.base_preset_uuid is None: old_preset = old_preset.fork() editor = PresetEditor(old_preset) self._logic_settings_window = CustomizePresetDialog(self._window_manager, editor) self._logic_settings_window.on_preset_changed(editor.create_custom_preset_with()) editor.on_changed = lambda: self._logic_settings_window.on_preset_changed(editor.create_custom_preset_with()) result = await async_dialog.execute_dialog(self._logic_settings_window) self._logic_settings_window = None if result == QtWidgets.QDialog.Accepted: self._add_new_preset(VersionedPreset.with_preset(editor.create_custom_preset_with())) def _on_delete_preset(self): self._window_manager.preset_manager.delete_preset(self._current_preset_data) self._update_preset_tree_items() self._on_select_preset() def _on_view_preset_history(self): pass def _on_export_preset(self): default_name = "{}.rdvpreset".format(self._current_preset_data.slug_name) path = common_qt_lib.prompt_user_for_preset_file(self._window_manager, new_file=True, name=default_name) if path is not None: self._current_preset_data.save_to_file(path) def _on_duplicate_preset(self): old_preset = self._current_preset_data self._add_new_preset(VersionedPreset.with_preset(old_preset.get_preset().fork())) @asyncSlot() async def _on_open_map_tracker_for_preset(self): await self._window_manager.open_map_tracker(self._current_preset_data.get_preset()) def _on_open_required_tricks_for_preset(self): from randovania.gui.dialog.trick_usage_popup import TrickUsagePopup self._trick_usage_popup = TrickUsagePopup(self, self._window_manager, self._current_preset_data.get_preset()) self._trick_usage_popup.setWindowModality(QtCore.Qt.WindowModal) self._trick_usage_popup.open() def _on_import_preset(self): path = common_qt_lib.prompt_user_for_preset_file(self._window_manager, new_file=False) if path is not None: self.import_preset_file(path) def import_preset_file(self, path: Path): preset = VersionedPreset.from_file_sync(path) try: preset.get_preset() except InvalidPreset: QtWidgets.QMessageBox.critical( self._window_manager, "Error loading preset", "The file at '{}' contains an invalid preset.".format(path) ) return existing_preset = self._window_manager.preset_manager.preset_for_uuid(preset.uuid) if existing_preset is not None: user_response = QtWidgets.QMessageBox.warning( self._window_manager, "Preset ID conflict", "The new preset '{}' has the same ID as existing '{}'. Do you want to overwrite it?".format( preset.name, existing_preset.name, ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel ) if user_response == QtWidgets.QMessageBox.Cancel: return elif user_response == QtWidgets.QMessageBox.No: preset = VersionedPreset.with_preset(dataclasses.replace(preset.get_preset(), uuid=uuid.uuid4())) self._add_new_preset(preset) def _on_select_preset(self): preset_data = self._current_preset_data self.on_preset_changed(preset_data) if preset_data is not None: with self._options as options: options.selected_preset_uuid = preset_data.uuid def _on_tree_context_menu(self, pos: QtCore.QPoint): item: QtWidgets.QTreeWidgetItem = self.window.create_preset_tree.itemAt(pos) preset = None if item is not None: preset = self.window.create_preset_tree.preset_for_item(item) self._preset_menu.set_preset(preset) self._preset_menu.exec_(QtGui.QCursor.pos()) # Generate seed def _generate_new_seed(self, spoiler: bool, retries: Optional[int] = None): preset = self._current_preset_data num_players = self.window.num_players_spin_box.value() self.generate_seed_from_permalink(Permalink.from_parameters(GeneratorParameters( seed_number=random.randint(0, 2 ** 31), spoiler=spoiler, presets=[preset.get_preset()] * num_players, )), retries=retries) def generate_seed_from_permalink(self, permalink: Permalink, retries: Optional[int] = None): def work(progress_update: ProgressUpdateCallable): try: layout = simplified_patcher.generate_layout(progress_update=progress_update, parameters=permalink.parameters, options=self._options, retries=retries) progress_update(f"Success! (Seed hash: {layout.shareable_hash})", 1) persist_layout(self._options.game_history_path, layout) self._window_manager.open_game_details(layout) except GenerationFailure as generate_exception: self.failure_handler.handle_failure(generate_exception) progress_update("Generation Failure: {}".format(generate_exception), -1) if self._window_manager.is_preview_mode: logging.info(f"Permalink: {permalink.as_base64_str}") self.run_in_background_thread(work, "Creating a seed...") def on_options_changed(self, options: Options): self.window.create_preset_tree.set_show_experimental(options.experimental_games) if not self._has_set_from_last_selected: self._has_set_from_last_selected = True preset = self._window_manager.preset_manager.preset_for_uuid(options.selected_preset_uuid) if preset is None: preset = self._window_manager.preset_manager.default_preset self.window.create_preset_tree.select_preset(preset) def on_preset_changed(self, preset: Optional[VersionedPreset]): can_generate = False if preset is None: description = "Please select a preset from the list, not a game." else: try: raw_preset = preset.get_preset() can_generate = True description = f"<p style='font-weight:600;'>{raw_preset.name}</p><p>{raw_preset.description}</p>" description += preset_describer.merge_categories(preset_describer.describe(raw_preset)) except InvalidPreset as e: logging.exception(f"Invalid preset for {preset.name}") description = ( f"Preset {preset.name} can't be used as it contains the following error:" f"\n{e.original_exception}\n" f"\nPlease open edit the preset file with id {preset.uuid} manually or delete this preset." ) self.window.create_preset_description.setText(description) for btn in [self.window.create_generate_button, self.window.create_generate_race_button]: btn.setEnabled(can_generate) def update_progress(self, message: str, percentage: int): self.window.progress_label.setText(message) if "Aborted" in message: percentage = 0 if percentage >= 0: self.window.progress_bar.setRange(0, 100) self.window.progress_bar.setValue(percentage) else: self.window.progress_bar.setRange(0, 0)
class GenerateSeedTab(QWidget, BackgroundTaskMixin): _logic_settings_window: Optional[LogicSettingsWindow] = None _has_preset: bool = False _tool_button_menu: QMenu _action_delete: QAction def __init__(self, window: Ui_MainWindow, window_manager: WindowManager, options: Options): super().__init__() self.window = window self._window_manager = window_manager self.failure_handler = GenerationFailureHandler(self) self._options = options def setup_ui(self): window = self.window # Progress self.background_tasks_button_lock_signal.connect( self.enable_buttons_with_background_tasks) self.progress_update_signal.connect(self.update_progress) self.window.stop_background_process_button.clicked.connect( self.stop_background_process) for game in RandovaniaGame: self.window.create_choose_game_combo.addItem(game.long_name, game) self.window.create_choose_game_combo.setVisible( self._window_manager.is_preview_mode) self.window.create_choose_game_label.setVisible( self._window_manager.is_preview_mode) self.window.num_players_spin_box.setVisible( self._window_manager.is_preview_mode) # Menu self._tool_button_menu = QMenu(window.preset_tool_button) window.preset_tool_button.setMenu(self._tool_button_menu) self._action_delete = QAction(window) self._action_delete.setText("Delete") self._tool_button_menu.addAction(self._action_delete) action_export_preset = QAction(window) action_export_preset.setText("Export") self._tool_button_menu.addAction(action_export_preset) action_import_preset = QAction(window) action_import_preset.setText("Import") self._tool_button_menu.addAction(action_import_preset) # Signals window.create_choose_game_combo.activated.connect(self._on_select_game) window.preset_tool_button.clicked.connect(self._on_customize_button) window.create_preset_combo.activated.connect(self._on_select_preset) window.create_generate_button.clicked.connect( partial(self._generate_new_seed, True)) window.create_generate_race_button.clicked.connect( partial(self._generate_new_seed, False)) self._action_delete.triggered.connect(self._on_delete_preset) action_export_preset.triggered.connect(self._on_export_preset) action_import_preset.triggered.connect(self._on_import_preset) @property def _current_preset_data(self) -> Optional[VersionedPreset]: return self._window_manager.preset_manager.preset_for_name( self.window.create_preset_combo.currentData()) def enable_buttons_with_background_tasks(self, value: bool): self.window.stop_background_process_button.setEnabled(not value) self.window.create_generate_button.setEnabled(value) self.window.create_generate_race_button.setEnabled(value) def _create_button_for_preset(self, preset: VersionedPreset): create_preset_combo = self.window.create_preset_combo create_preset_combo.addItem(preset.name, preset.name) def _add_new_preset(self, preset: VersionedPreset): with self._options as options: options.selected_preset_name = preset.name if self._window_manager.preset_manager.add_new_preset(preset): self._create_button_for_preset(preset) self.on_preset_changed(preset.get_preset()) @asyncSlot() async def _on_customize_button(self): if self._logic_settings_window is not None: self._logic_settings_window.raise_() return editor = PresetEditor(self._current_preset_data.get_preset()) self._logic_settings_window = LogicSettingsWindow( self._window_manager, editor) self._logic_settings_window.on_preset_changed( editor.create_custom_preset_with()) editor.on_changed = lambda: self._logic_settings_window.on_preset_changed( editor.create_custom_preset_with()) result = await async_dialog.execute_dialog(self._logic_settings_window) self._logic_settings_window = None if result == QDialog.Accepted: self._add_new_preset( VersionedPreset.with_preset( editor.create_custom_preset_with())) def _on_delete_preset(self): self._window_manager.preset_manager.delete_preset( self._current_preset_data) self.window.create_preset_combo.removeItem( self.window.create_preset_combo.currentIndex()) self._on_select_preset() def _on_export_preset(self): path = common_qt_lib.prompt_user_for_preset_file(self._window_manager, new_file=True) if path is not None: self._current_preset_data.save_to_file(path) def _on_import_preset(self): path = common_qt_lib.prompt_user_for_preset_file(self._window_manager, new_file=False) if path is None: return preset = VersionedPreset.from_file_sync(path) try: preset.get_preset() except (ValueError, KeyError): QMessageBox.critical( self._window_manager, "Error loading preset", "The file at '{}' contains an invalid preset.".format(path)) return if self._window_manager.preset_manager.preset_for_name( preset.name) is not None: user_response = QMessageBox.warning( self._window_manager, "Preset name conflict", "A preset named '{}' already exists. Do you want to overwrite it?" .format(preset.name), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if user_response == QMessageBox.No: return self._add_new_preset(preset) def select_game(self, game: RandovaniaGame): combo_index = self.window.create_choose_game_combo.findData(game) self.window.create_choose_game_combo.setCurrentIndex(combo_index) self._update_create_preset_combo(game) def _on_select_game(self): game = self.window.create_choose_game_combo.currentData() self._update_create_preset_combo(game) self._on_select_preset() def _update_create_preset_combo(self, game: RandovaniaGame): self.window.create_preset_combo.clear() for preset in self._window_manager.preset_manager.all_presets: if preset.game == game: self._create_button_for_preset(preset) def _on_select_preset(self): preset_data = self._current_preset_data try: self.on_preset_changed(preset_data.get_preset()) except InvalidPreset as e: logging.exception(f"Invalid preset for {preset_data.name}") QMessageBox.warning( self._window_manager, "Incompatible Preset", f"Preset {preset_data.name} can't be used as it contains the following error:\n{e.original_exception}" ) self.window.create_preset_combo.setCurrentIndex(0) self.on_preset_changed(self._window_manager.preset_manager. default_preset.get_preset()) return with self._options as options: options.selected_preset_name = preset_data.name # Generate seed def _generate_new_seed(self, spoiler: bool): preset = self._current_preset_data num_players = self.window.num_players_spin_box.value() self.generate_seed_from_permalink( Permalink( seed_number=random.randint(0, 2**31), spoiler=spoiler, presets={i: preset.get_preset() for i in range(num_players)}, )) def generate_seed_from_permalink(self, permalink: Permalink): def work(progress_update: ProgressUpdateCallable): try: layout = simplified_patcher.generate_layout( progress_update=progress_update, permalink=permalink, options=self._options) progress_update( f"Success! (Seed hash: {layout.shareable_hash})", 1) persist_layout(self._options.data_dir, layout) self._window_manager.open_game_details(layout) except GenerationFailure as generate_exception: self.failure_handler.handle_failure(generate_exception) progress_update( "Generation Failure: {}".format(generate_exception), -1) if self._window_manager.is_preview_mode: print(f"Permalink: {permalink.as_base64_str}") self.run_in_background_thread(work, "Creating a seed...") def on_options_changed(self, options: Options): if not self._has_preset: selected_preset = self._window_manager.preset_manager.preset_for_name( options.selected_preset_name) if selected_preset is not None: self.select_game(selected_preset.game) index = self.window.create_preset_combo.findText( selected_preset.name) if index != -1: self.window.create_preset_combo.setCurrentIndex(index) try: self.on_preset_changed( self._current_preset_data.get_preset()) return except InvalidPreset: logging.exception( f"Invalid preset for {options.selected_preset_name}" ) else: self.select_game(RandovaniaGame.PRIME2) self.window.create_preset_combo.setCurrentIndex(0) self.on_preset_changed(self._window_manager.preset_manager. default_preset.get_preset()) def on_preset_changed(self, preset: Preset): self._has_preset = True self.window.create_preset_description.setText(preset.description) self._action_delete.setEnabled(preset.base_preset_name is not None) create_preset_combo = self.window.create_preset_combo create_preset_combo.setCurrentIndex( create_preset_combo.findText(preset.name)) categories = list(preset_describer.describe(preset)) left_categories = categories[::2] right_categories = categories[1::2] self.window.create_describe_left_label.setText( preset_describer.merge_categories(left_categories)) self.window.create_describe_right_label.setText( preset_describer.merge_categories(right_categories)) def update_progress(self, message: str, percentage: int): self.window.progress_label.setText(message) if "Aborted" in message: percentage = 0 if percentage >= 0: self.window.progress_bar.setRange(0, 100) self.window.progress_bar.setValue(percentage) else: self.window.progress_bar.setRange(0, 0)