Core: provide a way to add to CollectionState init and copy

SM: use that way
OoT: use that way
This commit is contained in:
Fabian Dill
2022-02-17 07:07:34 +01:00
parent c525c80b49
commit daea0f3e5e
8 changed files with 125 additions and 153 deletions

View File

@@ -31,7 +31,7 @@ from BaseClasses import MultiWorld, CollectionState, RegionType
from Options import Range, Toggle, OptionList
from Fill import fill_restrictive, FillError
from worlds.generic.Rules import exclusion_rules
from ..AutoWorld import World
from ..AutoWorld import World, AutoLogicRegister
location_id_offset = 67000
@@ -39,6 +39,33 @@ location_id_offset = 67000
i_o_limiter = threading.Semaphore(2)
class OOTCollectionState(metaclass=AutoLogicRegister):
def init_mixin(self, parent: MultiWorld):
all_ids = parent.get_all_ids()
self.child_reachable_regions = {player: set() for player in all_ids}
self.adult_reachable_regions = {player: set() for player in all_ids}
self.child_blocked_connections = {player: set() for player in all_ids}
self.adult_blocked_connections = {player: set() for player in all_ids}
self.day_reachable_regions = {player: set() for player in all_ids}
self.dampe_reachable_regions = {player: set() for player in all_ids}
self.age = {player: None for player in all_ids}
def copy_mixin(self, ret) -> CollectionState:
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
self.child_reachable_regions}
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.adult_reachable_regions}
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
self.child_blocked_connections}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
self.adult_blocked_connections}
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.day_reachable_regions}
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.dampe_reachable_regions}
return ret
class OOTWorld(World):
"""
The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods,
@@ -56,55 +83,10 @@ class OOTWorld(World):
data_version = 1
def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__
orig_copy = CollectionState.copy
def oot_init(self, parent: MultiWorld):
orig_init(self, parent)
self.child_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.day_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.dampe_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.age = {player: None for player in range(1, parent.players + 1)}
def oot_copy(self):
ret = orig_copy(self)
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
return ret
CollectionState.__init__ = oot_init
CollectionState.copy = oot_copy
# also need to add the names to the passed MultiWorld's CollectionState, since it was initialized before we could get to it
if world:
world.state.child_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.day_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.dampe_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.age = {player: None for player in range(1, world.players + 1)}
return super().__new__(cls)
def __init__(self, world, player):
self.hint_data_available = threading.Event()
super(OOTWorld, self).__init__(world, player)
def generate_early(self):
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
@@ -217,7 +199,8 @@ class OOTWorld(World):
self.shopsanity = str(self.shop_slots)
# fixing some options
self.starting_tod = self.starting_tod.replace('_', '-') # Fixes starting time spelling: "witching_hour" -> "witching-hour"
# Fixes starting time spelling: "witching_hour" -> "witching-hour"
self.starting_tod = self.starting_tod.replace('_', '-')
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
# Get hint distribution
@@ -258,14 +241,14 @@ class OOTWorld(World):
# Determine items which are not considered advancement based on settings. They will never be excluded.
self.nonadvancement_items = {'Double Defense', 'Ice Arrows'}
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
# nayru's love may be required to prevent forced damage
self.nonadvancement_items.add('Nayrus Love')
if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony':
# Stone of Agony skippable if not used for hints or grottos
self.nonadvancement_items.add('Stone of Agony')
if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
not self.warp_songs and not self.spawn_positions):
# Serenade and Prelude are never required unless one of those settings is enabled
self.nonadvancement_items.add('Serenade of Water')
@@ -277,7 +260,7 @@ class OOTWorld(World):
if not getattr(self, 'logic_water_central_gs_fw', False):
# Farore's Wind skippable if not used for this logic trick in Water Temple
self.nonadvancement_items.add('Farores Wind')
def load_regions_from_json(self, file_path):
region_json = read_json(file_path)
@@ -428,8 +411,9 @@ class OOTWorld(World):
def create_item(self, name: str):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False))
return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
None) else False))
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
def make_event_item(self, name, location, item=None):
@@ -507,7 +491,8 @@ class OOTWorld(World):
shuffle_random_entrances(self)
except EntranceShuffleError as e:
tries -= 1
logging.getLogger('').debug(f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
logger.debug(
f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
if tries == 0:
raise e
# Restore original state and delete assumed entrances
@@ -586,8 +571,10 @@ class OOTWorld(World):
"Spirit Temple Twinrova Heart",
"Song from Impa",
"Sheik in Ice Cavern",
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists
# only one exists
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest",
# only one exists
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest",
]
# Place/set rules for dungeon items
@@ -612,7 +599,7 @@ class OOTWorld(World):
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
if loc.item is None and (
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
if itempools['dungeon']: # only do this if there's anything to shuffle
for item in itempools['dungeon']:
self.world.itempool.remove(item)
@@ -623,28 +610,32 @@ class OOTWorld(World):
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
if self.shuffle_fortresskeys == 'any_dungeon':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
self.world.itempool)
itempools['any_dungeon'].extend(fortresskeys)
if itempools['any_dungeon']:
for item in itempools['any_dungeon']:
self.world.itempool.remove(item)
itempools['any_dungeon'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
itempools['any_dungeon'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
self.world.random.shuffle(any_dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
itempools['any_dungeon'], True, True)
# If anything is overworld-only, fill into local non-dungeon locations
if self.shuffle_fortresskeys == 'overworld':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
self.world.itempool)
itempools['overworld'].extend(fortresskeys)
if itempools['overworld']:
for item in itempools['overworld']:
self.world.itempool.remove(item)
itempools['overworld'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')]
itempools['overworld'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
non_dungeon_locations = [loc for loc in self.get_locations() if
not loc.item and loc not in any_dungeon_locations
and loc.type != 'Shop' and (
loc.type != 'Song' or self.shuffle_song_items != 'song')]
self.world.random.shuffle(non_dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
itempools['overworld'], True, True)
@@ -666,8 +657,8 @@ class OOTWorld(World):
for song in songs:
self.world.itempool.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
song_order = {
'Zeldas Lullaby': 1,
'Eponas Song': 1,
@@ -709,15 +700,17 @@ class OOTWorld(World):
# Place shop items
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
if self.shopsanity != 'off':
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
shop_items = list(
filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.world.get_unfilled_locations(player=self.player)))
shop_items.sort(key=lambda item: {
'Buy Deku Shield': 3*int(self.open_forest == 'closed'),
'Buy Goron Tunic': 2,
'Buy Deku Shield': 3 * int(self.open_forest == 'closed'),
'Buy Goron Tunic': 2,
'Buy Zora Tunic': 2
}.get(item.name, int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
}.get(item.name,
int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
self.world.random.shuffle(shop_locations)
for item in shop_items:
self.world.itempool.remove(item)
@@ -812,13 +805,13 @@ class OOTWorld(World):
@classmethod
def stage_generate_output(cls, world: MultiWorld, output_directory: str):
def hint_type_players(hint_type: str) -> set:
return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time")
return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time")
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
try:
item_hint_players = hint_type_players('item')
item_hint_players = hint_type_players('item')
barren_hint_players = hint_type_players('barren')
woth_hint_players = hint_type_players('woth')
woth_hint_players = hint_type_players('woth')
items_by_region = {}
for player in barren_hint_players:
@@ -834,12 +827,12 @@ class OOTWorld(World):
for loc in world.get_locations():
player = loc.item.player
autoworld = world.worlds[player]
if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])):
autoworld.major_item_locations.append(loc)
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(loc.item.type == 'Song' or
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(loc.item.type == 'Song' or
(loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
(loc.item.type == 'FortressSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
(loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
@@ -870,7 +863,8 @@ class OOTWorld(World):
if not world.can_beat_game(state):
world.worlds[player].required_locations.append(loc)
for player in barren_hint_players:
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']}
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items()
if info['is_barren']}
except Exception as e:
raise e
finally:
@@ -886,7 +880,7 @@ class OOTWorld(World):
hint_entrances.add(entrance[2][0])
def get_entrance_to_region(region):
if region.name == 'Root':
if region.name == 'Root':
return None
for entrance in region.entrances:
if entrance.name in hint_entrances:
@@ -900,7 +894,8 @@ class OOTWorld(World):
try:
multidata["precollected_items"][self.player].remove(item_id)
except ValueError as e:
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
logger.warning(
f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
# Add ER hint data
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
@@ -913,15 +908,15 @@ class OOTWorld(World):
er_hint_data[location.address] = main_entrance.name
multidata['er_hint_data'][self.player] = er_hint_data
# Helper functions
def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
(type == None or entrance.type == type) and
(not only_primary or entrance.primary))]
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
(type == None or entrance.type == type) and
(not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled]
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
entrance.shuffled]
def get_locations(self):
for region in self.regions: