mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
Pokemon Emerald: Fix pre-fill problems (#4686)
Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user