OoT Time Optimization (#2401)

- Entrance randomizer no longer grows with multiworld
- Improved ER success rate again by prioritizing Temple of Time even more
- Prefill is faster, has slightly reduced failure rate when map/compass are in dungeon but previous items in any_dungeon (which consumed all available locations), no longer removes items from the main itempool; itemlinked prefill items removed to accomodate improvements
- Now triggers only one recache after `generate_basic` instead of one per oot world
- Avoids recaches during `create_regions`
- All ER temp entrances have unique names (so the entrance cache does not break)
This commit is contained in:
espeon65536 2023-10-29 21:05:49 -06:00 committed by GitHub
parent f81e72686a
commit db978aa48a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 146 deletions

View File

@ -1,6 +1,4 @@
from BaseClasses import Entrance from BaseClasses import Entrance
from .Regions import TimeOfDay
class OOTEntrance(Entrance): class OOTEntrance(Entrance):
game: str = 'Ocarina of Time' game: str = 'Ocarina of Time'
@ -29,16 +27,16 @@ class OOTEntrance(Entrance):
self.connected_region = None self.connected_region = None
return previously_connected return previously_connected
def get_new_target(self): def get_new_target(self, pool_type):
root = self.multiworld.get_region('Root Exits', self.player) root = self.multiworld.get_region('Root Exits', self.player)
target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root)
target_entrance.connect(self.connected_region) target_entrance.connect(self.connected_region)
target_entrance.replaces = self target_entrance.replaces = self
root.exits.append(target_entrance) root.exits.append(target_entrance)
return target_entrance return target_entrance
def assume_reachable(self): def assume_reachable(self, pool_type):
if self.assumed == None: if self.assumed == None:
self.assumed = self.get_new_target() self.assumed = self.get_new_target(pool_type)
self.disconnect() self.disconnect()
return self.assumed return self.assumed

View File

@ -2,6 +2,7 @@ from itertools import chain
import logging import logging
from worlds.generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
from BaseClasses import CollectionState
from .Hints import get_hint_area, HintAreaNotFound from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay from .Regions import TimeOfDay
@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
return_entrance.data['index'] = 0x7FFF return_entrance.data['index'] = 0x7FFF
def assume_entrance_pool(entrance_pool, ootworld): def assume_entrance_pool(entrance_pool, ootworld, pool_type):
assumed_pool = [] assumed_pool = []
for entrance in entrance_pool: for entrance in entrance_pool:
assumed_forward = entrance.assume_reachable() assumed_forward = entrance.assume_reachable(pool_type)
if entrance.reverse != None and not ootworld.decouple_entrances: if entrance.reverse != None and not ootworld.decouple_entrances:
assumed_return = entrance.reverse.assume_reachable() assumed_return = entrance.reverse.assume_reachable(pool_type)
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)): if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \ if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances): (entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
return assumed_pool return assumed_pool
def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()): def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()):
one_way_entrances = [] one_way_entrances = []
for pool_type in types_to_include: for pool_type in types_to_include:
one_way_entrances += world.get_shufflable_entrances(type=pool_type) one_way_entrances += world.get_shufflable_entrances(type=pool_type)
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances)) valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
if target_region_names: if target_region_names:
return [entrance.get_new_target() for entrance in valid_one_way_entrances return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances
if entrance.connected_region.name in target_region_names] if entrance.connected_region.name in target_region_names]
return [entrance.get_new_target() for entrance in valid_one_way_entrances] return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances]
# Abbreviations # Abbreviations
@ -423,14 +424,14 @@ multi_interior_regions = {
} }
interior_entrance_bias = { interior_entrance_bias = {
'Kakariko Village -> Kak Potion Shop Front': 4, 'ToT Entrance -> Temple of Time': 4,
'Kak Backyard -> Kak Potion Shop Back': 4, 'Kakariko Village -> Kak Potion Shop Front': 3,
'Kakariko Village -> Kak Impas House': 3, 'Kak Backyard -> Kak Potion Shop Back': 3,
'Kak Impas Ledge -> Kak Impas House Back': 3, 'Kakariko Village -> Kak Impas House': 2,
'Goron City -> GC Shop': 2, 'Kak Impas Ledge -> Kak Impas House Back': 2,
'Zoras Domain -> ZD Shop': 2,
'Market Entrance -> Market Guard House': 2, 'Market Entrance -> Market Guard House': 2,
'ToT Entrance -> Temple of Time': 1, 'Goron City -> GC Shop': 1,
'Zoras Domain -> ZD Shop': 1,
} }
@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld):
player = ootworld.player player = ootworld.player
# Gather locations to keep reachable for validation # Gather locations to keep reachable for validation
all_state = world.get_all_state(use_cache=True) all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_events(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances # Set entrance data for all entrances
@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld):
for pool_type, entrance_pool in one_way_entrance_pools.items(): for pool_type, entrance_pool in one_way_entrance_pools.items():
if pool_type == 'OwlDrop': if pool_type == 'OwlDrop':
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra') valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]: for target in one_way_target_entrance_pools[pool_type]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player)) set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}: elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra') valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types) one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable # Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]: for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))()) add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):
target_entrance_pools = {} target_entrance_pools = {}
for pool_type, entrance_pool in entrance_pools.items(): for pool_type, entrance_pool in entrance_pools.items():
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type)
# Build all_state and none_state # Build all_state and none_state
all_state = ootworld.get_state_with_complete_itempool() all_state = ootworld.get_state_with_complete_itempool()
none_state = all_state.copy() none_state = CollectionState(ootworld.multiworld)
for item_tuple in none_state.prog_items:
if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0
# Plando entrances # Plando entrances
if world.plando_connections[player]: if world.plando_connections[player]:
@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}') logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable # Game is beatable
new_all_state = world.get_all_state(use_cache=False) new_all_state = ootworld.get_state_with_complete_itempool()
if not world.has_beaten_game(new_all_state, player): if not world.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game') raise EntranceShuffleError('Cannot beat game')
# Validate world # Validate world
@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10):
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances): def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
world = ootworld.multiworld
player = ootworld.player player = ootworld.player
# Disconnect all root assumed entrances and save original connections # Disconnect all root assumed entrances and save original connections
@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
if entrance.connected_region: if entrance.connected_region:
original_connected_regions[entrance] = entrance.disconnect() original_connected_regions[entrance] = entrance.disconnect()
all_state = world.get_all_state(use_cache=False) all_state = ootworld.get_state_with_complete_itempool()
restrictive_entrances = [] restrictive_entrances = []
soft_entrances = [] soft_entrances = []
@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy() all_state = all_state_orig.copy()
none_state = none_state_orig.copy() none_state = none_state_orig.copy()
all_state.sweep_for_events() all_state.sweep_for_events(locations=ootworld.get_locations())
none_state.sweep_for_events() none_state.sweep_for_events(locations=ootworld.get_locations())
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy() time_travel_state = none_state.copy()

View File

@ -1,8 +1,12 @@
from collections import deque from collections import deque
import logging import logging
import typing
from .Regions import TimeOfDay from .Regions import TimeOfDay
from .DungeonList import dungeon_table
from .Hints import HintArea
from .Items import oot_is_item_of_type from .Items import oot_is_item_of_type
from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
@ -150,11 +154,16 @@ def set_rules(ootworld):
location = world.get_location('Forest Temple MQ First Room Chest', player) location = world.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items: if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle. # This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player) location = world.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
if ootworld.shuffle_child_trade == 'skip_child_zelda':
# Song from Impa must be local
location = world.get_location('Song from Impa', player)
add_item_rule(location, lambda item: item.player == player)
for name in ootworld.always_hints: for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint) add_rule(world.get_location(name, player), guarantee_hint)
@ -176,11 +185,6 @@ def create_shop_rule(location, parser):
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price)) return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
def limit_to_itemset(location, itemset):
old_rule = location.item_rule
location.item_rule = lambda item: item.name in itemset and old_rule(item)
# This function should be run once after the shop items are placed in the world. # This function should be run once after the shop items are placed in the world.
# It should be run before other items are placed in the world so that logic has # It should be run before other items are placed in the world so that logic has
# the correct checks for them. This is safe to do since every shop is still # the correct checks for them. This is safe to do since every shop is still

View File

@ -170,15 +170,19 @@ class OOTWorld(World):
location_name_groups = build_location_name_groups() location_name_groups = build_location_name_groups()
def __init__(self, world, player): def __init__(self, world, player):
self.hint_data_available = threading.Event() self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event() self.collectible_flags_available = threading.Event()
super(OOTWorld, self).__init__(world, player) super(OOTWorld, self).__init__(world, player)
@classmethod @classmethod
def stage_assert_generate(cls, multiworld: MultiWorld): def stage_assert_generate(cls, multiworld: MultiWorld):
rom = Rom(file=get_options()['oot_options']['rom_file']) rom = Rom(file=get_options()['oot_options']['rom_file'])
# Option parsing, handling incompatible options, building useful-item table
def generate_early(self): def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player) self.parser = Rule_AST_Transformer(self, self.player)
@ -194,9 +198,10 @@ class OOTWorld(World):
option_value = result.current_key option_value = result.current_key
setattr(self, option_name, option_value) setattr(self, option_name, option_value)
self.shop_prices = {}
self.regions = [] # internal caches of regions for this world, used later self.regions = [] # internal caches of regions for this world, used later
self._regions_cache = {} self._regions_cache = {}
self.shop_prices = {}
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter() self.starting_items = Counter()
self.songs_as_items = False self.songs_as_items = False
@ -490,6 +495,8 @@ class OOTWorld(World):
# Farore's Wind skippable if not used for this logic trick in Water Temple # Farore's Wind skippable if not used for this logic trick in Water Temple
self.nonadvancement_items.add('Farores Wind') self.nonadvancement_items.add('Farores Wind')
# Reads a group of regions from the given JSON file.
def load_regions_from_json(self, file_path): def load_regions_from_json(self, file_path):
region_json = read_json(file_path) region_json = read_json(file_path)
@ -561,8 +568,9 @@ class OOTWorld(World):
self.multiworld.regions.append(new_region) self.multiworld.regions.append(new_region)
self.regions.append(new_region) self.regions.append(new_region)
self._regions_cache[new_region.name] = new_region self._regions_cache[new_region.name] = new_region
# self.multiworld._recache()
# Sets deku scrub prices
def set_scrub_prices(self): def set_scrub_prices(self):
# Get Deku Scrub Locations # Get Deku Scrub Locations
scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}] scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
@ -591,6 +599,8 @@ class OOTWorld(World):
if location.item is not None: if location.item is not None:
location.item.price = price location.item.price = price
# Sets prices for shuffled shop locations
def random_shop_prices(self): def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6'] shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {} self.shop_prices = {}
@ -616,6 +626,8 @@ class OOTWorld(World):
elif self.shopsanity_prices == 'tycoons_wallet': elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
# Fill boss prizes
def fill_bosses(self, bossCount=9): def fill_bosses(self, bossCount=9):
boss_location_names = ( boss_location_names = (
'Queen Gohma', 'Queen Gohma',
@ -644,6 +656,44 @@ class OOTWorld(World):
loc.place_locked_item(item) loc.place_locked_item(item)
self.hinted_dungeon_reward_locations[item.name] = loc self.hinted_dungeon_reward_locations[item.name] = loc
# Separate the result from generate_itempool into main and prefill pools
def divide_itempools(self):
prefill_item_types = set()
if self.shopsanity != 'off':
prefill_item_types.add('Shop')
if self.shuffle_song_items != 'any':
prefill_item_types.add('Song')
if self.shuffle_smallkeys != 'keysanity':
prefill_item_types.add('SmallKey')
if self.shuffle_bosskeys != 'keysanity':
prefill_item_types.add('BossKey')
if self.shuffle_hideoutkeys != 'keysanity':
prefill_item_types.add('HideoutSmallKey')
if self.shuffle_ganon_bosskey != 'keysanity':
prefill_item_types.add('GanonBossKey')
if self.shuffle_mapcompass != 'keysanity':
prefill_item_types.update({'Map', 'Compass'})
main_items = []
prefill_items = []
for item in self.itempool:
if item.type in prefill_item_types:
prefill_items.append(item)
else:
main_items.append(item)
return main_items, prefill_items
# only returns proper result after create_items and divide_itempools are run
def get_pre_fill_items(self):
return self.pre_fill_items
# Note on allow_arbitrary_name:
# OoT defines many helper items and event names that are treated indistinguishably from regular items,
# but are only defined in the logic files. This means we need to create items for any name.
# Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
def create_item(self, name: str, allow_arbitrary_name: bool = False): def create_item(self, name: str, allow_arbitrary_name: bool = False):
if name in item_table: if name in item_table:
return OOTItem(name, self.player, item_table[name], False, return OOTItem(name, self.player, item_table[name], False,
@ -663,7 +713,9 @@ class OOTWorld(World):
location.internal = True location.internal = True
return item return item
def create_regions(self): # create and link regions
# Create regions, locations, and entrances
def create_regions(self):
if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL
world_type = 'World' world_type = 'World'
else: else:
@ -689,6 +741,8 @@ class OOTWorld(World):
for exit in region.exits: for exit in region.exits:
exit.connect(self.get_region(exit.vanilla_connected_region)) exit.connect(self.get_region(exit.vanilla_connected_region))
# Create items, starting item handling, boss prize fill (before entrance randomizer)
def create_items(self): def create_items(self):
# Generate itempool # Generate itempool
generate_itempool(self) generate_itempool(self)
@ -714,12 +768,16 @@ class OOTWorld(World):
if self.start_with_rupees: if self.start_with_rupees:
self.starting_items['Rupees'] = 999 self.starting_items['Rupees'] = 999
# Divide itempool into prefill and main pools
self.itempool, self.pre_fill_items = self.divide_itempools()
self.multiworld.itempool += self.itempool self.multiworld.itempool += self.itempool
self.remove_from_start_inventory.extend(removed_items) self.remove_from_start_inventory.extend(removed_items)
# Fill boss prizes. needs to happen before entrance shuffle # Fill boss prizes. needs to happen before entrance shuffle
self.fill_bosses() self.fill_bosses()
def set_rules(self): def set_rules(self):
# This has to run AFTER creating items but BEFORE set_entrances_based_rules # This has to run AFTER creating items but BEFORE set_entrances_based_rules
if self.entrance_shuffle: if self.entrance_shuffle:
@ -757,6 +815,7 @@ class OOTWorld(World):
set_rules(self) set_rules(self)
set_entrances_based_rules(self) set_entrances_based_rules(self)
def generate_basic(self): # mostly killing locations that shouldn't exist by settings def generate_basic(self): # mostly killing locations that shouldn't exist by settings
# Gather items for ice trap appearances # Gather items for ice trap appearances
@ -769,7 +828,8 @@ class OOTWorld(World):
# Kill unreachable events that can't be gotten even with all items # Kill unreachable events that can't be gotten even with all items
# Make sure to only kill actual internal events, not in-game "events" # Make sure to only kill actual internal events, not in-game "events"
all_state = self.multiworld.get_all_state(use_cache=True) all_state = self.get_state_with_complete_itempool()
all_state.sweep_for_events()
all_locations = self.get_locations() all_locations = self.get_locations()
reachable = self.multiworld.get_reachable_locations(all_state, self.player) reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if unreachable = [loc for loc in all_locations if
@ -791,35 +851,63 @@ class OOTWorld(World):
loc = self.multiworld.get_location("Deliver Rutos Letter", self.player) loc = self.multiworld.get_location("Deliver Rutos Letter", self.player)
loc.parent_region.locations.remove(loc) loc.parent_region.locations.remove(loc)
def pre_fill(self): def pre_fill(self):
def prefill_state(base_state):
state = base_state.copy()
for item in self.get_pre_fill_items():
self.collect(state, item)
state.sweep_for_events(self.get_locations())
return state
# Prefill shops, songs, and dungeon items
items = self.get_pre_fill_items()
locations = list(self.multiworld.get_unfilled_locations(self.player))
self.multiworld.random.shuffle(locations)
# Set up initial state
state = CollectionState(self.multiworld)
for item in self.itempool:
self.collect(state, item)
state.sweep_for_events(self.get_locations())
# Place dungeon items # Place dungeon items
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
world_items = [item for item in self.multiworld.itempool if item.player == self.player] type_to_setting = {
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
'BossKey': 'shuffle_bosskeys',
'HideoutSmallKey': 'shuffle_hideoutkeys',
'GanonBossKey': 'shuffle_ganon_bosskey',
}
special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1)
for fill_stage in special_fill_types: for fill_stage in special_fill_types:
stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items)) stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items))
if not stage_items: if not stage_items:
continue continue
if fill_stage in ['GanonBossKey', 'HideoutSmallKey']: if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
locations = gather_locations(self.multiworld, fill_stage, self.player) locations = gather_locations(self.multiworld, fill_stage, self.player)
if isinstance(locations, list): if isinstance(locations, list):
for item in stage_items: for item in stage_items:
self.multiworld.itempool.remove(item) self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations) self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
single_player_placement=True, lock=True, allow_excluded=True) single_player_placement=True, lock=True, allow_excluded=True)
else: else:
for dungeon_info in dungeon_table: for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name'] dungeon_name = dungeon_info['name']
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
if not dungeon_items:
continue
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
if isinstance(locations, list): if isinstance(locations, list):
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
if not dungeon_items:
continue
for item in dungeon_items: for item in dungeon_items:
self.multiworld.itempool.remove(item) self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations) self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
single_player_placement=True, lock=True, allow_excluded=True) single_player_placement=True, lock=True, allow_excluded=True)
# Place songs # Place songs
@ -835,9 +923,9 @@ class OOTWorld(World):
else: else:
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items))
for song in songs: for song in songs:
self.multiworld.itempool.remove(song) self.pre_fill_items.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions) self.warp_songs or self.spawn_positions)
@ -860,7 +948,7 @@ class OOTWorld(World):
while tries: while tries:
try: try:
self.multiworld.random.shuffle(song_locations) self.multiworld.random.shuffle(song_locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
single_player_placement=True, lock=True, allow_excluded=True) single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
except FillError as e: except FillError as e:
@ -882,10 +970,8 @@ class OOTWorld(World):
# Place shop items # 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 # 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': if self.shopsanity != 'off':
shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop' shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
and item.advancement, self.multiworld.itempool)) shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
and not item.advancement, self.multiworld.itempool))
shop_locations = list( shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.multiworld.get_unfilled_locations(player=self.player))) self.multiworld.get_unfilled_locations(player=self.player)))
@ -895,30 +981,14 @@ class OOTWorld(World):
'Buy Zora Tunic': 1, 'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
self.multiworld.random.shuffle(shop_locations) self.multiworld.random.shuffle(shop_locations)
for item in shop_prog + shop_junk: self.pre_fill_items = [] # all prefill should be done
self.multiworld.itempool.remove(item) fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog,
single_player_placement=True, lock=True, allow_excluded=True) single_player_placement=True, lock=True, allow_excluded=True)
fast_fill(self.multiworld, shop_junk, shop_locations) fast_fill(self.multiworld, shop_junk, shop_locations)
for loc in shop_locations: for loc in shop_locations:
loc.locked = True loc.locked = True
set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled
# If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
impa = self.multiworld.get_location("Song from Impa", self.player)
if self.shuffle_child_trade == 'skip_child_zelda':
if impa.item is None:
candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
if candidate_items:
item_to_place = self.multiworld.random.choice(candidate_items)
self.multiworld.itempool.remove(item_to_place)
else:
item_to_place = self.create_item("Recovery Heart")
impa.place_locked_item(item_to_place)
# Give items to startinventory
self.multiworld.push_precollected(impa.item)
self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge # Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
# Check for dungeon ER later # Check for dungeon ER later
if self.logic_rules == 'glitchless': if self.logic_rules == 'glitchless':
@ -953,49 +1023,6 @@ class OOTWorld(World):
or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None loc.address = None
# Handle item-linked dungeon items and songs
@classmethod
def stage_pre_fill(cls, multiworld: MultiWorld):
special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
for group_id, group in multiworld.groups.items():
if group['game'] != cls.game:
continue
group_items = [item for item in multiworld.itempool if item.player == group_id]
for fill_stage in special_fill_types:
group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items))
if not group_stage_items:
continue
if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']:
# No need to subdivide by dungeon name
locations = gather_locations(multiworld, fill_stage, group['players'])
if isinstance(locations, list):
for item in group_stage_items:
multiworld.itempool.remove(item)
multiworld.random.shuffle(locations)
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
single_player_placement=False, lock=True, allow_excluded=True)
if fill_stage == 'Song':
# We don't want song locations to contain progression unless it's a song
# or it was marked as priority.
# We do this manually because we'd otherwise have to either
# iterate twice or do many function calls.
for loc in locations:
if loc.progress_type == LocationProgressType.DEFAULT:
loc.progress_type = LocationProgressType.EXCLUDED
add_item_rule(loc, lambda i: not (i.advancement or i.useful))
else:
# Perform the fill task once per dungeon
for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name']
locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name)
if isinstance(locations, list):
group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items))
for item in group_dungeon_items:
multiworld.itempool.remove(item)
multiworld.random.shuffle(locations)
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
single_player_placement=False, lock=True, allow_excluded=True)
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
if self.hints != 'none': if self.hints != 'none':
@ -1134,6 +1161,15 @@ class OOTWorld(World):
continue continue
multidata["precollected_items"][self.player].remove(item_id) multidata["precollected_items"][self.player].remove(item_id)
# If skip child zelda, push item onto autotracker
if self.shuffle_child_trade == 'skip_child_zelda':
impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None)
zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None)
if impa_item_id:
multidata["precollected_items"][self.player].append(impa_item_id)
if zelda_item_id:
multidata["precollected_items"][self.player].append(zelda_item_id)
def extend_hint_information(self, er_hint_data: dict): def extend_hint_information(self, er_hint_data: dict):
@ -1248,17 +1284,15 @@ class OOTWorld(World):
return False return False
def get_shufflable_entrances(self, type=None, only_primary=False): def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.multiworld.get_entrances(self.player) if ( return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type)
(type == None or entrance.type == type) and (not only_primary or entrance.primary))] and (not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False): 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 return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
entrance.shuffled] entrance.shuffled]
def get_locations(self): def get_locations(self):
for region in self.regions: return self.multiworld.get_locations(self.player)
for loc in region.locations:
yield loc
def get_location(self, location): def get_location(self, location):
return self.multiworld.get_location(location, self.player) return self.multiworld.get_location(location, self.player)
@ -1271,6 +1305,9 @@ class OOTWorld(World):
self._regions_cache[region_name] = ret self._regions_cache[region_name] = ret
return ret return ret
def get_entrances(self):
return self.multiworld.get_entrances(self.player)
def get_entrance(self, entrance): def get_entrance(self, entrance):
return self.multiworld.get_entrance(entrance, self.player) return self.multiworld.get_entrance(entrance, self.player)
@ -1304,9 +1341,8 @@ class OOTWorld(World):
# In particular, ensures that Time Travel needs to be found. # In particular, ensures that Time Travel needs to be found.
def get_state_with_complete_itempool(self): def get_state_with_complete_itempool(self):
all_state = CollectionState(self.multiworld) all_state = CollectionState(self.multiworld)
for item in self.multiworld.itempool: for item in self.itempool + self.pre_fill_items:
if item.player == self.player: self.multiworld.worlds[item.player].collect(all_state, item)
self.multiworld.worlds[item.player].collect(all_state, item)
# If free_scarecrow give Scarecrow Song # If free_scarecrow give Scarecrow Song
if self.free_scarecrow: if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True) all_state.collect(self.create_item("Scarecrow Song"), event=True)
@ -1346,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld,
dungeon: str = '' dungeon: str = ''
) -> Optional[List[OOTLocation]]: ) -> Optional[List[OOTLocation]]:
type_to_setting = { type_to_setting = {
'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass', 'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass', 'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys', 'SmallKey': 'shuffle_smallkeys',
@ -1365,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld,
players = {players} players = {players}
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players} fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
locations = [] locations = []
if item_type == 'Song': if any(map(lambda v: v == 'keysanity', fill_opts.values())):
if any(map(lambda v: v == 'any', fill_opts.values())): return None
return None for player, option in fill_opts.items():
for player, option in fill_opts.items(): condition = functools.partial(valid_dungeon_item_location,
if option == 'song': multiworld.worlds[player], option, dungeon)
condition = lambda location: location.type == 'Song' locations += filter(condition, multiworld.get_unfilled_locations(player=player))
elif option == 'dungeon':
condition = lambda location: location.name in dungeon_song_locations
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
else:
if any(map(lambda v: v == 'keysanity', fill_opts.values())):
return None
for player, option in fill_opts.items():
condition = functools.partial(valid_dungeon_item_location,
multiworld.worlds[player], option, dungeon)
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
return locations return locations