From 9ec0680ce5bc32d3cc3c4c0f9f2fea6aaf17aaab Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 10 Aug 2021 09:03:44 +0200 Subject: [PATCH] LttP: move game specific fill to new AutoWorld fill_hook --- Fill.py | 54 ++------------ Main.py | 6 +- worlds/AutoWorld.py | 10 ++- worlds/alttp/Options.py | 1 + worlds/alttp/__init__.py | 139 +++++++++++++++++++++++++------------ worlds/subnautica/Rules.py | 1 - 6 files changed, 111 insertions(+), 100 deletions(-) diff --git a/Fill.py b/Fill.py index 91ace4c0..940865e4 100644 --- a/Fill.py +++ b/Fill.py @@ -7,6 +7,7 @@ from BaseClasses import CollectionState, Location, MultiWorld from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import key_drop_data from worlds.generic import PlandoItem +from worlds.AutoWorld import call_all class FillError(RuntimeError): @@ -69,7 +70,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool.extend(unplaced_items) -def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None): +def distribute_items_restrictive(world: MultiWorld, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: fill_locations = world.get_unfilled_locations() @@ -92,51 +93,9 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo else: restitempool.append(item) - standard_keyshuffle_players = set() - - # fill in gtower locations with trash first - for player in world.get_game_players("A Link to the Past"): - if not gftower_trash or not world.ganonstower_vanilla[player] or \ - world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: - gtower_trash_count = 0 - elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): - gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2, - world.crystals_needed_for_gt[player] * 4) - else: - gtower_trash_count = world.random.randint(0, world.crystals_needed_for_gt[player] * 2) - - if gtower_trash_count: - gtower_locations = [location for location in fill_locations if - 'Ganons Tower' in location.name and location.player == player] - world.random.shuffle(gtower_locations) - trashcnt = 0 - localrest = localrestitempool[player] - if localrest: - gt_item_pool = restitempool + localrest - world.random.shuffle(gt_item_pool) - else: - gt_item_pool = restitempool.copy() - - while gtower_locations and gt_item_pool and trashcnt < gtower_trash_count: - spot_to_fill = gtower_locations.pop() - item_to_place = gt_item_pool.pop() - if item_to_place in localrest: - localrest.remove(item_to_place) - else: - restitempool.remove(item_to_place) - world.push_item(spot_to_fill, item_to_place, False) - fill_locations.remove(spot_to_fill) - trashcnt += 1 - if world.mode[player] == 'standard' and world.keyshuffle[player] is True: - standard_keyshuffle_players.add(player) - - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots - if standard_keyshuffle_players: - progitempool.sort( - key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and - item.player in standard_keyshuffle_players else 0) - world.random.shuffle(fill_locations) + call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations) + fill_restrictive(world, world.state, fill_locations, progitempool) if nonexcludeditempool: @@ -167,11 +126,8 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo unplaced = [item for item in progitempool + restitempool] unfilled = [location.name for location in fill_locations] - for location in fill_locations: - world.push_item(location, ItemFactory('Nothing', location.player), False) - if unplaced or unfilled: - logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') + raise FillError(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]: diff --git a/Main.py b/Main.py index d83c411b..3b0b4e16 100644 --- a/Main.py +++ b/Main.py @@ -218,12 +218,8 @@ def main(args, seed=None): if world.algorithm == 'flood': flood_items(world) # different algo, biased towards early game progress items - elif world.algorithm == 'vt25': - distribute_items_restrictive(world, False) - elif world.algorithm == 'vt26': - distribute_items_restrictive(world, True) elif world.algorithm == 'balanced': - distribute_items_restrictive(world, True) + distribute_items_restrictive(world) logger.info("Filling Shop Slots") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 71e64c35..d22ff52f 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import Dict, Set, Tuple +from typing import Dict, Set, Tuple, List -from BaseClasses import MultiWorld, Item, CollectionState +from BaseClasses import MultiWorld, Item, CollectionState, Location class AutoWorldRegister(type): @@ -126,6 +126,12 @@ class World(metaclass=AutoWorldRegister): """Optional method that is supposed to be used for special fill stages. This is run *after* plando.""" pass + def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item], + localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]): + """Special method that gets called as part of distribute_items_restrictive (main fill). + This gets called once per present world type.""" + pass + def generate_output(self, output_directory: str): """This method gets called from a threadpool, do not use world.random here. If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 26e4bab8..4132f2da 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -143,6 +143,7 @@ class HeartColor(Choice): # remove when this becomes a base Choice feature if text == "random": return cls(random.randint(0, 3)) + return super(HeartColor, cls).from_text(text) class QuickSwap(DefaultOnToggle): displayname = "L/R Quickswapping" diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index f93ba2a1..4019d006 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -184,63 +184,69 @@ class ALTTPWorld(World): def generate_output(self, output_directory: str): world = self.world player = self.player + try: + use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] + or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' + or world.shufflepots[player] or world.bush_shuffle[player] + or world.killable_thieves[player]) - use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] - or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or world.shufflepots[player] or world.bush_shuffle[player] - or world.killable_thieves[player]) + rom = LocalRom(world.alttp_rom) - rom = LocalRom(world.alttp_rom) + patch_rom(world, rom, player, use_enemizer) - patch_rom(world, rom, player, use_enemizer) + if use_enemizer: + patch_enemizer(world, player, rom, world.enemizer, output_directory) - if use_enemizer: - patch_enemizer(world, player, rom, world.enemizer, output_directory) + if world.is_race: + patch_race_rom(rom, world, player) - if world.is_race: - patch_race_rom(rom, world, player) + world.spoiler.hashes[player] = get_hash_string(rom.hash) - world.spoiler.hashes[player] = get_hash_string(rom.hash) + palettes_options = { + 'dungeon': world.uw_palettes[player], + 'overworld': world.ow_palettes[player], + 'hud': world.hud_palettes[player], + 'sword': world.sword_palettes[player], + 'shield': world.shield_palettes[player], + 'link': world.link_palettes[player] + } + palettes_options = {key: option.current_key for key, option in palettes_options.items()} - palettes_options = { - 'dungeon': world.uw_palettes[player], - 'overworld': world.ow_palettes[player], - 'hud': world.hud_palettes[player], - 'sword': world.sword_palettes[player], - 'shield': world.shield_palettes[player], - 'link': world.link_palettes[player] - } - palettes_options = {key: option.current_key for key, option in palettes_options.items()} + apply_rom_settings(rom, world.heartbeep[player].current_key, + world.heartcolor[player].current_key, + world.quickswap[player], + world.menuspeed[player].current_key, + world.music[player], + world.sprite[player], + palettes_options, world, player, True, + reduceflashing=world.reduceflashing[player] or world.is_race, + triforcehud=world.triforcehud[player].current_key) - apply_rom_settings(rom, world.heartbeep[player].current_key, - world.heartcolor[player].current_key, - world.quickswap[player], - world.menuspeed[player].current_key, - world.music[player], - world.sprite[player], - palettes_options, world, player, True, - reduceflashing=world.reduceflashing[player] or world.is_race, - triforcehud=world.triforcehud[player].current_key) + outfilepname = f'_P{player}' + outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ + if world.player_name[player] != 'Player%d' % player else '' - outfilepname = f'_P{player}' - outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ - if world.player_name[player] != 'Player%d' % player else '' - - rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') - rom.write_to_file(rompath, hide_enemizer=True) - Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player]) - os.unlink(rompath) - self.rom_name = rom.name - self.rom_name_available_event.set() + rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rom.write_to_file(rompath, hide_enemizer=True) + Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player]) + os.unlink(rompath) + self.rom_name = rom.name + except: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected def modify_multidata(self, multidata: dict): import base64 # wait for self.rom_name to be available. self.rom_name_available_event.wait() - new_name = base64.b64encode(bytes(self.rom_name)).decode() - payload = multidata["connect_names"][self.world.player_name[self.player]] - multidata["connect_names"][new_name] = payload - del (multidata["connect_names"][self.world.player_name[self.player]]) + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + payload = multidata["connect_names"][self.world.player_name[self.player]] + multidata["connect_names"][new_name] = payload + del (multidata["connect_names"][self.world.player_name[self.player]]) def get_required_client_version(self) -> tuple: return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) @@ -248,4 +254,51 @@ class ALTTPWorld(World): def create_item(self, name: str) -> Item: return ALttPItem(name, self.player, **as_dict_item_table[name]) + @classmethod + def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations): + trash_counts = {} + standard_keyshuffle_players = set() + for player in world.get_game_players("A Link to the Past"): + if world.mode[player] == 'standard' and world.keyshuffle[player] is True: + standard_keyshuffle_players.add(player) + if not world.ganonstower_vanilla[player] or \ + world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: + pass + elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): + trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2, + world.crystals_needed_for_gt[player] * 4) + else: + trash_counts[player] = world.random.randint(0, world.crystals_needed_for_gt[player] * 2) + # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots + if standard_keyshuffle_players: + progitempool.sort( + key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and + item.player in standard_keyshuffle_players else 0) + + if trash_counts: + locations_mapping = {player: [] for player in trash_counts} + for location in fill_locations: + if 'Ganons Tower' in location.name and location.player in locations_mapping: + locations_mapping[location.player].append(location) + + for player, trash_count in trash_counts.items(): + gtower_locations = locations_mapping[player] + world.random.shuffle(gtower_locations) + localrest = localrestitempool[player] + if localrest: + gt_item_pool = restitempool + localrest + world.random.shuffle(gt_item_pool) + else: + gt_item_pool = restitempool.copy() + + while gtower_locations and gt_item_pool and trash_count > 0: + spot_to_fill = gtower_locations.pop() + item_to_place = gt_item_pool.pop() + if item_to_place in localrest: + localrest.remove(item_to_place) + else: + restitempool.remove(item_to_place) + world.push_item(spot_to_fill, item_to_place, False) + fill_locations.remove(spot_to_fill) # very slow, unfortunately + trash_count -= 1 diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 90bac487..a19c42df 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -231,7 +231,6 @@ def set_location_rule(world, player, loc): def set_rules(world, player): - logging.warning(type(location_table)) for loc in location_table: set_location_rule(world, player, loc)