Ocarina of Time 7.0 (#1277)

## What is this fixing or adding?
- Adds the majority of OoTR 7.0 features:
  - Pot shuffle, Freestanding item shuffle, Crate shuffle, Beehive shuffle
  - Key rings mode
  - Dungeon shortcuts to speed up dungeons
  - "Regional" shuffle for dungeon items
  - New options for shop pricing in shopsanity
  - Expanded Ganon's Boss Key shuffle options
  - Pre-planted beans
  - Improved Chest Appearance Matches Contents mode
  - Blue Fire Arrows
  - Bonk self-damage
  - Finer control over MQ dungeons and spawn position randomization
- Several bugfixes as a result of the update:
  - Items recognized by the server and valid starting items are now in a 1-to-1 correspondence. In particular, starting with keys is now supported.
  - Entrance randomization success rate improved. Hopefully it is now at 100%. 

Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
espeon65536
2022-12-10 21:11:40 -06:00
committed by GitHub
parent 2cdd03f786
commit aee0df5359
110 changed files with 37691 additions and 18648 deletions

View File

@@ -1,7 +1,8 @@
import logging
import threading
import copy
from typing import Optional, List, AbstractSet # remove when 3.8 support is dropped
import functools
from typing import Optional, List, AbstractSet, Union # remove when 3.8 support is dropped
from collections import Counter, deque
from string import printable
@@ -10,8 +11,9 @@ logger = logging.getLogger("Ocarina of Time")
from .Location import OOTLocation, LocationFactory, location_name_to_id
from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .Hints import HintArea
from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
from .Regions import OOTRegion, TimeOfDay
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
from .RuleParser import Rule_AST_Transformer
@@ -21,22 +23,19 @@ from .LocationList import business_scrubs, set_drop_location_names, dungeon_song
from .DungeonList import dungeon_table, create_dungeons
from .LogicTricks import normalized_name_tricks
from .Rom import Rom
from .Patches import patch_rom
from .Patches import OoTContainer, patch_rom
from .N64Patch import create_patch_file
from .Cosmetics import patch_cosmetics
from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
from .HintList import getRequiredHints
from .SaveContext import SaveContext
from Utils import get_options, output_path
from Utils import get_options
from BaseClasses import MultiWorld, CollectionState, RegionType, Tutorial, LocationProgressType
from Options import Range, Toggle, OptionList
from Options import Range, Toggle, VerifyKeys
from Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule
from ..AutoWorld import World, AutoLogicRegister, WebWorld
location_id_offset = 67000
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
i_o_limiter = threading.Semaphore(2)
@@ -100,13 +99,18 @@ class OOTWorld(World):
option_definitions: dict = oot_options
topology_present: bool = True
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
data[2] is not None}
data[2] is not None and item_name not in {
'Keaton Mask', 'Skull Mask', 'Spooky Mask', 'Bunny Hood',
'Mask of Truth', 'Goron Mask', 'Zora Mask', 'Gerudo Mask',
'Buy Magic Bean', 'Milk',
'Small Key', 'Map', 'Compass', 'Boss Key',
}} # These are items which aren't used, but have get-item values
location_name_to_id = location_name_to_id
web = OOTWeb()
data_version = 2
data_version = 3
required_client_version = (0, 3, 2)
required_client_version = (0, 3, 6)
item_name_groups = {
# internal groups
@@ -133,6 +137,7 @@ class OOTWorld(World):
def __init__(self, world, player):
self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event()
super(OOTWorld, self).__init__(world, player)
@classmethod
@@ -148,7 +153,7 @@ class OOTWorld(World):
option_value = int(result)
elif isinstance(result, Toggle):
option_value = bool(result)
elif isinstance(result, OptionList):
elif isinstance(result, VerifyKeys):
option_value = result.value
else:
option_value = result.current_key
@@ -161,6 +166,7 @@ class OOTWorld(World):
self.songs_as_items = False
self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)]
self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16))
self.collectible_flag_addresses = {}
# Incompatible option handling
# ER and glitched logic are not compatible; glitched takes priority
@@ -171,7 +177,15 @@ class OOTWorld(World):
self.shuffle_overworld_entrances = False
self.owl_drops = False
self.warp_songs = False
self.spawn_positions = False
self.spawn_positions = 'off'
# Fix spawn positions option
new_sp = []
if self.spawn_positions in {'child', 'both'}:
new_sp.append('child')
if self.spawn_positions in {'adult', 'both'}:
new_sp.append('adult')
self.spawn_positions = new_sp
# Closed forest and adult start are not compatible; closed forest takes priority
if self.open_forest == 'closed':
@@ -180,13 +194,9 @@ class OOTWorld(World):
if (self.shuffle_interior_entrances == 'all' or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions):
self.open_forest = 'closed_deku'
# Skip child zelda and shuffle egg are not compatible; skip-zelda takes priority
if self.skip_child_zelda:
self.shuffle_weird_egg = False
# Ganon boss key should not be in itempool in triforce hunt
if self.triforce_hunt:
self.shuffle_ganon_bosskey = 'remove'
self.shuffle_ganon_bosskey = 'triforce'
# If songs/keys locked to own world by settings, add them to local_items
local_types = []
@@ -196,7 +206,7 @@ class OOTWorld(World):
local_types += ['Map', 'Compass']
if self.shuffle_smallkeys != 'keysanity':
local_types.append('SmallKey')
if self.shuffle_fortresskeys != 'keysanity':
if self.shuffle_hideoutkeys != 'keysanity':
local_types.append('HideoutSmallKey')
if self.shuffle_bosskeys != 'keysanity':
local_types.append('BossKey')
@@ -219,15 +229,6 @@ class OOTWorld(World):
chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list}
# Determine which dungeons are MQ
# Possible future plan: allow user to pick which dungeons are MQ
if self.logic_rules == 'glitchless':
mq_dungeons = self.multiworld.random.sample(dungeon_table, self.mq_dungeons)
else:
self.mq_dungeons = 0
mq_dungeons = []
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
# Determine tricks in logic
if self.logic_rules == 'glitchless':
for trick in self.logic_tricks:
@@ -245,15 +246,31 @@ class OOTWorld(World):
setattr(self, trick['name'], True)
# Not implemented for now, but needed to placate the generator. Remove as they are implemented
self.mq_dungeons_random = False # this will be a deprecated option later
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
self.mix_entrance_pools = False
self.decouple_entrances = False
self.available_tokens = 100
# Deprecated LACS options
self.lacs_condition = 'vanilla'
self.lacs_stones = 3
self.lacs_medallions = 6
self.lacs_rewards = 9
self.lacs_tokens = 100
self.lacs_hearts = 20
# RuleParser hack
self.triforce_goal_per_world = self.triforce_goal
# Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
self.trials_random = self.multiworld.trials[self.player].randomized
self.mq_dungeons_random = self.multiworld.mq_dungeons[self.player].randomized
self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized
self.easier_fire_arrow_entry = self.fae_torch_count < 24
if self.misc_hints:
self.misc_hints = ['ganondorf', 'altar', 'warp_songs', 'dampe_diary',
'10_skulltulas', '20_skulltulas', '30_skulltulas', '40_skulltulas', '50_skulltulas']
else:
self.misc_hints = []
# Hint stuff
self.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text
@@ -261,8 +278,11 @@ class OOTWorld(World):
self.required_locations = []
self.empty_areas = {}
self.major_item_locations = []
self.hinted_dungeon_reward_locations = {}
# ER names
self.shuffle_special_dungeon_entrances = self.shuffle_dungeon_entrances == 'all'
self.shuffle_dungeon_entrances = self.shuffle_dungeon_entrances != 'off'
self.ensure_tod_access = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances or self.spawn_positions
self.entrance_shuffle = (self.shuffle_interior_entrances != 'off') or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \
self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions
@@ -275,6 +295,60 @@ class OOTWorld(World):
elif self.shopsanity == 'fixed_number':
self.shopsanity = str(self.shop_slots)
# Rename options
self.dungeon_shortcuts_choice = self.dungeon_shortcuts
if self.dungeon_shortcuts_choice == 'random_dungeons':
self.dungeon_shortcuts_choice = 'random'
self.key_rings_list = {s.replace("'", "") for s in self.key_rings_list}
self.dungeon_shortcuts = {s.replace("'", "") for s in self.dungeon_shortcuts_list}
self.mq_dungeons_specific = {s.replace("'", "") for s in self.mq_dungeons_list}
# self.empty_dungeons_specific = {s.replace("'", "") for s in self.empty_dungeons_list}
# Determine which dungeons have key rings.
keyring_dungeons = [d['name'] for d in dungeon_table if d['small_key']] + ['Thieves Hideout']
if self.key_rings == 'off':
self.key_rings = []
elif self.key_rings == 'all':
self.key_rings = keyring_dungeons
elif self.key_rings == 'choose':
self.key_rings = self.key_rings_list
elif self.key_rings == 'random_dungeons':
self.key_rings = self.multiworld.random.sample(keyring_dungeons,
self.multiworld.random.randint(0, len(keyring_dungeons)))
# Determine which dungeons are MQ. Not compatible with glitched logic.
mq_dungeons = set()
if self.logic_rules != 'glitched':
if self.mq_dungeons_mode == 'mq':
mq_dungeons = dungeon_table.keys()
elif self.mq_dungeons_mode == 'specific':
mq_dungeons = self.mq_dungeons_specific
elif self.mq_dungeons_mode == 'count':
mq_dungeons = self.multiworld.random.sample(dungeon_table, self.mq_dungeons_count)
else:
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}
# Empty dungeon placeholder for the moment
self.empty_dungeons = {name: False for name in self.dungeon_mq}
# Determine which dungeons have shortcuts. Not compatible with glitched logic.
shortcut_dungeons = ['Deku Tree', 'Dodongos Cavern', \
'Jabu Jabus Belly', 'Forest Temple', 'Fire Temple', \
'Water Temple', 'Shadow Temple', 'Spirit Temple']
if self.logic_rules != 'glitched':
if self.dungeon_shortcuts_choice == 'off':
self.dungeon_shortcuts = set()
elif self.dungeon_shortcuts_choice == 'all':
self.dungeon_shortcuts = set(shortcut_dungeons)
elif self.dungeon_shortcuts_choice == 'random':
self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons,
self.multiworld.random.randint(0, len(shortcut_dungeons)))
# == 'choice', leave as previous
else:
self.dungeon_shortcuts = set()
# fixing some options
# Fixes starting time spelling: "witching_hour" -> "witching-hour"
self.starting_tod = self.starting_tod.replace('_', '-')
@@ -286,7 +360,7 @@ class OOTWorld(World):
self.added_hint_types = {}
self.item_added_hint_types = {}
self.hint_exclusions = set()
if self.skip_child_zelda:
if self.shuffle_child_trade == 'skip_child_zelda':
self.hint_exclusions.add('Song from Impa')
self.hint_type_overrides = {}
self.item_hint_type_overrides = {}
@@ -317,7 +391,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', 'Ice Arrows'}
self.nonadvancement_items = {'Double Defense'}
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
@@ -330,6 +404,22 @@ class OOTWorld(World):
# Serenade and Prelude are never required unless one of those settings is enabled
self.nonadvancement_items.add('Serenade of Water')
self.nonadvancement_items.add('Prelude of Light')
if not self.blue_fire_arrows:
# Ice Arrows serve no purpose if they're not hacked to have one
self.nonadvancement_items.add('Ice Arrows')
if not self.bombchus_in_logic:
# Nonrenewable bombchus are not a default logical explosive
self.nonadvancement_items.update({
'Bombchus (5)',
'Bombchus (10)',
'Bombchus (20)',
})
if not (self.bridge == 'hearts' or self.shuffle_ganon_bosskey == 'hearts'):
self.nonadvancement_items.update({
'Heart Container',
'Piece of Heart',
'Piece of Heart (Treasure Chest Game)'
})
if self.logic_rules == 'glitchless':
# Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless
self.nonadvancement_items.add('Biggoron Sword')
@@ -350,10 +440,14 @@ class OOTWorld(World):
new_region.font_color = region['font_color']
if 'scene' in region:
new_region.scene = region['scene']
if 'hint' in region:
new_region.hint_text = region['hint']
if 'dungeon' in region:
new_region.dungeon = region['dungeon']
if 'is_boss_room' in region:
new_region.is_boss_room = region['is_boss_room']
if 'hint' in region:
new_region.set_hint_data(region['hint'])
if 'alt_hint' in region:
new_region.alt_hint = HintArea[region['alt_hint']]
if 'time_passes' in region:
new_region.time_passes = region['time_passes']
new_region.provides_time = TimeOfDay.ALL
@@ -404,7 +498,7 @@ class OOTWorld(World):
def set_scrub_prices(self):
# Get Deku Scrub Locations
scrub_locations = [location for location in self.get_locations() if 'Deku Scrub' in location.name]
scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
scrub_dictionary = {}
self.scrub_prices = {}
for location in scrub_locations:
@@ -442,7 +536,18 @@ class OOTWorld(World):
for location in region.locations:
if location.type == 'Shop':
if location.name[-1:] in shop_item_indexes[:shop_item_count]:
self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5
if self.shopsanity_prices == 'normal':
self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5
elif self.shopsanity_prices == 'affordable':
self.shop_prices[location.name] = 10
elif self.shopsanity_prices == 'starting_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5)
elif self.shopsanity_prices == 'adults_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5)
elif self.shopsanity_prices == 'giants_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5)
elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
def fill_bosses(self, bossCount=9):
boss_location_names = (
@@ -471,6 +576,7 @@ class OOTWorld(World):
loc = prize_locs.pop()
loc.place_locked_item(item)
self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc
def create_item(self, name: str):
if name in item_table:
@@ -495,11 +601,13 @@ class OOTWorld(World):
else:
world_type = 'Glitched World'
overworld_data_path = data_path(world_type, 'Overworld.json')
bosses_data_path = data_path(world_type, 'Bosses.json')
menu = OOTRegion('Menu', None, None, self.player)
start = OOTEntrance(self.player, self.multiworld, 'New Game', menu)
menu.exits.append(start)
self.multiworld.regions.append(menu)
self.load_regions_from_json(overworld_data_path)
self.load_regions_from_json(bosses_data_path)
start.connect(self.multiworld.get_region('Root', self.player))
create_dungeons(self)
self.parser.create_delayed_rules()
@@ -514,9 +622,10 @@ class OOTWorld(World):
exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player))
def create_items(self):
# Uniquely rename drop locations for each region and erase them from the spoiler
set_drop_location_names(self)
# Generate itempool
generate_itempool(self)
add_dungeon_items(self)
# Add dungeon rewards
rewardlist = sorted(list(self.item_name_groups['rewards']))
self.itempool += map(self.create_item, rewardlist)
@@ -529,16 +638,13 @@ class OOTWorld(World):
self.remove_from_start_inventory.remove(item.name)
removed_items.append(item.name)
else:
if item.name not in SaveContext.giveable_items:
raise Exception(f"Invalid OoT starting item: {item.name}")
else:
self.starting_items[item.name] += 1
if item.type == 'Song':
self.songs_as_items = True
# Call the junk fill and get a replacement
if item in self.itempool:
self.itempool.remove(item)
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
self.starting_items[item.name] += 1
if item.type == 'Song':
self.songs_as_items = True
# Call the junk fill and get a replacement
if item in self.itempool:
self.itempool.remove(item)
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
if self.start_with_consumables:
self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40
@@ -548,6 +654,9 @@ class OOTWorld(World):
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:
@@ -587,12 +696,6 @@ class OOTWorld(World):
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
# Fill boss prizes. needs to happen before killing unreachable locations
self.fill_bosses()
# Uniquely rename drop locations for each region and erase them from the spoiler
set_drop_location_names(self)
# Gather items for ice trap appearances
self.fake_items = []
if self.ice_trap_appearance in ['major_only', 'anything']:
@@ -628,72 +731,32 @@ class OOTWorld(World):
def pre_fill(self):
def get_names(items):
for item in items:
yield item.name
# Place/set rules for dungeon items
itempools = {
'dungeon': set(),
'overworld': set(),
'any_dungeon': set(),
}
any_dungeon_locations = []
for dungeon in self.dungeons:
itempools['dungeon'] = set()
# Put the dungeon items into their appropriate pools.
# Build in reverse order since we need to fill boss key first and pop() returns the last element
if self.shuffle_mapcompass in itempools:
itempools[self.shuffle_mapcompass].update(get_names(dungeon.dungeon_items))
if self.shuffle_smallkeys in itempools:
itempools[self.shuffle_smallkeys].update(get_names(dungeon.small_keys))
shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
if shufflebk in itempools:
itempools[shufflebk].update(get_names(dungeon.boss_key))
# 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)]
if itempools['dungeon']: # only do this if there's anything to shuffle
dungeon_itempool = [item for item in self.multiworld.itempool if item.player == self.player and item.name in itempools['dungeon']]
for item in dungeon_itempool:
self.multiworld.itempool.remove(item)
self.multiworld.random.shuffle(dungeon_locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), dungeon_locations,
dungeon_itempool, True, True)
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
# 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':
itempools['any_dungeon'].add('Small Key (Thieves Hideout)')
if itempools['any_dungeon']:
any_dungeon_itempool = [item for item in self.multiworld.itempool if item.player == self.player and item.name in itempools['any_dungeon']]
for item in any_dungeon_itempool:
self.multiworld.itempool.remove(item)
any_dungeon_itempool.sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
self.multiworld.random.shuffle(any_dungeon_locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), any_dungeon_locations,
any_dungeon_itempool, True, True)
# If anything is overworld-only, fill into local non-dungeon locations
if self.shuffle_fortresskeys == 'overworld':
itempools['overworld'].add('Small Key (Thieves Hideout)')
if itempools['overworld']:
overworld_itempool = [item for item in self.multiworld.itempool if item.player == self.player and item.name in itempools['overworld']]
for item in overworld_itempool:
self.multiworld.itempool.remove(item)
overworld_itempool.sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 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' or loc.name in self.shop_prices) and
(loc.type != 'Song' or self.shuffle_song_items != 'song') and
(loc.name not in dungeon_song_locations or self.shuffle_song_items != 'dungeon')]
self.multiworld.random.shuffle(non_dungeon_locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), non_dungeon_locations,
overworld_itempool, True, True)
# 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]
for fill_stage in special_fill_types:
stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_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.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
single_player_placement=True, lock=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))
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)
# Place songs
# 5 built-in retries because this section can fail sometimes
@@ -778,10 +841,10 @@ class OOTWorld(World):
# 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.skip_child_zelda:
if self.shuffle_child_trade == 'skip_child_zelda':
if impa.item is None:
item_to_place = self.multiworld.random.choice(list(item for item in self.multiworld.itempool if
item.player == self.player and item.name in SaveContext.giveable_items))
item_to_place = self.multiworld.random.choice(
list(item for item in self.multiworld.itempool if item.player == self.player))
impa.place_locked_item(item_to_place)
self.multiworld.itempool.remove(item_to_place)
# Give items to startinventory
@@ -801,11 +864,14 @@ class OOTWorld(World):
ganon_junk_fill = 2 / 9
elif self.bridge == 'tokens':
ganon_junk_fill = self.bridge_tokens / 100
elif self.bridge == 'hearts':
ganon_junk_fill = self.bridge_hearts / 20
elif self.bridge == 'open':
ganon_junk_fill = 0
else:
raise Exception("Unexpected bridge setting")
ganon_junk_fill = min(1, ganon_junk_fill)
gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons))
locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None]
junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill))
@@ -816,48 +882,12 @@ class OOTWorld(World):
for loc in self.get_locations():
if loc.address is not None and (
not loc.show_in_spoiler or oot_is_item_of_type(loc.item, 'Shop')
or (self.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
# Handle item-linked dungeon items and songs
@classmethod
def stage_pre_fill(cls, multiworld: MultiWorld):
def gather_locations(item_type: str, players: AbstractSet[int], dungeon: str = '') -> Optional[List[OOTLocation]]:
type_to_setting = {
'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
'BossKey': 'shuffle_bosskeys',
'HideoutSmallKey': 'shuffle_fortresskeys',
'GanonBossKey': 'shuffle_ganon_bosskey',
}
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():
if option == 'dungeon':
condition = lambda location: getattr(location.parent_region.dungeon, 'name', None) == dungeon
elif option == 'overworld':
condition = lambda location: location.parent_region.dungeon is None
elif option == 'any_dungeon':
condition = lambda location: location.parent_region.dungeon is not None
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
return locations
special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
for group_id, group in multiworld.groups.items():
if group['game'] != cls.game:
@@ -869,7 +899,7 @@ class OOTWorld(World):
continue
if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']:
# No need to subdivide by dungeon name
locations = gather_locations(fill_stage, group['players'])
locations = gather_locations(multiworld, fill_stage, group['players'])
if isinstance(locations, list):
for item in group_stage_items:
multiworld.itempool.remove(item)
@@ -889,7 +919,7 @@ class OOTWorld(World):
# Perform the fill task once per dungeon
for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name']
locations = gather_locations(fill_stage, group['players'], dungeon=dungeon_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:
@@ -916,12 +946,20 @@ class OOTWorld(World):
rom = Rom(file=get_options()['oot_options']['rom_file'])
if self.hints != 'none':
buildWorldGossipHints(self)
# try:
patch_rom(self, rom)
# except Exception as e:
# print(e)
patch_cosmetics(self, rom)
rom.update_header()
create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5'))
patch_data = create_patch_file(rom)
rom.restore()
apz5 = OoTContainer(patch_data, outfile_name, output_directory,
player=self.player,
player_name=self.multiworld.get_player_name(self.player))
apz5.write()
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(reverse=True, key=lambda x: x.name)
@@ -966,7 +1004,7 @@ class OOTWorld(World):
items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True}
for d in multiworld.worlds[player].dungeons:
items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True}
del (items_by_region[player]["Link's Pocket"])
del (items_by_region[player]["Link's pocket"])
del (items_by_region[player][None])
if item_hint_players: # loop once over all locations to gather major items. Check oot locations for barren/woth if needed
@@ -980,7 +1018,7 @@ 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_fortresskeys == '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'))):
if loc.player in barren_hint_players:
@@ -1017,22 +1055,42 @@ class OOTWorld(World):
for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
autoworld.hint_data_available.set()
def fill_slot_data(self):
self.collectible_flags_available.wait()
return {
'collectible_override_flags': self.collectible_override_flags,
'collectible_flag_offsets': self.collectible_flag_offsets
}
def modify_multidata(self, multidata: dict):
# Replace connect name
multidata['connect_names'][self.connect_name] = multidata['connect_names'][self.multiworld.player_name[self.player]]
# Remove undesired items from start_inventory
# This is because we don't want them to show up in the autotracker,
# they just don't exist in-game.
for item_name in self.remove_from_start_inventory:
item_id = self.item_name_to_id.get(item_name, None)
if item_id is None:
continue
multidata["precollected_items"][self.player].remove(item_id)
def extend_hint_information(self, er_hint_data: dict):
er_hint_data[self.player] = {}
hint_entrances = set()
for entrance in entrance_shuffle_table:
hint_entrances.add(entrance[1][0])
if len(entrance) > 2:
hint_entrances.add(entrance[2][0])
if entrance[0] in {'Dungeon', 'DungeonSpecial', 'Interior', 'SpecialInterior', 'Grotto', 'Grave'}:
hint_entrances.add(entrance[1][0])
# Get main hint entrance to region.
# If the region is directly adjacent to a hint-entrance, we return that one.
# If it's in a dungeon, scan all the entrances for all the regions in the dungeon.
# This should terminate on the first region anyway, but we scan everything to be safe.
# If it's one of the special cases, go one level deeper.
# If it's a boss room, go one level deeper to the boss door region, which is in a dungeon.
# Otherwise return None.
def get_entrance_to_region(region):
special_case_regions = {
@@ -1048,33 +1106,50 @@ class OOTWorld(World):
for e in r.entrances:
if e.name in hint_entrances:
return e
if region.name in special_case_regions:
if region.is_boss_room or region.name in special_case_regions:
return get_entrance_to_region(region.entrances[0].parent_region)
return None
# Remove undesired items from start_inventory
# This is because we don't want them to show up in the autotracker,
# they just don't exist in-game.
for item_name in self.remove_from_start_inventory:
item_id = self.item_name_to_id.get(item_name, None)
if item_id is None:
continue
multidata["precollected_items"][self.player].remove(item_id)
# Add ER hint data
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
er_hint_data = {}
if (self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances
or self.shuffle_grotto_entrances or self.shuffle_bosses != 'off'):
for region in self.regions:
if not any(bool(loc.address) for loc in region.locations): # check if region has any non-event locations
continue
main_entrance = get_entrance_to_region(region)
if main_entrance is not None and main_entrance.shuffled:
if main_entrance is not None and (main_entrance.shuffled or (region.is_boss_room and self.shuffle_bosses != 'off')):
for location in region.locations:
if type(location.address) == int:
er_hint_data[location.address] = main_entrance.name
multidata['er_hint_data'][self.player] = er_hint_data
er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
# 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
# picked up.
def collect(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
state.prog_items[alt_item_name, self.player] += count
return True
return super().collect(state, item)
def remove(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
state.prog_items[alt_item_name, self.player] -= count
if state.prog_items[alt_item_name, self.player] < 1:
del (state.prog_items[alt_item_name, self.player])
return True
return super().remove(state, item)
# Helper functions
def region_has_shortcuts(self, regionname):
region = self.get_region(regionname)
if not region.dungeon:
region = region.entrances[0].parent_region
return region.dungeon.name in self.dungeon_shortcuts
def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and
(type == None or entrance.type == type) and
@@ -1115,7 +1190,7 @@ class OOTWorld(World):
return False
if item.type == 'SmallKey' and self.shuffle_smallkeys in ['dungeon', 'vanilla']:
return False
if item.type == 'HideoutSmallKey' and self.shuffle_fortresskeys == 'vanilla':
if item.type == 'HideoutSmallKey' and self.shuffle_hideoutkeys == 'vanilla':
return False
if item.type == 'BossKey' and self.shuffle_bosskeys in ['dungeon', 'vanilla']:
return False
@@ -1130,7 +1205,7 @@ class OOTWorld(World):
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 and item_table[item][0] != 'DungeonReward')):
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}
@@ -1152,3 +1227,63 @@ class OOTWorld(World):
def get_filler_item_name(self) -> str:
return get_junk_item(count=1, pool=get_junk_pool(self))[0]
def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool:
if option == 'dungeon':
return (getattr(loc.parent_region.dungeon, 'name', None) == dungeon
and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
elif option == 'any_dungeon':
return (loc.parent_region.dungeon is not None
and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
elif option == 'overworld':
return (loc.parent_region.dungeon is None
and (loc.type != 'Shop' or loc.name in world.shop_prices)
and (world.shuffle_song_items != 'song' or loc.type != 'Song')
and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
elif option == 'regional':
color = HintArea.for_dungeon(dungeon).color
return (HintArea.at(loc).color == color
and (loc.type != 'Shop' or loc.name in world.shop_prices)
and (world.shuffle_song_items != 'song' or loc.type != 'Song')
and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
return False
# raise ValueError(f'Unexpected argument to valid_dungeon_item_location: {option}')
def gather_locations(multiworld: MultiWorld,
item_type: str,
players: Union[int, AbstractSet[int]],
dungeon: str = ''
) -> Optional[List[OOTLocation]]:
type_to_setting = {
'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
'BossKey': 'shuffle_bosskeys',
'HideoutSmallKey': 'shuffle_hideoutkeys',
'GanonBossKey': 'shuffle_ganon_bosskey',
}
if isinstance(players, int):
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 in {'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