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, )
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
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
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
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 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)
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
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)
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
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}")
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()))
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)
def _on_duplicate_preset(self): old_preset = self._current_preset_data self._add_new_preset(VersionedPreset.with_preset(old_preset.get_preset().fork()))
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
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)