diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 4721c58d..2a0130b1 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -14,6 +14,9 @@ _not_ used for logical access (the seed will never require you to catch somethin - Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize event tickets. +- Fixed handling of shuffle option for badges/HMs in the case that the player sets those items to nonlocal or uses +plando to put an item in one of those locations, or in the case that fill gets itself stuck on these items and has to +retry. # 2.3.0 diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 9996bfc6..c1875710 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -8,7 +8,7 @@ import os import pkgutil from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union -from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType +from BaseClasses import CollectionState, ItemClassification, MultiWorld, Tutorial, LocationProgressType from Fill import FillError, fill_restrictive from Options import OptionError, Toggle import settings @@ -100,6 +100,7 @@ class PokemonEmeraldWorld(World): required_client_version = (0, 4, 6) + item_pool: List[PokemonEmeraldItem] badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] free_fly_location_id: int @@ -185,7 +186,7 @@ class PokemonEmeraldWorld(World): # In race mode we don't patch any item location information into the ROM if self.multiworld.is_race and not self.options.remote_items: - logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.", + logging.warning("Pokemon Emerald: Forcing player %s (%s) to use remote items due to race mode.", self.player, self.player_name) self.options.remote_items.value = Toggle.option_true @@ -197,7 +198,7 @@ class PokemonEmeraldWorld(World): # Prevent setting the number of required legendaries higher than the number of enabled legendaries if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value): - logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed " + logging.warning("Pokemon Emerald: Legendary hunt count for player %s (%s) higher than number of allowed " "legendary encounters. Reducing to number of allowed encounters.", self.player, self.player_name) self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value) @@ -234,10 +235,17 @@ class PokemonEmeraldWorld(World): max_norman_count = 4 if self.options.norman_count.value > max_norman_count: - logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " + logging.warning("Pokemon Emerald: Norman requirements for player %s (%s) are unsafe in combination with " "other settings. Reducing to 4.", self.player, self.player_name) self.options.norman_count.value = max_norman_count + # Shuffled badges/hms will always be placed locally, so add them to local_items + if self.options.badges == RandomizeBadges.option_shuffle: + self.options.local_items.value.update(self.item_name_groups["Badge"]) + + if self.options.hms == RandomizeHms.option_shuffle: + self.options.local_items.value.update(self.item_name_groups["HM"]) + def create_regions(self) -> None: from .regions import create_regions all_regions = create_regions(self) @@ -377,12 +385,11 @@ class PokemonEmeraldWorld(World): item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories] default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations] - # Take the itempool as-is if self.options.item_pool_type == ItemPoolType.option_shuffled: - self.multiworld.itempool += default_itempool - - # Recreate the itempool from random items + # Take the itempool as-is + self.item_pool = default_itempool elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced): + # Recreate the itempool from random items item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone", "Money", "TM", "Held", "Misc", "Berry"] @@ -392,6 +399,7 @@ class PokemonEmeraldWorld(World): if not item.advancement: item_category_counter.update([tag for tag in item.tags if tag in item_categories]) + self.item_pool = [] item_category_weights = [item_category_counter.get(category) for category in item_categories] item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights] @@ -436,19 +444,10 @@ class PokemonEmeraldWorld(World): item_code = self.random.choice(fill_item_candidates_by_category[category]) item = self.create_item_by_code(item_code) - self.multiworld.itempool.append(item) + self.item_pool.append(item) - def set_rules(self) -> None: - from .rules import set_rules - set_rules(self) + self.multiworld.itempool += self.item_pool - def generate_basic(self) -> None: - # Create auth - # self.auth = self.random.randbytes(16) # Requires >=3.9 - self.auth = self.random.getrandbits(16 * 8).to_bytes(16, "little") - - randomize_types(self) - randomize_wild_encounters(self) set_free_fly(self) set_legendary_cave_entrances(self) @@ -475,9 +474,20 @@ class PokemonEmeraldWorld(World): if not self.options.key_items: convert_unrandomized_items_to_events(LocationCategory.KEY) - def pre_fill(self) -> None: - # Badges and HMs that are set to shuffle need to be placed at - # their own subset of locations + def set_rules(self): + from .rules import set_rules + set_rules(self) + + def connect_entrances(self): + randomize_wild_encounters(self) + self.shuffle_badges_hms() + # For entrance randomization, disconnect entrances here, randomize map, then + # undo badge/HM placement and re-shuffle them in the new map. + + def shuffle_badges_hms(self) -> None: + my_progression_items = [item for item in self.item_pool if item.advancement] + my_locations = list(self.get_locations()) + if self.options.badges == RandomizeBadges.option_shuffle: badge_locations: List[PokemonEmeraldLocation] badge_items: List[PokemonEmeraldItem] @@ -502,41 +512,20 @@ class PokemonEmeraldWorld(World): badge_priority["Knuckle Badge"] = 0 badge_items.sort(key=lambda item: badge_priority.get(item.name, 0)) - # Un-exclude badge locations, since we need to put progression items on them - for location in badge_locations: - location.progress_type = LocationProgressType.DEFAULT \ - if location.progress_type == LocationProgressType.EXCLUDED \ - else location.progress_type - - collection_state = self.multiworld.get_all_state(False) - - # If HM shuffle is on, HMs are not placed and not in the pool, so - # `get_all_state` did not contain them. Collect them manually for - # this fill. We know that they will be included in all state after - # this stage. + # Build state + state = CollectionState(self.multiworld) + for item in my_progression_items: + state.collect(item, True) + # If HM shuffle is on, HMs are neither placed in locations nor in + # the item pool, so we also need to collect them. if self.hm_shuffle_info is not None: for _, item in self.hm_shuffle_info: - collection_state.collect(item) + state.collect(item, True) + state.sweep_for_advancements(my_locations) - # In specific very constrained conditions, fill_restrictive may run - # out of swaps before it finds a valid solution if it gets unlucky. - # This is a band-aid until fill/swap can reliably find those solutions. - attempts_remaining = 2 - while attempts_remaining > 0: - attempts_remaining -= 1 - self.random.shuffle(badge_locations) - try: - fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items, - single_player_placement=True, lock=True, allow_excluded=True) - break - except FillError as exc: - if attempts_remaining == 0: - raise exc + # Shuffle badges + self.fill_subset_with_retries(badge_items, badge_locations, state) - logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.") - continue - - # Badges are guaranteed to be either placed or in the multiworld's itempool now if self.options.hms == RandomizeHms.option_shuffle: hm_locations: List[PokemonEmeraldLocation] hm_items: List[PokemonEmeraldItem] @@ -559,33 +548,56 @@ class PokemonEmeraldWorld(World): if self.options.badges == RandomizeBadges.option_vanilla and \ self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave): hm_priority["HM05 Flash"] = 0 - hm_items.sort(key=lambda item: hm_priority.get(item.name, 0)) + hm_items.sort(key=lambda item: hm_priority.get(item.name, 0), reverse=True) - # Un-exclude HM locations, since we need to put progression items on them - for location in hm_locations: - location.progress_type = LocationProgressType.DEFAULT \ - if location.progress_type == LocationProgressType.EXCLUDED \ - else location.progress_type + # Build state + # Badges are either in the item pool, or already placed and collected during sweep + state = CollectionState(self.multiworld) + for item in my_progression_items: + state.collect(item, True) + state.sweep_for_advancements(my_locations) - collection_state = self.multiworld.get_all_state(False) + # Shuffle HMs + self.fill_subset_with_retries(hm_items, hm_locations, state) - # In specific very constrained conditions, fill_restrictive may run - # out of swaps before it finds a valid solution if it gets unlucky. - # This is a band-aid until fill/swap can reliably find those solutions. - attempts_remaining = 2 - while attempts_remaining > 0: - attempts_remaining -= 1 - self.random.shuffle(hm_locations) - try: - fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items, - single_player_placement=True, lock=True, allow_excluded=True) - break - except FillError as exc: - if attempts_remaining == 0: - raise exc + def fill_subset_with_retries(self, items: list[PokemonEmeraldItem], locations: list[PokemonEmeraldLocation], state: CollectionState): + # Un-exclude locations, since we need to put progression items on them + for location in locations: + location.progress_type = LocationProgressType.DEFAULT \ + if location.progress_type == LocationProgressType.EXCLUDED \ + else location.progress_type - logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.") - continue + # In specific very constrained conditions, `fill_restrictive` may run + # out of swaps before it finds a valid solution if it gets unlucky. + attempts_remaining = 2 + while attempts_remaining > 0: + attempts_remaining -= 1 + locations_copy = locations.copy() + items_copy = items.copy() + self.random.shuffle(locations_copy) + try: + fill_restrictive(self.multiworld, state, locations_copy, items_copy, single_player_placement=True, + lock=True) + break + except FillError as exc: + if attempts_remaining <= 0: + raise exc + + # Undo partial item placement + for location in locations: + location.locked = False + if location.item is not None: + location.item.location = None + location.item = None + + logging.debug(f"Failed to shuffle items for player {self.player} ({self.player_name}). Retrying.") + continue + + def generate_basic(self) -> None: + # Create auth + self.auth = self.random.randbytes(16) + + randomize_types(self) def generate_output(self, output_directory: str) -> None: self.modified_trainers = copy.deepcopy(emerald_data.trainers)