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 .Regions import TimeOfDay
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
@ -29,16 +27,16 @@ class OOTEntrance(Entrance):
self.connected_region = None
return previously_connected
def get_new_target(self):
def get_new_target(self, pool_type):
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.replaces = self
root.exits.append(target_entrance)
return target_entrance
def assume_reachable(self):
def assume_reachable(self, pool_type):
if self.assumed == None:
self.assumed = self.get_new_target()
self.assumed = self.get_new_target(pool_type)
self.disconnect()
return self.assumed

View File

@ -2,6 +2,7 @@ from itertools import chain
import logging
from worlds.generic.Rules import set_rule, add_rule
from BaseClasses import CollectionState
from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
return_entrance.data['index'] = 0x7FFF
def assume_entrance_pool(entrance_pool, ootworld):
def assume_entrance_pool(entrance_pool, ootworld, pool_type):
assumed_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:
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 (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):
@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
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 = []
for pool_type in types_to_include:
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))
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]
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
@ -423,14 +424,14 @@ multi_interior_regions = {
}
interior_entrance_bias = {
'Kakariko Village -> Kak Potion Shop Front': 4,
'Kak Backyard -> Kak Potion Shop Back': 4,
'Kakariko Village -> Kak Impas House': 3,
'Kak Impas Ledge -> Kak Impas House Back': 3,
'Goron City -> GC Shop': 2,
'Zoras Domain -> ZD Shop': 2,
'ToT Entrance -> Temple of Time': 4,
'Kakariko Village -> Kak Potion Shop Front': 3,
'Kak Backyard -> Kak Potion Shop Back': 3,
'Kakariko Village -> Kak Impas House': 2,
'Kak Impas Ledge -> Kak Impas House Back': 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
# 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))}
# 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():
if pool_type == 'OwlDrop':
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]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}:
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
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)))())
@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):
target_entrance_pools = {}
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
all_state = ootworld.get_state_with_complete_itempool()
none_state = all_state.copy()
for item_tuple in none_state.prog_items:
if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0
none_state = CollectionState(ootworld.multiworld)
# Plando entrances
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 has too many entrances left after shuffling entrances')
# 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):
raise EntranceShuffleError('Cannot beat game')
# 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}')
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)
@ -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):
world = ootworld.multiworld
player = ootworld.player
# 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:
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 = []
soft_entrances = []
@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()
all_state.sweep_for_events()
none_state.sweep_for_events()
all_state.sweep_for_events(locations=ootworld.get_locations())
none_state.sweep_for_events(locations=ootworld.get_locations())
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()

View File

@ -1,8 +1,12 @@
from collections import deque
import logging
import typing
from .Regions import TimeOfDay
from .DungeonList import dungeon_table
from .Hints import HintArea
from .Items import oot_is_item_of_type
from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState
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)
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.
# This is required if map/compass included, or any_dungeon shuffle.
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:
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))
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.
# 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

View File

@ -170,15 +170,19 @@ class OOTWorld(World):
location_name_groups = build_location_name_groups()
def __init__(self, world, player):
self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event()
super(OOTWorld, self).__init__(world, player)
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom = Rom(file=get_options()['oot_options']['rom_file'])
# Option parsing, handling incompatible options, building useful-item table
def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player)
@ -194,9 +198,10 @@ class OOTWorld(World):
option_value = result.current_key
setattr(self, option_name, option_value)
self.shop_prices = {}
self.regions = [] # internal caches of regions for this world, used later
self._regions_cache = {}
self.shop_prices = {}
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
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
self.nonadvancement_items.add('Farores Wind')
# Reads a group of regions from the given JSON file.
def load_regions_from_json(self, file_path):
region_json = read_json(file_path)
@ -561,8 +568,9 @@ class OOTWorld(World):
self.multiworld.regions.append(new_region)
self.regions.append(new_region)
self._regions_cache[new_region.name] = new_region
# self.multiworld._recache()
# Sets deku scrub prices
def set_scrub_prices(self):
# Get Deku Scrub Locations
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:
location.item.price = price
# Sets prices for shuffled shop locations
def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {}
@ -616,6 +626,8 @@ class OOTWorld(World):
elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
# Fill boss prizes
def fill_bosses(self, bossCount=9):
boss_location_names = (
'Queen Gohma',
@ -644,6 +656,44 @@ class OOTWorld(World):
loc.place_locked_item(item)
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):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False,
@ -663,7 +713,9 @@ class OOTWorld(World):
location.internal = True
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
world_type = 'World'
else:
@ -689,6 +741,8 @@ class OOTWorld(World):
for exit in region.exits:
exit.connect(self.get_region(exit.vanilla_connected_region))
# Create items, starting item handling, boss prize fill (before entrance randomizer)
def create_items(self):
# Generate itempool
generate_itempool(self)
@ -714,12 +768,16 @@ class OOTWorld(World):
if self.start_with_rupees:
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.remove_from_start_inventory.extend(removed_items)
# Fill boss prizes. needs to happen before entrance shuffle
self.fill_bosses()
def set_rules(self):
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
if self.entrance_shuffle:
@ -757,6 +815,7 @@ class OOTWorld(World):
set_rules(self)
set_entrances_based_rules(self)
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
# 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
# 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()
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
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.parent_region.locations.remove(loc)
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
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:
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:
continue
if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
locations = gather_locations(self.multiworld, fill_stage, self.player)
if isinstance(locations, list):
for item in stage_items:
self.multiworld.itempool.remove(item)
self.pre_fill_items.remove(item)
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)
else:
for dungeon_info in dungeon_table:
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)
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:
self.multiworld.itempool.remove(item)
self.pre_fill_items.remove(item)
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)
# Place songs
@ -835,9 +923,9 @@ class OOTWorld(World):
else:
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:
self.multiworld.itempool.remove(song)
self.pre_fill_items.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
@ -860,7 +948,7 @@ class OOTWorld(World):
while tries:
try:
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)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
except FillError as e:
@ -882,10 +970,8 @@ 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_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
and item.advancement, self.multiworld.itempool))
shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
and not item.advancement, self.multiworld.itempool))
shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.multiworld.get_unfilled_locations(player=self.player)))
@ -895,30 +981,14 @@ class OOTWorld(World):
'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
self.multiworld.random.shuffle(shop_locations)
for item in shop_prog + shop_junk:
self.multiworld.itempool.remove(item)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog,
self.pre_fill_items = [] # all prefill should be done
fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
single_player_placement=True, lock=True, allow_excluded=True)
fast_fill(self.multiworld, shop_junk, shop_locations)
for loc in shop_locations:
loc.locked = True
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
# Check for dungeon ER later
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'])):
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):
if self.hints != 'none':
@ -1134,6 +1161,15 @@ class OOTWorld(World):
continue
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):
@ -1248,17 +1284,15 @@ class OOTWorld(World):
return False
def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.multiworld.get_entrances(self.player) if (
(type == None or entrance.type == type) and (not only_primary or entrance.primary))]
return [entrance for entrance in self.get_entrances() if ((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]
def get_locations(self):
for region in self.regions:
for loc in region.locations:
yield loc
return self.multiworld.get_locations(self.player)
def get_location(self, location):
return self.multiworld.get_location(location, self.player)
@ -1271,6 +1305,9 @@ class OOTWorld(World):
self._regions_cache[region_name] = ret
return ret
def get_entrances(self):
return self.multiworld.get_entrances(self.player)
def get_entrance(self, entrance):
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.
def get_state_with_complete_itempool(self):
all_state = CollectionState(self.multiworld)
for item in self.multiworld.itempool:
if item.player == self.player:
self.multiworld.worlds[item.player].collect(all_state, item)
for item in self.itempool + self.pre_fill_items:
self.multiworld.worlds[item.player].collect(all_state, item)
# If free_scarecrow give Scarecrow Song
if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True)
@ -1346,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld,
dungeon: str = ''
) -> Optional[List[OOTLocation]]:
type_to_setting = {
'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
@ -1365,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld,
players = {players}
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
locations = []
if item_type == 'Song':
if any(map(lambda v: v == 'any', fill_opts.values())):
return None
for player, option in fill_opts.items():
if option == 'song':
condition = lambda location: location.type == 'Song'
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))
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