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)
示例#3
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)