Example #1
0
def test_starting_location_world_select(skip_qtbot, default_preset):
    # Setup
    preset = dataclasses.replace(
        default_preset,
        uuid=uuid.UUID('b41fde84-1f57-4b79-8cd6-3e5a78077fa6'),
        base_preset_uuid=default_preset.uuid)
    editor = PresetEditor(preset)
    window = LogicSettingsWindow(None, editor)
    skip_qtbot.addWidget(window)

    # Run
    checkbox_list = window._starting_location_for_world
    window.on_preset_changed(editor.create_custom_preset_with())
    assert len(checkbox_list) == 10
    temple_grounds_checkbox = checkbox_list["Temple Grounds"]
    assert temple_grounds_checkbox.checkState() == Qt.PartiallyChecked
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    assert temple_grounds_checkbox.checkState() == Qt.Checked
    assert len(editor.configuration.starting_location.locations) == 39
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    assert temple_grounds_checkbox.checkState() == Qt.Unchecked
    assert len(editor.configuration.starting_location.locations) == 0
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    window.on_preset_changed(editor.create_custom_preset_with())
    assert temple_grounds_checkbox.checkState() == Qt.Checked
    assert len(editor.configuration.starting_location.locations) == 39
def test_on_preset_changed(skip_qtbot, preset_manager, game):
    # Setup
    editor = PresetEditor(
        preset_manager.default_preset_for_game(game).get_preset())
    window = LogicSettingsWindow(None, editor)

    # Run
    window.on_preset_changed(editor.create_custom_preset_with())
Example #3
0
def test_on_preset_changed(skip_qtbot, preset_manager, game):
    # Setup
    base = preset_manager.default_preset_for_game(game).get_preset()
    preset = dataclasses.replace(
        base,
        uuid=uuid.UUID('b41fde84-1f57-4b79-8cd6-3e5a78077fa6'),
        base_preset_uuid=base.uuid)
    editor = PresetEditor(preset)
    window = LogicSettingsWindow(None, editor)

    # Run
    window.on_preset_changed(editor.create_custom_preset_with())
def test_starting_location_world_select(skip_qtbot, default_preset):
    # Setup
    editor = PresetEditor(default_preset)
    window = LogicSettingsWindow(None, editor)
    skip_qtbot.addWidget(window)

    # Run
    checkbox_list = window._starting_location_for_world
    window.on_preset_changed(editor.create_custom_preset_with())
    assert len(checkbox_list) == 10
    temple_grounds_checkbox = checkbox_list["Temple Grounds"]
    assert temple_grounds_checkbox.checkState() == Qt.PartiallyChecked
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    assert temple_grounds_checkbox.checkState() == Qt.Checked
    assert len(editor.configuration.starting_location.locations) == 39
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    assert temple_grounds_checkbox.checkState() == Qt.Unchecked
    assert len(editor.configuration.starting_location.locations) == 0
    skip_qtbot.mouseClick(temple_grounds_checkbox, Qt.LeftButton)
    window.on_preset_changed(editor.create_custom_preset_with())
    assert temple_grounds_checkbox.checkState() == Qt.Checked
    assert len(editor.configuration.starting_location.locations) == 39
Example #5
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)
Example #6
0
class GenerateSeedTab(QtWidgets.QWidget, BackgroundTaskMixin):
    _logic_settings_window: Optional[LogicSettingsWindow] = None
    _has_set_from_last_selected: bool = False
    _preset_menu: PresetMenu
    _action_delete: QtWidgets.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

        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_import.triggered.connect(
            self._on_import_preset)

        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.window.create_preset_tree.update_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.window.create_preset_tree.update_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
        editor = PresetEditor(old_preset.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 == 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)
        index = self.window.create_preset_tree.currentIndex()
        self.window.create_preset_tree.update_items()
        self.window.create_preset_tree.setCurrentIndex(index)
        self._on_select_preset()

    def _on_view_preset_history(self):
        pass

    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_duplicate_preset(self):
        old_preset = self._current_preset_data
        self._add_new_preset(
            VersionedPreset.with_preset(old_preset.get_preset().fork()))

    def _on_open_map_tracker_for_preset(self):
        self._window_manager.open_map_tracker(
            self._current_preset_data.get_preset().configuration)

    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(
            seed_number=random.randint(0, 2**31),
            spoiler=spoiler,
            presets={i: preset.get_preset()
                     for i in range(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,
                    permalink=permalink,
                    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:
            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_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)