Ocarina of Time: long-awaited bugfixes (#2344)
- Added location name groups, so you can make your entire Water Temple priority to annoy everyone else - Significant improvement to ER generation success rate (~80% to >99%) - Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice) - Major improvements to the option tooltips where needed. (Possibly too much text now) - Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up. - Makes stick and nut capacity upgrades useful items - Added shop prices and required trials to spoiler log - Added Cojiro to adult trade item group, because it had been forgotten previously - Fixed size-modified chests not being moved properly due to trap appearance changing the size - Fixed Thieves Hideout keyring not being allowed in start inventory - Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings - Fixed bug where you could plando arbitrarily-named items into the world, breaking everything
This commit is contained in:
@@ -10,7 +10,7 @@ from string import printable
|
||||
|
||||
logger = logging.getLogger("Ocarina of Time")
|
||||
|
||||
from .Location import OOTLocation, LocationFactory, location_name_to_id
|
||||
from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
|
||||
from .Entrance import OOTEntrance
|
||||
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
|
||||
from .HintList import getRequiredHints
|
||||
@@ -163,11 +163,13 @@ class OOTWorld(World):
|
||||
"Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
|
||||
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
|
||||
"Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
|
||||
"Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom",
|
||||
"Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
|
||||
"Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
|
||||
"Eyeball Frog", "Eyedrops", "Claim Check"}
|
||||
"Eyeball Frog", "Eyedrops", "Claim Check"},
|
||||
}
|
||||
|
||||
location_name_groups = build_location_name_groups()
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.hint_data_available = threading.Event()
|
||||
self.collectible_flags_available = threading.Event()
|
||||
@@ -384,6 +386,7 @@ class OOTWorld(World):
|
||||
self.mq_dungeons_mode = 'count'
|
||||
self.mq_dungeons_count = 0
|
||||
self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
|
||||
self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287
|
||||
|
||||
# Empty dungeon placeholder for the moment
|
||||
self.empty_dungeons = {name: False for name in self.dungeon_mq}
|
||||
@@ -409,6 +412,9 @@ class OOTWorld(World):
|
||||
self.starting_tod = self.starting_tod.replace('_', '-')
|
||||
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
|
||||
|
||||
# Convert adult trade option to expected Set
|
||||
self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
|
||||
|
||||
# Get hint distribution
|
||||
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
|
||||
|
||||
@@ -446,7 +452,7 @@ class OOTWorld(World):
|
||||
self.always_hints = [hint.name for hint in getRequiredHints(self)]
|
||||
|
||||
# Determine items which are not considered advancement based on settings. They will never be excluded.
|
||||
self.nonadvancement_items = {'Double Defense'}
|
||||
self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
|
||||
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
|
||||
@@ -633,16 +639,18 @@ class OOTWorld(World):
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.hinted_dungeon_reward_locations[item.name] = loc
|
||||
|
||||
def create_item(self, name: str):
|
||||
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,
|
||||
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
|
||||
None) else False))
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
if allow_arbitrary_name:
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
raise Exception(f"Invalid item name: {name}")
|
||||
|
||||
def make_event_item(self, name, location, item=None):
|
||||
if item is None:
|
||||
item = self.create_item(name)
|
||||
item = self.create_item(name, allow_arbitrary_name=True)
|
||||
self.multiworld.push_item(location, item, collect=False)
|
||||
location.locked = True
|
||||
location.event = True
|
||||
@@ -800,23 +808,25 @@ class OOTWorld(World):
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
|
||||
single_player_placement=True, lock=True)
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
else:
|
||||
for dungeon_info in dungeon_table:
|
||||
dungeon_name = dungeon_info['name']
|
||||
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.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
|
||||
single_player_placement=True, lock=True)
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
if self.shuffle_song_items != 'any':
|
||||
tries = 5
|
||||
tries = 10
|
||||
if self.shuffle_song_items == 'song':
|
||||
song_locations = list(filter(lambda location: location.type == 'Song',
|
||||
self.multiworld.get_unfilled_locations(player=self.player)))
|
||||
@@ -852,7 +862,7 @@ class OOTWorld(World):
|
||||
try:
|
||||
self.multiworld.random.shuffle(song_locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
|
||||
True, 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)")
|
||||
except FillError as e:
|
||||
tries -= 1
|
||||
@@ -888,7 +898,8 @@ class OOTWorld(World):
|
||||
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, True, True)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), 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
|
||||
@@ -963,7 +974,7 @@ class OOTWorld(World):
|
||||
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)
|
||||
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.
|
||||
@@ -984,7 +995,7 @@ class OOTWorld(World):
|
||||
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)
|
||||
single_player_placement=False, lock=True, allow_excluded=True)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
@@ -1051,7 +1062,10 @@ class OOTWorld(World):
|
||||
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
|
||||
def hint_type_players(hint_type: str) -> set:
|
||||
return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
|
||||
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
|
||||
if autoworld.hints != 'none'
|
||||
and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
|
||||
and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0
|
||||
or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
|
||||
|
||||
try:
|
||||
item_hint_players = hint_type_players('item')
|
||||
@@ -1078,10 +1092,10 @@ class OOTWorld(World):
|
||||
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
||||
(oot_is_item_of_type(loc.item, 'Song') or
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
|
||||
if loc.player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
||||
@@ -1096,7 +1110,12 @@ class OOTWorld(World):
|
||||
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
|
||||
for player in (barren_hint_players | woth_hint_players):
|
||||
for loc in multiworld.worlds[player].get_locations():
|
||||
if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
|
||||
if loc.item.code and (not loc.locked or
|
||||
(oot_is_item_of_type(loc.item, 'Song') or
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
|
||||
if player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[player][hint_area]['weight'] += 1
|
||||
@@ -1183,6 +1202,15 @@ class OOTWorld(World):
|
||||
er_hint_data[self.player][location.address] = main_entrance.name
|
||||
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
|
||||
spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
|
||||
|
||||
if self.shopsanity != 'off':
|
||||
spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for k, v in self.shop_prices.items():
|
||||
spoiler_handle.write(f"{k}: {v} Rupees\n")
|
||||
|
||||
# Key ring handling:
|
||||
# Key rings are multiple items glued together into one, so we need to give
|
||||
# the appropriate number of keys in the collection state when they are
|
||||
@@ -1265,25 +1293,13 @@ class OOTWorld(World):
|
||||
# Specifically ensures that only real items are gotten, not any events.
|
||||
# In particular, ensures that Time Travel needs to be found.
|
||||
def get_state_with_complete_itempool(self):
|
||||
all_state = self.multiworld.get_all_state(use_cache=False)
|
||||
# Remove event progression items
|
||||
for item, player in all_state.prog_items:
|
||||
if player == self.player and (item not in item_table or item_table[item][2] is None):
|
||||
all_state.prog_items[(item, player)] = 0
|
||||
# Remove all events and checked locations
|
||||
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
|
||||
all_state.events = {loc for loc in all_state.events if loc.player != self.player}
|
||||
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)
|
||||
# If free_scarecrow give Scarecrow Song
|
||||
if self.free_scarecrow:
|
||||
all_state.collect(self.create_item("Scarecrow Song"), event=True)
|
||||
|
||||
# Invalidate caches
|
||||
all_state.child_reachable_regions[self.player] = set()
|
||||
all_state.adult_reachable_regions[self.player] = set()
|
||||
all_state.child_blocked_connections[self.player] = set()
|
||||
all_state.adult_blocked_connections[self.player] = set()
|
||||
all_state.day_reachable_regions[self.player] = set()
|
||||
all_state.dampe_reachable_regions[self.player] = set()
|
||||
all_state.stale[self.player] = True
|
||||
|
||||
return all_state
|
||||
@@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld,
|
||||
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 in {'keysanity'}, fill_opts.values())):
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user