示例#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 _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
示例#7
0
    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
示例#8
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)
示例#9
0
    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"],
        )
示例#10
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
示例#11
0
    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"]
            ],
        )
示例#12
0
    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)
示例#13
0
 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
示例#14
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)
示例#15
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
示例#17
0
    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
示例#18
0
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)
示例#19
0
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)
示例#20
0
    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
示例#21
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}")
示例#22
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()))
示例#23
0
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)
示例#25
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)
def test_included_presets_are_valid(f):
    VersionedPreset.from_file_sync(f).get_preset()
示例#27
0
 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)
     ]
示例#28
0
 def _file_name_for_preset(self, preset: VersionedPreset) -> Path:
     return self._data_dir.joinpath("{}.{}".format(preset.uuid,
                                                   preset.file_extension()))
示例#29
0
 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