Пример #1
0
    async def on_request_presets(self, ctx: ComponentContext):
        try:
            title = ctx.origin_message.embeds[0].title
            # Trim leading and trailing `s
            permalink = Permalink.from_str(title[1:-1])

        except (IndexError, ValueError, UnsupportedPermalink) as e:
            logging.exception("Unable to find permalink on message that sent attach_presets_of_permalink")
            permalink = None

        files = []

        if permalink is not None:
            for player, preset in enumerate(permalink.parameters.presets):
                data = io.BytesIO()
                VersionedPreset.with_preset(preset).save_to_io(data)
                data.seek(0)
                files.append(
                    discord.File(data, filename=f"Player {player + 1}'s Preset.{VersionedPreset.file_extension()}")
                )

        await ctx.edit_origin(
            components=[],
            files=files,
        )
Пример #2
0
def test_load_previous_state_missing_state(tmp_path: Path, default_preset):
    # Setup
    VersionedPreset.with_preset(default_preset).save_to_file(tmp_path.joinpath("preset.rdvpreset"))

    # Run
    result = tracker_window._load_previous_state(tmp_path, default_preset.configuration)

    # Assert
    assert result is None
Пример #3
0
def test_load_previous_state_invalid_state(tmp_path: Path, default_preset):
    # Setup
    VersionedPreset.with_preset(default_preset).save_to_file(tmp_path.joinpath("preset.rdvpreset"))
    tmp_path.joinpath("state.json").write_text("")

    # Run
    result = tracker_window._load_previous_state(tmp_path, default_preset.configuration)

    # Assert
    assert result is None
Пример #4
0
def test_load_previous_state_success(tmp_path: Path, default_preset):
    # Setup
    data = {"asdf": 5, "zxcv": 123}
    VersionedPreset.with_preset(default_preset).save_to_file(tmp_path.joinpath("preset.rdvpreset"))
    tmp_path.joinpath("state.json").write_text(json.dumps(data))

    # Run
    result = tracker_window._load_previous_state(tmp_path, default_preset.configuration)

    # Assert
    assert result == data
Пример #5
0
    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)
Пример #6
0
    def dropEvent(self, event: QtGui.QDropEvent) -> None:
        item: QtWidgets.QTreeWidgetItem = self.itemAt(event.pos())
        if not item:
            return event.setDropAction(Qt.IgnoreAction)

        source = self.preset_for_item(self.currentItem())
        target = self.preset_for_item(item)

        if source is None or target is None:
            return event.setDropAction(Qt.IgnoreAction)

        if source.game != target.game or source.base_preset_uuid is None:
            return event.setDropAction(Qt.IgnoreAction)

        try:
            source_preset = source.get_preset()
        except InvalidPreset:
            return event.setDropAction(Qt.IgnoreAction)

        self.window_manager.preset_manager.add_new_preset(
            VersionedPreset.with_preset(
                dataclasses.replace(source_preset,
                                    base_preset_uuid=target.uuid)))

        return super().dropEvent(event)
Пример #7
0
    def as_json(self, *, force_spoiler: bool = False) -> dict:
        result = {
            "schema_version": description_migration.CURRENT_VERSION,
            "info": {
                "randovania_version":
                self.randovania_version_text,
                "randovania_version_git":
                self.randovania_version_git.hex(),
                "permalink":
                self.permalink.as_base64_str,
                "has_spoiler":
                self.has_spoiler,
                "seed":
                self.generator_parameters.seed_number,
                "hash":
                self.shareable_hash,
                "word_hash":
                self.shareable_word_hash,
                "presets": [
                    VersionedPreset.with_preset(preset).as_json
                    for preset in self.all_presets
                ],
            }
        }

        if self.has_spoiler or force_spoiler:
            result["game_modifications"] = self._serialized_patches
            result["item_order"] = self.item_order

        return result
Пример #8
0
async def test_on_load_preset(skip_qtbot, blank_game_description, mocker,
                              preset_state, tmp_path, preset_manager):
    preset_path = tmp_path.joinpath("preset.rdvpreset")

    base_preset = preset_manager.default_preset_for_game(
        blank_game_description.game).get_preset()
    trick_level = base_preset.configuration.trick_level
    for trick in blank_game_description.resource_database.trick:
        trick_level = trick_level.set_level_for_trick(
            trick, LayoutTrickLevel.HYPERMODE)

    preset = dataclasses.replace(
        base_preset,
        configuration=dataclasses.replace(base_preset.configuration,
                                          trick_level=trick_level),
    )

    if preset_state > 1:
        VersionedPreset.with_preset(preset).save_to_file(preset_path)

    mock_prompt_preset: AsyncMock = mocker.patch(
        "randovania.gui.lib.file_prompts.prompt_preset",
        return_value=preset_path if preset_state > 0 else None,
    )
    mock_warning: AsyncMock = mocker.patch(
        "randovania.gui.lib.async_dialog.warning")

    root = QtWidgets.QWidget()
    skip_qtbot.addWidget(root)

    widget = ConnectionLayerWidget(root, blank_game_description)

    # Run
    await widget._on_load_preset()

    # Assert
    mock_prompt_preset.assert_awaited_once_with(widget, False)
    if preset_state == 1:
        mock_warning.assert_awaited_once_with(widget, "Invalid preset", ANY)
    else:
        mock_warning.assert_not_awaited()

    for (trick, trick_check), combo in widget.tricks.items():
        assert trick_check.isChecked() == (preset_state == 2)
        assert combo.currentData() == (5 if preset_state == 2 else 0)
Пример #9
0
async def test_add_then_delete_preset(tmp_path, default_preset):
    p = VersionedPreset.with_preset(default_preset.fork())

    dulwich.repo.Repo.init(tmp_path)
    manager = preset_manager.PresetManager(tmp_path.joinpath("presets"))
    await manager.load_user_presets()

    assert manager.preset_for_uuid(p.uuid) is None
    manager.add_new_preset(p)
    assert manager.preset_for_uuid(p.uuid) == p
    manager.delete_preset(p)
    assert manager.preset_for_uuid(p.uuid) is None
Пример #10
0
def _change_layout_description(sio: ServerApp, session: GameSession,
                               description_json: Optional[dict]):
    _verify_has_admin(sio, session.id, None)
    _verify_in_setup(session)
    rows_to_update = []

    if description_json is None:
        description = None
    else:
        if session.generation_in_progress != sio.get_current_user():
            if session.generation_in_progress is None:
                raise InvalidAction(f"Not waiting for a layout.")
            else:
                raise InvalidAction(
                    f"Waiting for a layout from {session.generation_in_progress.name}."
                )

        _verify_no_layout_description(session)
        description = LayoutDescription.from_json_dict(description_json)
        if description.player_count != session.num_rows:
            raise InvalidAction(
                f"Description is for a {description.player_count} players,"
                f" while the session is for {session.num_rows}.")

        for permalink_preset, preset_row in zip(description.all_presets,
                                                session.presets):
            preset_row = typing.cast(GameSessionPreset, preset_row)
            if _get_preset(json.loads(
                    preset_row.preset)).get_preset() != permalink_preset:
                preset = VersionedPreset.with_preset(permalink_preset)
                if preset.game not in session.allowed_games:
                    raise InvalidAction(f"{preset.game} preset not allowed.")
                preset_row.preset = json.dumps(preset.as_json)
                rows_to_update.append(preset_row)

    with database.db.atomic():
        for preset_row in rows_to_update:
            preset_row.save()

        session.generation_in_progress = None
        session.layout_description = description
        session.save()
        _add_audit_entry(
            sio, session, "Removed generated game" if description is None else
            f"Set game to {description.shareable_word_hash}")
Пример #11
0
    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()))
Пример #12
0
    async def configure(self):
        player_pool = await generator.create_player_pool(
            None, self.game_configuration, 0, 1, rng_required=False)
        pool_patches = player_pool.patches

        bootstrap = self.game_configuration.game.generator.bootstrap

        self.game_description, self._initial_state = bootstrap.logic_bootstrap(
            self.preset.configuration, player_pool.game, pool_patches)
        self.logic = Logic(self.game_description, self.preset.configuration)
        self.map_canvas.select_game(self.game_description.game)

        self._initial_state.resources.add_self_as_requirement_to_resources = True

        self.menu_reset_action.triggered.connect(self._confirm_reset)
        self.resource_filter_check.stateChanged.connect(
            self.update_locations_tree_for_reachable_nodes)
        self.hide_collected_resources_check.stateChanged.connect(
            self.update_locations_tree_for_reachable_nodes)
        self.undo_last_action_button.clicked.connect(self._undo_last_action)

        self.configuration_label.setText(
            "Trick Level: {}; Starts with:\n{}".format(
                self.preset.configuration.trick_level.pretty_description,
                ", ".join(resource.short_name for resource, _ in
                          pool_patches.starting_items.as_resource_gain())))

        self.setup_pickups_box(player_pool.pickups)
        self.setup_possible_locations_tree()
        self.setup_elevators()
        self.setup_translator_gates()

        # Map
        for world in sorted(self.game_description.world_list.worlds,
                            key=lambda x: x.name):
            self.map_world_combo.addItem(world.name, userData=world)

        self.on_map_world_combo(0)
        self.map_world_combo.currentIndexChanged.connect(
            self.on_map_world_combo)
        self.map_area_combo.currentIndexChanged.connect(self.on_map_area_combo)
        self.map_canvas.set_edit_mode(False)
        self.map_canvas.SelectAreaRequest.connect(self.focus_on_area)

        # Graph Map
        from randovania.gui.widgets.tracker_map import MatplotlibWidget
        self.matplot_widget = MatplotlibWidget(
            self.tab_graph_map, self.game_description.world_list)
        self.tab_graph_map_layout.addWidget(self.matplot_widget)
        self.map_tab_widget.currentChanged.connect(self._on_tab_changed)

        for world in self.game_description.world_list.worlds:
            self.graph_map_world_combo.addItem(world.name, world)
        self.graph_map_world_combo.currentIndexChanged.connect(
            self.on_graph_map_world_combo)

        self.persistence_path.mkdir(parents=True, exist_ok=True)
        previous_state = _load_previous_state(self.persistence_path,
                                              self.preset.configuration)

        if not self.apply_previous_state(previous_state):
            self.setup_starting_location(None)

            VersionedPreset.with_preset(self.preset).save_to_file(
                _persisted_preset_path(self.persistence_path))
            self._add_new_action(self._initial_state.node)
Пример #13
0
 def _on_duplicate_preset(self):
     old_preset = self._current_preset_data
     self._add_new_preset(VersionedPreset.with_preset(old_preset.get_preset().fork()))
Пример #14
0
async def test_apply_previous_state(skip_qtbot, tmp_path: Path, default_echoes_preset, shuffle_advanced,
                              echoes_game_description):
    configuration = default_echoes_preset.configuration
    assert isinstance(configuration, EchoesConfiguration)

    if shuffle_advanced:
        translator_requirement = copy.copy(
            configuration.translator_configuration.translator_requirement)
        for gate in translator_requirement.keys():
            translator_requirement[gate] = LayoutTranslatorRequirement.RANDOM
            break

        new_gate = dataclasses.replace(configuration.translator_configuration,
                                       translator_requirement=translator_requirement)
        layout_config = dataclasses.replace(
            configuration,
            elevators=dataclasses.replace(
                configuration.elevators,
                mode=TeleporterShuffleMode.ONE_WAY_ANYTHING,
            ),
            translator_configuration=new_gate)

        preset = dataclasses.replace(default_echoes_preset.fork(), configuration=layout_config)

    else:
        preset = default_echoes_preset

    state: dict = {
        "actions": [
            "Temple Grounds/Landing Site/Save Station"
        ],
        "collected_pickups": {
            'Amber Translator': 0,
            'Annihilator Beam': 0,
            'Boost Ball': 0,
            'Cobalt Translator': 0,
            'Dark Agon Key 1': 0,
            'Dark Agon Key 2': 0,
            'Dark Agon Key 3': 0,
            'Dark Ammo Expansion': 0,
            'Dark Beam': 0,
            'Dark Torvus Key 1': 0,
            'Dark Torvus Key 2': 0,
            'Dark Torvus Key 3': 0,
            'Dark Visor': 0,
            'Darkburst': 0,
            'Echo Visor': 0,
            'Emerald Translator': 0,
            'Energy Tank': 0,
            'Grapple Beam': 0,
            'Gravity Boost': 0,
            'Ing Hive Key 1': 0,
            'Ing Hive Key 2': 0,
            'Ing Hive Key 3': 0,
            'Light Ammo Expansion': 0,
            'Light Beam': 0,
            'Missile Expansion': 0,
            'Missile Launcher': 0,
            'Morph Ball Bomb': 0,
            'Power Bomb': 0,
            'Power Bomb Expansion': 0,
            'Progressive Suit': 0,
            'Screw Attack': 0,
            'Seeker Launcher': 0,
            'Sky Temple Key 1': 0,
            'Sky Temple Key 2': 0,
            'Sky Temple Key 3': 0,
            'Sky Temple Key 4': 0,
            'Sky Temple Key 5': 0,
            'Sky Temple Key 6': 0,
            'Sky Temple Key 7': 0,
            'Sky Temple Key 8': 0,
            'Sky Temple Key 9': 0,
            'Sonic Boom': 0,
            'Space Jump Boots': 1,
            'Spider Ball': 0,
            'Sunburst': 0,
            'Super Missile': 0,
            'Violet Translator': 0,
        },
        "elevators": [
            {'data': None,
             'teleporter': {'area_name': 'Transport to Temple Grounds',
                            'node_name': 'Elevator to Temple Grounds - Transport to Agon Wastes',
                            'world_name': 'Agon Wastes'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Torvus Bog',
                            'node_name': 'Elevator to Torvus Bog - Transport to Agon Wastes',
                            'world_name': 'Agon Wastes'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Sanctuary Fortress',
                            'node_name': 'Elevator to Sanctuary Fortress - Transport to Agon Wastes',
                            'world_name': 'Agon Wastes'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport C',
                            'node_name': 'Elevator to Temple Grounds - Temple Transport C',
                            'world_name': 'Great Temple'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport A',
                            'node_name': 'Elevator to Temple Grounds - Temple Transport A',
                            'world_name': 'Great Temple'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport B',
                            'node_name': 'Elevator to Temple Grounds - Temple Transport B',
                            'world_name': 'Great Temple'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Temple Grounds',
                            'node_name': 'Elevator to Temple Grounds - Transport to Sanctuary Fortress',
                            'world_name': 'Sanctuary Fortress'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Agon Wastes',
                            'node_name': 'Elevator to Agon Wastes - Transport to Sanctuary Fortress',
                            'world_name': 'Sanctuary Fortress'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Torvus Bog',
                            'node_name': 'Elevator to Torvus Bog - Transport to Sanctuary Fortress',
                            'world_name': 'Sanctuary Fortress'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Agon Wastes',
                            'node_name': 'Elevator to Agon Wastes - Transport to Temple Grounds',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport B',
                            'node_name': 'Elevator to Great Temple - Temple Transport B',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Sanctuary Fortress',
                            'node_name': 'Elevator to Sanctuary Fortress - Transport to Temple Grounds',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport A',
                            'node_name': 'Elevator to Great Temple - Temple Transport A',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Torvus Bog',
                            'node_name': 'Elevator to Torvus Bog - Transport to Temple Grounds',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Temple Transport C',
                            'node_name': 'Elevator to Great Temple - Temple Transport C',
                            'world_name': 'Temple Grounds'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Sanctuary Fortress',
                            'node_name': 'Elevator to Sanctuary Fortress - Transport to Torvus Bog',
                            'world_name': 'Torvus Bog'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Temple Grounds',
                            'node_name': 'Elevator to Temple Grounds - Transport to Torvus Bog',
                            'world_name': 'Torvus Bog'}},
            {'data': None,
             'teleporter': {'area_name': 'Transport to Agon Wastes',
                            'node_name': 'Elevator to Agon Wastes - Transport to Torvus Bog',
                            'world_name': 'Torvus Bog'}}
        ],
        "configurable_nodes": {
            'Agon Wastes/Mining Plaza/Translator Gate': None,
            'Agon Wastes/Mining Station A/Translator Gate': None,
            'Great Temple/Temple Sanctuary/Transport A Translator Gate': None,
            'Great Temple/Temple Sanctuary/Transport B Translator Gate': None,
            'Great Temple/Temple Sanctuary/Transport C Translator Gate': None,
            'Sanctuary Fortress/Reactor Core/Translator Gate': None,
            'Sanctuary Fortress/Sanctuary Temple/Translator Gate': None,
            'Temple Grounds/GFMC Compound/Translator Gate': None,
            'Temple Grounds/Hive Access Tunnel/Translator Gate': None,
            'Temple Grounds/Hive Transport Area/Translator Gate': None,
            'Temple Grounds/Industrial Site/Translator Gate': None,
            'Temple Grounds/Meeting Grounds/Translator Gate': None,
            'Temple Grounds/Path of Eyes/Translator Gate': None,
            'Temple Grounds/Temple Assembly Site/Translator Gate': None,
            'Torvus Bog/Great Bridge/Translator Gate': None,
            'Torvus Bog/Torvus Temple/Elevator Translator Scan': None,
            'Torvus Bog/Torvus Temple/Translator Gate': None,
        },
        "starting_location": {'world_name': 'Temple Grounds', 'area_name': 'Landing Site'}
    }

    if shuffle_advanced:
        for elevator in state["elevators"]:
            if elevator["teleporter"]["node_name"] == "Elevator to Sanctuary Fortress - Transport to Agon Wastes":
                elevator["data"] = {'area_name': "Agon Energy Controller", 'world_name': "Agon Wastes"}
        state["configurable_nodes"]['Temple Grounds/Hive Access Tunnel/Translator Gate'] = "violet"
    VersionedPreset.with_preset(preset).save_to_file(tmp_path.joinpath("preset.rdvpreset"))
    tmp_path.joinpath("state.json").write_text(json.dumps(state), "utf-8")

    # Run
    window = await tracker_window.TrackerWindow.create_new(tmp_path, preset)
    skip_qtbot.add_widget(window)

    # Assert
    assert window.state_for_current_configuration() is not None
    persisted_data = json.loads(tmp_path.joinpath("state.json").read_text("utf-8"))
    assert persisted_data == state

    window.reset()
    window.persist_current_state()

    persisted_data = json.loads(tmp_path.joinpath("state.json").read_text("utf-8"))
    assert persisted_data != state
Пример #15
0
 def _export_preset(self):
     preset = self.layout_description.get_preset(self.current_player_index)
     output_path = common_qt_lib.prompt_user_for_preset_file(self, new_file=True)
     if output_path is not None:
         VersionedPreset.with_preset(preset).save_to_file(output_path)