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 _get_preset(preset_json: dict) -> VersionedPreset: try: preset = VersionedPreset(preset_json) preset.get_preset() # test if valid return preset except Exception as e: raise InvalidAction(f"invalid preset: {e}")
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 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 add_new_preset(self, new_preset: VersionedPreset) -> bool: """ Adds a new custom preset. :param: new_preset :return True, if there wasn't any preset with that name """ assert new_preset.uuid not in self.included_presets existed_before = new_preset.uuid in self.custom_presets self.custom_presets[new_preset.uuid] = new_preset path = self._file_name_for_preset(new_preset) new_preset.save_to_file(path) self._commit(f"Update preset '{new_preset.name}'", path, False) return not existed_before
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 from_json_dict(cls, json_dict: dict) -> "LayoutDescription": json_dict = description_migration.convert_to_current_version(json_dict) if "game_modifications" not in json_dict: raise ValueError("Unable to read details of a race game file") generator_parameters = GeneratorParameters( seed_number=json_dict["info"]["seed"], spoiler=json_dict["info"]["has_spoiler"], presets=[ VersionedPreset(preset).get_preset() for preset in json_dict["info"]["presets"] ], ) return LayoutDescription( randovania_version_text=json_dict["info"]["randovania_version"], randovania_version_git=bytes.fromhex( json_dict["info"]["randovania_version_git"]), generator_parameters=generator_parameters, all_patches=game_patches_serializer.decode( json_dict["game_modifications"], { index: preset.configuration for index, preset in enumerate( generator_parameters.presets) }), item_order=json_dict["item_order"], )
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
def from_json(cls, data) -> "GameSessionEntry": data = convert_to_raw_python(BinaryGameSessionEntry.parse(data)) player_entries = [ PlayerSessionEntry.from_json(player_json) for player_json in data["players"] ] return GameSessionEntry( id=data["id"], name=data["name"], presets=[ VersionedPreset(json.loads(preset_json)) for preset_json in data["presets"] ], players={ player_entry.id: player_entry for player_entry in player_entries }, game_details=GameDetails.from_json(data["game_details"]) if data["game_details"] is not None else None, state=GameSessionState(data["state"]), generation_in_progress=data["generation_in_progress"], allowed_games=[ RandovaniaGame(game) for game in data["allowed_games"] ], )
async def on_message(self, message: discord.Message): if message.author == self.bot.user: return for attachment in message.attachments: filename: str = attachment.filename if filename.endswith(VersionedPreset.file_extension()): data = await attachment.read() versioned_preset = VersionedPreset(json.loads(data.decode("utf-8"))) await reply_for_preset(message, versioned_preset) elif filename.endswith(LayoutDescription.file_extension()): data = await attachment.read() description = LayoutDescription.from_json_dict(json.loads(data.decode("utf-8"))) await reply_for_layout_description(message, description) await look_for_permalinks(message)
async def load_user_presets(self): all_files = self._data_dir.glob( f"*.{VersionedPreset.file_extension()}") user_presets = await asyncio.gather( *[VersionedPreset.from_file(f) for f in all_files]) for preset in typing.cast(List[VersionedPreset], user_presets): if preset.is_for_known_game(): self.custom_presets[preset.uuid] = preset
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)
def refresh_presets_command_logic(args): for game in enum_lib.iterate_enum(RandovaniaGame): logging.info(f"Refreshing presets for {game.long_name}") base_path = game.data_path.joinpath("presets") for preset_relative_path in game.data.presets: preset_path = base_path.joinpath(preset_relative_path["path"]) preset = VersionedPreset.from_file_sync(preset_path) preset.ensure_converted() preset.save_to_file(preset_path)
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 __init__(self, data_dir: Optional[Path]): self.logger = logging.getLogger("PresetManager") self.included_presets = { preset.uuid: preset for preset in [VersionedPreset.from_file_sync(f) for f in read_preset_list()] } self.custom_presets = {} if data_dir is not None: self._data_dir = data_dir else: self._data_dir = None
async def reply_for_preset(message: discord.Message, versioned_preset: VersionedPreset): try: preset = versioned_preset.get_preset() except ValueError as e: logging.info("Invalid preset '{}' from {}: {}".format(versioned_preset.name, message.author.display_name, e)) return embed = discord.Embed(title=preset.name, description=preset.description) _add_preset_description_to_embed(embed, preset) await message.reply(embed=embed, mention_author=False)
def test_elements_init(skip_qtbot, test_files_dir): preset_path = test_files_dir.joinpath( "presets/super_test_preset.rdvpreset") preset = VersionedPreset.from_file_sync(preset_path).get_preset() assert isinstance(preset.configuration, SuperMetroidConfiguration) editor = PresetEditor(preset) super_patches_tab = PresetSuperPatchConfiguration(editor) skip_qtbot.addWidget(super_patches_tab) # Test whether visual elements are initialized correctly patches = preset.configuration.patches for field_name, checkbox in super_patches_tab.checkboxes.items(): assert checkbox.isChecked() == getattr(patches, field_name)
def dragEnterEvent(self, event: QtGui.QDragEnterEvent): from randovania.layout.versioned_preset import VersionedPreset valid_extensions = [ LayoutDescription.file_extension(), VersionedPreset.file_extension(), ] valid_extensions_with_dot = { f".{extension}" for extension in valid_extensions } for url in event.mimeData().urls(): ext = os.path.splitext(url.toLocalFile())[1] if ext in valid_extensions_with_dot: event.acceptProposedAction() return
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()))
def _load_previous_state( persistence_path: Path, game_configuration: BaseConfiguration, ) -> Optional[dict]: previous_layout_path = _persisted_preset_path(persistence_path) try: previous_configuration = VersionedPreset.from_file_sync( previous_layout_path).get_preset().configuration except (FileNotFoundError, json.JSONDecodeError, InvalidPreset): return None if previous_configuration != game_configuration: return None previous_state_path = persistence_path.joinpath("state.json") try: with previous_state_path.open() as previous_state_file: return json.load(previous_state_file) except (FileNotFoundError, json.JSONDecodeError): return None
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)
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 test_included_presets_are_valid(f): VersionedPreset.from_file_sync(f).get_preset()
def all_presets(self) -> List[Preset]: return [ VersionedPreset(json.loads(preset.preset)).get_preset() for preset in sorted(self.presets, key=lambda it: it.row) ]
def _file_name_for_preset(self, preset: VersionedPreset) -> Path: return self._data_dir.joinpath("{}.{}".format(preset.uuid, preset.file_extension()))
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