def apply_layout( description: LayoutDescription, players_config: PlayersConfiguration, cosmetic_patches: CosmeticPatches, backup_files_path: Optional[Path], progress_update: ProgressUpdateCallable, game_root: Path, ): """ Applies the modifications listed in the given LayoutDescription to the game in game_root. :param description: :param players_config: :param cosmetic_patches: :param game_root: :param backup_files_path: Path to use as pak backup, to remove/add menu mod. :param progress_update: :return: """ description.save_to_file( game_root.joinpath("files", f"randovania.{description.file_extension()}")) apply_patcher_file( game_root, backup_files_path, patcher_file.create_patcher_file(description, players_config, cosmetic_patches), description.all_patches[players_config.player_index].game_specific, progress_update)
def export_layout( layout: LayoutDescription, options: Options, ): """ Creates a seed log file for the given layout and saves it to the configured path :param layout: :param options: :return: """ output_json = options.output_directory.joinpath("{}.json".format( _output_name_for(layout))) if debug.debug_level() > 0: patcher = options.output_directory.joinpath("{}-patcher.json".format( _output_name_for(layout))) with patcher.open("w") as out_file: json.dump(patcher_file.create_patcher_file( layout, options.cosmetic_patches), out_file, indent=4, separators=(',', ': ')) # Save the layout to a file layout.save_to_file(output_json)
def persist_layout(data_dir: Path, description: LayoutDescription): history_dir = data_dir.joinpath("game_history") history_dir.mkdir(parents=True, exist_ok=True) date_format = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") file_path = history_dir.joinpath( f"{date_format}-{description.shareable_word_hash}.{description.file_extension()}") description.save_to_file(file_path)
def persist_layout(history_dir: Path, description: LayoutDescription): history_dir.mkdir(parents=True, exist_ok=True) games = "-".join(sorted(game.short_name for game in description.all_games)) date_format = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") file_path = history_dir.joinpath( f"{date_format}_{games}_{description.shareable_word_hash}.{description.file_extension()}") description.save_to_file(file_path)
def _create_patch_data(test_files_dir, mocker, in_file, out_file, cosmetic): # Setup f = test_files_dir.joinpath("log_files", "cave_story", f"{in_file}.rdvgame") description = LayoutDescription.from_file(f) players_config = PlayersConfiguration(0, {0: "Cave Story"}) mocker.patch( "randovania.layout.layout_description.LayoutDescription.shareable_hash_bytes", new_callable=PropertyMock, return_value=b'\x00\x00\x00\x00\x00') # Run data = CSPatchDataFactory(description, players_config, cosmetic).create_data() # Expected Result # strip mychar to just the filename rather than full path if data["mychar"] is not None: mychar = Path(data["mychar"]) data["mychar"] = mychar.name # Uncomment the following lines to update: # with test_files_dir.joinpath("caver_expected_data", f"{out_file}.json").open("w") as f: # json.dump(data, f) with test_files_dir.joinpath("caver_expected_data", f"{out_file}.json").open("r") as f: expected_data = json.load(f) assert data == expected_data
def _test_preset(rdvgame_file, expected_results_file, mocker): # Setup description = LayoutDescription.from_file(rdvgame_file) players_config = PlayersConfiguration(0, {0: "Prime", 1: "Echoes"}) cosmetic_patches = PrimeCosmeticPatches(use_hud_color=True, hud_color=(255, 0, 0), suit_color_rotations=(0, 40, 350, 12)) mocker.patch( "randovania.layout.layout_description.LayoutDescription.shareable_hash_bytes", new_callable=PropertyMock, return_value=b"\x00\x00\x00\x00\x00") # Run data = PrimePatchDataFactory(description, players_config, cosmetic_patches).create_data() # Expected Result with expected_results_file.open("r") as file: expected_data = json.load(file) # Uncomment to easily view diff of failed test # with expected_results_file.open("w") as file: # file.write(json.dumps(data, indent=4, separators=(',', ': '))) # Ignore the part of the main menu message which has the randovania version in it data["gameConfig"]["mainMenuMessage"] = data["gameConfig"][ "mainMenuMessage"].split("\n")[1] expected_data["gameConfig"]["mainMenuMessage"] = expected_data[ "gameConfig"]["mainMenuMessage"].split("\n")[1] assert data == expected_data
def test_apply_layout(mock_patch_game_name_and_id: MagicMock, mock_change_starting_spawn: MagicMock, mock_claris_apply_layout: MagicMock, empty_patches): # Setup layout = MagicMock(spec=LayoutDescription(version="0.15.0", permalink=MagicMock(), patches=empty_patches, solver_path=())) progress_update = MagicMock() options: Options = MagicMock() # Run simplified_patcher.apply_layout(layout, options, progress_update) # Assert mock_patch_game_name_and_id.assert_called_once_with( options.game_files_path, "Metroid Prime 2: Randomizer - {}".format(layout.shareable_hash)) mock_change_starting_spawn.assert_called_once_with( options.game_files_path, layout.patches.starting_location) mock_claris_apply_layout.assert_called_once_with( description=layout, cosmetic_patches=options.cosmetic_patches, game_root=options.game_files_path, backup_files_path=options.backup_files_path, progress_update=progress_update)
def export_layout( layout: LayoutDescription, options: Options, ): """ Creates a seed log file for the given layout and saves it to the configured path :param layout: :param options: :return: """ output_json = options.output_directory.joinpath("{}.json".format( _output_name_for(layout))) # Save the layout to a file layout.save_to_file(output_json)
def test_internal_patch_iso(mock_apply_layout: MagicMock, mock_pack_iso: MagicMock, mock_debug_level: MagicMock, empty_patches): # Setup mock_debug_level.return_value = 0 layout = MagicMock(spec=LayoutDescription(version="0.15.0", permalink=MagicMock(), patches=empty_patches, solver_path=())) layout.shareable_hash = "layout" options = MagicMock() options.output_directory = Path("fun") name = "Echoes Randomizer - layout" output_iso = Path("fun", name + ".iso") output_json = Path("fun", name + ".json") updaters = [MagicMock(), MagicMock()] # Run simplified_patcher._internal_patch_iso(updaters, layout, options) # Assert mock_apply_layout.assert_called_once_with(layout=layout, options=options, progress_update=updaters[0]) mock_pack_iso.assert_called_once_with(output_iso=output_iso, options=options, progress_update=updaters[1]) layout.save_to_file.assert_called_once_with(output_json)
def _create_description_mock(permalink: Permalink, empty_patches: GamePatches): return MagicMock(spec=LayoutDescription( version=randovania.VERSION, permalink=permalink, patches=empty_patches, solver_path=() ))
def test_GameSession_create_session_entry(clean_database, has_description, test_files_dir, mocker): # Setup description = LayoutDescription.from_file(test_files_dir.joinpath("log_files", "seed_a.rdvgame")) someone = database.User.create(name="Someone") s = database.GameSession.create(name="Debug", num_teams=1, creator=someone) game_details = None if has_description: s.layout_description = description s.save() game_details = { 'seed_hash': '5IENQWDS', 'spoiler': True, 'word_hash': 'Biostorage Cavern Watch', } # Run session = database.GameSession.get_by_id(1) result = session.create_session_entry() readable_result = construct_lib.convert_to_raw_python(BinaryGameSessionEntry.parse(result)) # Assert assert readable_result == { 'allowed_games': ['prime1', 'prime2'], 'game_details': game_details, 'generation_in_progress': None, 'id': 1, 'name': 'Debug', 'players': [], 'presets': [], 'state': 'setup', }
async def _create_description(generator_params: GeneratorParameters, status_update: Callable[[str], None], attempts: int, ) -> LayoutDescription: """ :param generator_params: :param status_update: :return: """ rng = Random(generator_params.as_bytes) presets = [ generator_params.get_preset(i) for i in range(generator_params.player_count) ] retrying = tenacity.AsyncRetrying( stop=tenacity.stop_after_attempt(attempts), retry=tenacity.retry_if_exception_type(UnableToGenerate), reraise=True ) filler_results = await retrying(_create_pools_and_fill, rng, presets, status_update) all_patches = _distribute_remaining_items(rng, filler_results.player_results) return LayoutDescription.create_new( generator_parameters=generator_params, all_patches=all_patches, item_order=filler_results.action_log, )
def _async_create_description( permalink: Permalink, status_update: Callable[[str], None], attempts: int, ) -> LayoutDescription: """ :param permalink: :param status_update: :return: """ rng = Random(permalink.as_bytes) presets = { i: permalink.get_preset(i) for i in range(permalink.player_count) } retrying = tenacity.Retrying( stop=tenacity.stop_after_attempt(attempts), retry=tenacity.retry_if_exception_type(UnableToGenerate), reraise=True) filler_results = retrying(_create_pools_and_fill, rng, presets, status_update) all_patches = _distribute_remaining_items(rng, filler_results.player_results) return LayoutDescription( permalink=permalink, version=VERSION, all_patches=all_patches, item_order=filler_results.action_log, )
def _async_create_description( permalink: Permalink, status_update: Callable[[str], None], ) -> LayoutDescription: """ :param permalink: :param status_update: :return: """ rng = Random(permalink.as_str) presets = { i: permalink.get_preset(i) for i in range(permalink.player_count) } filler_results = _retryable_create_patches(rng, presets, status_update) all_patches = _distribute_remaining_items(rng, filler_results.player_results) return LayoutDescription( permalink=permalink, version=VERSION, all_patches=all_patches, item_order=filler_results.action_log, )
def _create_test_layout_description( configuration: LayoutConfiguration, pickup_mapping: Iterable[int], ) -> LayoutDescription: """ Creates a LayoutDescription for the given configuration, with the patches being for the given pickup_mapping :param configuration: :param pickup_mapping: :return: """ game = data_reader.decode_data(configuration.game_data) pickup_database = game.pickup_database return LayoutDescription( version=VERSION, permalink=Permalink( seed_number=0, spoiler=True, patcher_configuration=PatcherConfiguration.default(), layout_configuration=configuration, ), patches=GamePatches.with_game(game).assign_new_pickups([ (PickupIndex(i), pickup_database.original_pickup_mapping[PickupIndex(new_index)]) for i, new_index in enumerate(pickup_mapping) ]), solver_path=())
def test_update_content(skip_qtbot, test_files_dir): # Setup description = LayoutDescription.from_file(test_files_dir.joinpath("log_files", "seed_a.rdvgame")) tab = TranslatorGateDetailsTab(None, RandovaniaGame.METROID_PRIME_ECHOES) # Run tab.update_content( description.get_preset(0).configuration, description.all_patches, PlayersConfiguration(0, {0: "You"}), ) # Assert counts = {} for i in range(tab.tree_widget.topLevelItemCount()): item = tab.tree_widget.topLevelItem(i) counts[item.text(0)] = item.childCount() assert counts == { 'Agon Wastes': 2, 'Great Temple': 3, 'Sanctuary Fortress': 2, 'Temple Grounds': 7, 'Torvus Bog': 3, }
def validate_command_logic(args): debug.set_level(args.debug) description = LayoutDescription.from_file(args.layout_file) if description.player_count != 1: raise ValueError( f"Validator does not support layouts with more than 1 player.") configuration = description.get_preset(0).configuration patches = description.all_patches[0] total_times = [] final_state_by_resolve = None for _ in range(args.repeat): before = time.perf_counter() final_state_by_resolve = asyncio.run( resolver.resolve(configuration=configuration, patches=patches)) after = time.perf_counter() total_times.append(after - before) print("Took {:.3f} seconds. Game is {}.".format( total_times[-1], "possible" if final_state_by_resolve is not None else "impossible")) if args.repeat > 1: cli_lib.print_report_multiple_times(total_times) if args.repeat < 1: raise ValueError("Expected at least 1 repeat") return 0 if final_state_by_resolve is not None else 1
async def reply_for_layout_description(message: discord.Message, description: LayoutDescription): embed = discord.Embed( title="Spoiler file for Randovania {}".format(description.randovania_version_text), ) if description.player_count == 1: preset = description.get_preset(0) embed.description = "{}, with preset {}.\nSeed Hash: {}\nPermalink: {}".format( preset.game.long_name, preset.name, description.shareable_word_hash, description.permalink.as_base64_str, ) _add_preset_description_to_embed(embed, preset) else: games = {preset.game.long_name for preset in description.all_presets} game_names = sorted(games) last_game = game_names.pop() games_text = ", ".join(game_names) if games_text: games_text += " and " games_text += last_game embed.description = "{} player multiworld for {}.\nSeed Hash: {}\nPermalink: {}".format( description.player_count, games_text, description.shareable_word_hash, description.permalink.as_base64_str, ) await message.reply(embed=embed, mention_author=False)
def __init__(self, description: LayoutDescription, players_config: PlayersConfiguration, cosmetic_patches: BaseCosmeticPatches): self.description = description self.players_config = players_config self.cosmetic_patches = cosmetic_patches self.item_db = default_database.item_database_for_game( self.game_enum()) self.patches = description.all_patches[players_config.player_index] self.configuration = description.get_preset( players_config.player_index).configuration self.rng = Random( description.get_seed_for_player(players_config.player_index)) self.game = filtered_database.game_description_for_layout( self.configuration)
def apply_layout(description: LayoutDescription, cosmetic_patches: CosmeticPatches, backup_files_path: Path, progress_update: ProgressUpdateCallable, game_root: Path): """ Applies the modifications listed in the given LayoutDescription to the game in game_root. :param cosmetic_patches: :param description: :param game_root: :param backup_files_path: Path to use as pak backup, to remove/add menu mod. :param progress_update: :return: """ patcher_configuration = description.permalink.patcher_configuration args = _base_args( game_root, hud_memo_popup_removal=cosmetic_patches.disable_hud_popup) status_update = status_update_lib.create_progress_update_from_successive_messages( progress_update, 400 if patcher_configuration.menu_mod else 100) _ensure_no_menu_mod(game_root, backup_files_path, status_update) _create_pak_backups(game_root, backup_files_path, status_update) indices = _calculate_indices(description) args += [ "-s", str(description.permalink.seed_number), "-p", ",".join(str(index) for index in indices), ] layout_configuration = description.permalink.layout_configuration if not is_vanilla_starting_location(layout_configuration): args.append("-i") if layout_configuration.elevators == LayoutRandomizedFlag.RANDOMIZED: args.append("-v") if cosmetic_patches.speed_up_credits: args.append("-c") if description.permalink.patcher_configuration.warp_to_start: args.append("-t") description.save_to_file(game_root.joinpath("files", "randovania.json")) _run_with_args(args, "Randomized!", status_update) if patcher_configuration.menu_mod: _add_menu_mod_to_files(game_root, status_update)
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)
def test_apply_layout( mock_ensure_no_menu_mod: MagicMock, mock_create_pak_backups: MagicMock, mock_add_menu_mod_to_files: MagicMock, mock_modern_api: MagicMock, mock_create_progress_update_from_successive_messages: MagicMock, mock_save_to_file: MagicMock, include_menu_mod: bool, has_backup_path: bool, ): # Setup cosmetic_patches = MagicMock() description = LayoutDescription( version=randovania.VERSION, permalink=Permalink(seed_number=1, spoiler=False, patcher_configuration=PatcherConfiguration( menu_mod=include_menu_mod, warp_to_start=MagicMock(), ), layout_configuration=MagicMock()), patches=MagicMock(), solver_path=(), ) game_root = MagicMock(spec=Path()) backup_files_path = MagicMock() if has_backup_path else None progress_update = MagicMock() status_update = mock_create_progress_update_from_successive_messages.return_value # Run claris_randomizer.apply_layout(description, cosmetic_patches, backup_files_path, progress_update, game_root) # Assert mock_create_progress_update_from_successive_messages.assert_called_once_with( progress_update, 400 if include_menu_mod else 100) mock_ensure_no_menu_mod.assert_called_once_with(game_root, backup_files_path, status_update) if has_backup_path: mock_create_pak_backups.assert_called_once_with( game_root, backup_files_path, status_update) else: mock_create_pak_backups.assert_not_called() game_root.joinpath.assert_called_once_with("files", "randovania.json") mock_save_to_file.assert_called_once_with(description, game_root.joinpath.return_value) mock_modern_api.assert_called_once_with(game_root, status_update, description, cosmetic_patches) if include_menu_mod: mock_add_menu_mod_to_files.assert_called_once_with( game_root, status_update) else: mock_add_menu_mod_to_files.assert_not_called()
def prompt_user_for_input_game_log(window: QMainWindow) -> Optional[Path]: """ Shows an QFileDialog asking the user for a Randovania seed log :param window: :return: A string if the user selected a file, None otherwise """ return _prompt_user_for_file(window, caption="Select a Randovania seed log.", filter="Randovania Game, *.{}".format(LayoutDescription.file_extension()), new_file=False)
def show_game_details(app: QApplication, options, game: Path): from randovania.layout.layout_description import LayoutDescription from randovania.gui.seed_details_window import SeedDetailsWindow layout = LayoutDescription.from_file(game) details_window = SeedDetailsWindow(None, options) details_window.update_layout_description(layout) details_window.show() app.details_window = details_window
def show_game_details(app: QtWidgets.QApplication, options, game: Path): from randovania.layout.layout_description import LayoutDescription from randovania.gui.game_details.game_details_window import GameDetailsWindow layout = LayoutDescription.from_file(game) details_window = GameDetailsWindow(None, options) details_window.update_layout_description(layout) logger.info("Displaying game details") details_window.show() app.details_window = details_window
async def test_create_patches( mock_random: MagicMock, mock_distribute_remaining_items: MagicMock, mock_create_player_pool: MagicMock, mock_validate_item_pool_size: MagicMock, mocker, ): # Setup filler_result = MagicMock() mock_run_filler: AsyncMock = mocker.patch( "randovania.generator.generator.run_filler", new_callable=AsyncMock, return_value=filler_result) mock_dock_weakness_distributor: AsyncMock = mocker.patch( "randovania.generator.dock_weakness_distributor.distribute_post_fill_weaknesses", new_callable=AsyncMock, return_value=filler_result) mock_distribute_remaining_items.return_value = filler_result num_players = 1 rng = mock_random.return_value status_update: Union[MagicMock, Callable[[str], None]] = MagicMock() player_pools = [MagicMock() for _ in range(num_players)] presets = [MagicMock() for _ in range(num_players)] mock_create_player_pool.side_effect = player_pools generator_parameters = MagicMock() generator_parameters.get_preset.side_effect = lambda i: presets[i] # Run result = await generator._create_description(generator_parameters, status_update, 0) # Assert mock_random.assert_called_once_with(generator_parameters.as_bytes) mock_create_player_pool.assert_has_calls([ call(rng, presets[i].configuration, i, num_players) for i in range(num_players) ]) mock_validate_item_pool_size.assert_has_calls([ call(player_pools[i].pickups, player_pools[i].game, player_pools[i].configuration) for i in range(num_players) ]) mock_run_filler.assert_awaited_once_with( rng, [player_pools[i] for i in range(num_players)], status_update) mock_distribute_remaining_items.assert_called_once_with(rng, filler_result) mock_dock_weakness_distributor.assert_called_once_with( rng, filler_result, status_update) assert result == LayoutDescription.create_new( generator_parameters=generator_parameters, all_patches={}, item_order=filler_result.action_log, )
def test_round_trip_default(permalink: Permalink, item_locations: Dict[str, Dict[str, str]], solver_path: Tuple[SolverPath, ...]): game = data_reader.decode_data(permalink.layout_configuration.game_data) original = LayoutDescription( version=randovania.VERSION, permalink=permalink, patches=GamePatches( _item_locations_to_pickup_assignment(game, item_locations), claris_randomizer.elevator_connections_for_seed_number( permalink.seed_number), {}, {}, (), game.starting_location), solver_path=solver_path, ) # Run decoded = LayoutDescription.from_json_dict(original.as_json) # Assert assert decoded == original
def __init__(self, json_path: Path): super().__init__() self.setupUi(self) set_default_window_icon(self) self.layout_description = LayoutDescription.from_file(json_path) # Keep the Layout Description visualizer ready, but invisible. self._create_pickup_spoilers() # And update self.update_layout_description(self.layout_description)
def generate_list(permalink: Permalink, status_update: Optional[Callable[[str], None]], timeout: Optional[int] = 120) -> LayoutDescription: if status_update is None: status_update = id data = permalink.layout_configuration.game_data create_patches_params = { "permalink": permalink, "game": data_reader.decode_data(data, False), "status_update": status_update } resolver_game = data_reader.decode_data(data) def create_failure(message: str): return GenerationFailure(message, permalink=permalink) new_patches = None final_state_by_resolve = None with multiprocessing.dummy.Pool(1) as dummy_pool: patches_async = dummy_pool.apply_async(func=_create_patches, kwds=create_patches_params) try: new_patches = patches_async.get(timeout) except multiprocessing.TimeoutError: raise create_failure("Timeout reached when generating patches.") resolve_params = { "configuration": permalink.layout_configuration, "game": resolver_game, "patches": new_patches, "status_update": status_update, } final_state_async = dummy_pool.apply_async(func=resolver.resolve, kwds=resolve_params) try: final_state_by_resolve = final_state_async.get(60) except multiprocessing.TimeoutError: raise create_failure("Timeout reached when validating possibility") if final_state_by_resolve is None: # Why is final_state_by_distribution not OK? raise create_failure( "Generated seed was considered impossible by the solver") else: solver_path = _state_to_solver_path(final_state_by_resolve, resolver_game) return LayoutDescription(permalink=permalink, version=VERSION, patches=new_patches, solver_path=solver_path)
def apply_layout( description: LayoutDescription, players_config: PlayersConfiguration, cosmetic_patches: CosmeticPatches, backup_files_path: Optional[Path], progress_update: ProgressUpdateCallable, game_root: Path, ): """ Applies the modifications listed in the given LayoutDescription to the game in game_root. :param description: :param players_config: :param cosmetic_patches: :param game_root: :param backup_files_path: Path to use as pak backup, to remove/add menu mod. :param progress_update: :return: """ patcher_configuration = description.permalink.get_preset( players_config.player_index).patcher_configuration status_update = status_update_lib.create_progress_update_from_successive_messages( progress_update, 400 if patcher_configuration.menu_mod else 100) _ensure_no_menu_mod(game_root, backup_files_path, status_update) if backup_files_path is not None: _create_pak_backups(game_root, backup_files_path, status_update) description.save_to_file( game_root.joinpath("files", f"randovania.{description.file_extension()}")) _modern_api(game_root, status_update, description, players_config, cosmetic_patches) dol_patcher.apply_patches( game_root, description.all_patches[players_config.player_index], cosmetic_patches) if patcher_configuration.menu_mod: _add_menu_mod_to_files(game_root, status_update)