Ocarina of Time: options and general cleanup (#3767)
* working? * missed one * fix old start inventory usage * missed global random usage --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
parent
fced9050a4
commit
025c550991
|
@ -1,9 +1,9 @@
|
|||
from .Utils import data_path, __version__
|
||||
from .Colors import *
|
||||
import logging
|
||||
import worlds.oot.Music as music
|
||||
import worlds.oot.Sounds as sfx
|
||||
import worlds.oot.IconManip as icon
|
||||
from . import Music as music
|
||||
from . import Sounds as sfx
|
||||
from . import IconManip as icon
|
||||
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
|
||||
import json
|
||||
|
||||
|
@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols):
|
|||
|
||||
# handle random
|
||||
if tunic_option == 'Random Choice':
|
||||
tunic_option = random.choice(tunic_color_list)
|
||||
tunic_option = ootworld.random.choice(tunic_color_list)
|
||||
# handle completely random
|
||||
if tunic_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols):
|
|||
|
||||
# choose a random choice for the whole group
|
||||
if navi_option_inner == 'Random Choice':
|
||||
navi_option_inner = random.choice(navi_color_list)
|
||||
navi_option_inner = ootworld.random.choice(navi_color_list)
|
||||
if navi_option_outer == 'Random Choice':
|
||||
navi_option_outer = random.choice(navi_color_list)
|
||||
navi_option_outer = ootworld.random.choice(navi_color_list)
|
||||
|
||||
if navi_option_outer == 'Match Inner':
|
||||
navi_option_outer = navi_option_inner
|
||||
|
@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols):
|
|||
|
||||
# handle random choice
|
||||
if option_inner == 'Random Choice':
|
||||
option_inner = random.choice(sword_trail_color_list)
|
||||
option_inner = ootworld.random.choice(sword_trail_color_list)
|
||||
if option_outer == 'Random Choice':
|
||||
option_outer = random.choice(sword_trail_color_list)
|
||||
option_outer = ootworld.random.choice(sword_trail_color_list)
|
||||
|
||||
if option_outer == 'Match Inner':
|
||||
option_outer = option_inner
|
||||
|
@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails):
|
|||
|
||||
# handle random choice
|
||||
if option_inner == 'Random Choice':
|
||||
option_inner = random.choice(trail_color_list)
|
||||
option_inner = ootworld.random.choice(trail_color_list)
|
||||
if option_outer == 'Random Choice':
|
||||
option_outer = random.choice(trail_color_list)
|
||||
option_outer = ootworld.random.choice(trail_color_list)
|
||||
|
||||
if option_outer == 'Match Inner':
|
||||
option_outer = option_inner
|
||||
|
@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols):
|
|||
|
||||
# handle random
|
||||
if gauntlet_option == 'Random Choice':
|
||||
gauntlet_option = random.choice(gauntlet_color_list)
|
||||
gauntlet_option = ootworld.random.choice(gauntlet_color_list)
|
||||
# handle completely random
|
||||
if gauntlet_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols):
|
|||
|
||||
# handle random
|
||||
if shield_frame_option == 'Random Choice':
|
||||
shield_frame_option = random.choice(shield_frame_color_list)
|
||||
shield_frame_option = ootworld.random.choice(shield_frame_color_list)
|
||||
# handle completely random
|
||||
if shield_frame_option == 'Completely Random':
|
||||
color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
|
||||
color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)]
|
||||
# grab the color from the list
|
||||
elif shield_frame_option in shield_frame_colors:
|
||||
color = list(shield_frame_colors[shield_frame_option])
|
||||
|
@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols):
|
|||
|
||||
# handle random
|
||||
if heart_option == 'Random Choice':
|
||||
heart_option = random.choice(heart_color_list)
|
||||
heart_option = ootworld.random.choice(heart_color_list)
|
||||
# handle completely random
|
||||
if heart_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols):
|
|||
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
|
||||
|
||||
if magic_option == 'Random Choice':
|
||||
magic_option = random.choice(magic_color_list)
|
||||
magic_option = ootworld.random.choice(magic_color_list)
|
||||
|
||||
if magic_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols):
|
|||
|
||||
# handle random
|
||||
if button_option == 'Random Choice':
|
||||
button_option = random.choice(list(button_colors.keys()))
|
||||
button_option = ootworld.random.choice(list(button_colors.keys()))
|
||||
# handle completely random
|
||||
if button_option == 'Completely Random':
|
||||
fixed_font_color = [10, 10, 10]
|
||||
|
@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols):
|
|||
rom.write_int16(loc, sound_id)
|
||||
else:
|
||||
if selection == 'random-choice':
|
||||
selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
|
||||
selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword
|
||||
elif selection == 'random-ear-safe':
|
||||
selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
|
||||
selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
|
||||
elif selection == 'completely-random':
|
||||
selection = random.choice(sfx.standard).value.keyword
|
||||
selection = ootworld.random.choice(sfx.standard).value.keyword
|
||||
sound_id = sound_dict[selection]
|
||||
for loc in hook.value.locations:
|
||||
rom.write_int16(loc, sound_id)
|
||||
|
@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols):
|
|||
|
||||
choice = ootworld.sfx_ocarina
|
||||
if choice == 'random-choice':
|
||||
choice = random.choice(list(instruments.keys()))
|
||||
choice = ootworld.random.choice(list(instruments.keys()))
|
||||
|
||||
rom.write_byte(0x00B53C7B, instruments[choice])
|
||||
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
|
||||
|
@ -769,7 +769,6 @@ patch_sets[0x1F073FD9] = {
|
|||
|
||||
def patch_cosmetics(ootworld, rom):
|
||||
# Use the world's slot seed for cosmetics
|
||||
random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random())
|
||||
|
||||
# try to detect the cosmetic patch data format
|
||||
versioned_patch_set = None
|
||||
|
|
|
@ -3,9 +3,9 @@ from BaseClasses import Entrance
|
|||
class OOTEntrance(Entrance):
|
||||
game: str = 'Ocarina of Time'
|
||||
|
||||
def __init__(self, player, world, name='', parent=None):
|
||||
def __init__(self, player, multiworld, name='', parent=None):
|
||||
super(OOTEntrance, self).__init__(player, name, parent)
|
||||
self.multiworld = world
|
||||
self.multiworld = multiworld
|
||||
self.access_rules = []
|
||||
self.reverse = None
|
||||
self.replaces = None
|
||||
|
|
|
@ -440,16 +440,16 @@ class EntranceShuffleError(Exception):
|
|||
|
||||
|
||||
def shuffle_random_entrances(ootworld):
|
||||
world = ootworld.multiworld
|
||||
multiworld = ootworld.multiworld
|
||||
player = ootworld.player
|
||||
|
||||
# Gather locations to keep reachable for validation
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_advancements(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 multiworld.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_all_entrances_data(world, player)
|
||||
set_all_entrances_data(multiworld, player)
|
||||
|
||||
# Determine entrance pools based on settings
|
||||
one_way_entrance_pools = {}
|
||||
|
@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld):
|
|||
none_state = CollectionState(ootworld.multiworld)
|
||||
|
||||
# Plando entrances
|
||||
if world.plando_connections[player]:
|
||||
if ootworld.options.plando_connections:
|
||||
rollbacks = []
|
||||
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
|
||||
for conn in world.plando_connections[player]:
|
||||
for conn in ootworld.options.plando_connections:
|
||||
try:
|
||||
entrance = ootworld.get_entrance(conn.entrance)
|
||||
exit = ootworld.get_entrance(conn.exit)
|
||||
|
@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld):
|
|||
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
|
||||
# Game is beatable
|
||||
new_all_state = ootworld.get_state_with_complete_itempool()
|
||||
if not world.has_beaten_game(new_all_state, player):
|
||||
if not multiworld.has_beaten_game(new_all_state, player):
|
||||
raise EntranceShuffleError('Cannot beat game')
|
||||
# Validate world
|
||||
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
||||
|
@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
|
|||
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
|
||||
|
||||
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
|
||||
ootworld.multiworld.random.shuffle(avail_pool)
|
||||
ootworld.random.shuffle(avail_pool)
|
||||
|
||||
for entrance in avail_pool:
|
||||
if entrance.replaces:
|
||||
|
@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances,
|
|||
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
|
||||
|
||||
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||
ootworld.multiworld.random.shuffle(entrances)
|
||||
ootworld.random.shuffle(entrances)
|
||||
for entrance in entrances:
|
||||
if entrance.connected_region != None:
|
||||
continue
|
||||
ootworld.multiworld.random.shuffle(target_entrances)
|
||||
ootworld.random.shuffle(target_entrances)
|
||||
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
|
||||
# success rate over randomization
|
||||
if pool_type in {'InteriorSoft', 'MixedSoft'}:
|
||||
|
@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
|
|||
# TODO: improve this function
|
||||
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
|
||||
|
||||
world = ootworld.multiworld
|
||||
multiworld = ootworld.multiworld
|
||||
player = ootworld.player
|
||||
|
||||
all_state = all_state_orig.copy()
|
||||
|
@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
|
||||
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
|
||||
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
|
||||
potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||
potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||
potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||
potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
|
||||
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
|
||||
elif (potion_front and not potion_back) or (not potion_front and potion_back):
|
||||
|
@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||
|
||||
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
|
||||
if ootworld.shuffle_cows:
|
||||
impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||
impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||
impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||
impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
|
||||
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
|
||||
elif (impas_front and not impas_back) or (not impas_front and impas_back):
|
||||
|
@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
|
||||
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
|
||||
|
||||
if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
|
||||
if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
|
||||
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
|
||||
if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
|
||||
if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
|
||||
raise EntranceShuffleError('Path to ToT as child not guaranteed')
|
||||
|
||||
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
|
||||
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
|
||||
# Ensure big poe shop is always reachable as adult
|
||||
if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
|
||||
if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
|
||||
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
|
||||
if ootworld.shopsanity == 'off':
|
||||
# Ensure that Goron and Zora shops are accessible as adult
|
||||
if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||
if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||
raise EntranceShuffleError('Goron City Shop not accessible as adult')
|
||||
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||
if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
|
||||
if ootworld.open_forest == 'closed':
|
||||
# Ensure that Kokiri Shop is reachable as child with no items
|
||||
if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
|
||||
if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
|
||||
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import random
|
||||
|
||||
from BaseClasses import LocationProgressType
|
||||
from .Items import OOTItem
|
||||
|
||||
|
@ -28,7 +26,7 @@ class Hint(object):
|
|||
text = ""
|
||||
type = []
|
||||
|
||||
def __init__(self, name, text, type, choice=None):
|
||||
def __init__(self, name, text, type, rand, choice=None):
|
||||
self.name = name
|
||||
self.type = [type] if not isinstance(type, list) else type
|
||||
|
||||
|
@ -36,31 +34,31 @@ class Hint(object):
|
|||
self.text = text
|
||||
else:
|
||||
if choice == None:
|
||||
self.text = random.choice(text)
|
||||
self.text = rand.choice(text)
|
||||
else:
|
||||
self.text = text[choice]
|
||||
|
||||
|
||||
def getHint(item, clearer_hint=False):
|
||||
def getHint(item, rand, clearer_hint=False):
|
||||
if item in hintTable:
|
||||
textOptions, clearText, hintType = hintTable[item]
|
||||
if clearer_hint:
|
||||
if clearText == None:
|
||||
return Hint(item, textOptions, hintType, 0)
|
||||
return Hint(item, clearText, hintType)
|
||||
return Hint(item, textOptions, hintType, rand, 0)
|
||||
return Hint(item, clearText, hintType, rand)
|
||||
else:
|
||||
return Hint(item, textOptions, hintType)
|
||||
return Hint(item, textOptions, hintType, rand)
|
||||
elif isinstance(item, str):
|
||||
return Hint(item, item, 'generic')
|
||||
return Hint(item, item, 'generic', rand)
|
||||
else: # is an Item
|
||||
return Hint(item.name, item.hint_text, 'item')
|
||||
return Hint(item.name, item.hint_text, 'item', rand)
|
||||
|
||||
|
||||
def getHintGroup(group, world):
|
||||
ret = []
|
||||
for name in hintTable:
|
||||
|
||||
hint = getHint(name, world.clearer_hints)
|
||||
hint = getHint(name, world.random, world.clearer_hints)
|
||||
|
||||
if hint.name in world.always_hints and group == 'always':
|
||||
hint.type = 'always'
|
||||
|
@ -95,7 +93,7 @@ def getHintGroup(group, world):
|
|||
def getRequiredHints(world):
|
||||
ret = []
|
||||
for name in hintTable:
|
||||
hint = getHint(name)
|
||||
hint = getHint(name, world.random)
|
||||
if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
|
||||
ret.append(hint)
|
||||
return ret
|
||||
|
@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False):
|
|||
|
||||
location_hints = []
|
||||
for name in hintTable:
|
||||
hint = getHint(name, world.clearer_hints)
|
||||
hint = getHint(name, world.random, world.clearer_hints)
|
||||
if any(item in hint.type for item in
|
||||
['always',
|
||||
'dual_always',
|
||||
|
|
|
@ -136,13 +136,13 @@ def getItemGenericName(item):
|
|||
def isRestrictedDungeonItem(dungeon, item):
|
||||
if not isinstance(item, OOTItem):
|
||||
return False
|
||||
if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon':
|
||||
if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon':
|
||||
return item in dungeon.dungeon_items
|
||||
if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon':
|
||||
if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon':
|
||||
return item in dungeon.small_keys
|
||||
if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon':
|
||||
if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon':
|
||||
return item in dungeon.boss_key
|
||||
if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon':
|
||||
if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon':
|
||||
return item in dungeon.boss_key
|
||||
return False
|
||||
|
||||
|
@ -261,8 +261,8 @@ hintPrefixes = [
|
|||
'',
|
||||
]
|
||||
|
||||
def getSimpleHintNoPrefix(item):
|
||||
hint = getHint(item.name, True).text
|
||||
def getSimpleHintNoPrefix(item, rand):
|
||||
hint = getHint(item.name, rand, True).text
|
||||
|
||||
for prefix in hintPrefixes:
|
||||
if hint.startswith(prefix):
|
||||
|
@ -417,9 +417,9 @@ class HintArea(Enum):
|
|||
|
||||
# Formats the hint text for this area with proper grammar.
|
||||
# Dungeons are hinted differently depending on the clearer_hints setting.
|
||||
def text(self, clearer_hints, preposition=False, world=None):
|
||||
def text(self, rand, clearer_hints, preposition=False, world=None):
|
||||
if self.is_dungeon:
|
||||
text = getHint(self.dungeon_name, clearer_hints).text
|
||||
text = getHint(self.dungeon_name, rand, clearer_hints).text
|
||||
else:
|
||||
text = str(self)
|
||||
prefix, suffix = text.replace('#', '').split(' ', 1)
|
||||
|
@ -489,7 +489,7 @@ def get_woth_hint(world, checked):
|
|||
|
||||
if getattr(location.parent_region, "dungeon", None):
|
||||
world.woth_dungeon += 1
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text
|
||||
else:
|
||||
location_text = get_hint_area(location)
|
||||
|
||||
|
@ -570,9 +570,9 @@ def get_good_item_hint(world, checked):
|
|||
location = world.hint_rng.choice(locations)
|
||||
checked[location.player].add(location.name)
|
||||
|
||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||
if getattr(location.parent_region, "dungeon", None):
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||
['Green', 'Red']), location)
|
||||
else:
|
||||
|
@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked):
|
|||
|
||||
location = world.hint_rng.choice(locations)
|
||||
checked[location.player].add(location.name)
|
||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||
|
||||
if getattr(location.parent_region, "dungeon", None):
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
||||
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||
if world.hint_dist_user.get('vague_named_items', False):
|
||||
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
|
||||
else:
|
||||
|
@ -648,9 +648,9 @@ def get_random_location_hint(world, checked):
|
|||
checked[location.player].add(location.name)
|
||||
dungeon = location.parent_region.dungeon
|
||||
|
||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||
if dungeon:
|
||||
location_text = getHint(dungeon.name, world.clearer_hints).text
|
||||
location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||
['Green', 'Red']), location)
|
||||
else:
|
||||
|
@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type):
|
|||
location_text = hint.text
|
||||
if '#' not in location_text:
|
||||
location_text = '#%s#' % location_text
|
||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||
|
||||
return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||
['Green', 'Red']), location)
|
||||
|
@ -724,9 +724,9 @@ def get_entrance_hint(world, checked):
|
|||
|
||||
connected_region = entrance.connected_region
|
||||
if connected_region.dungeon:
|
||||
region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
|
||||
region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||
else:
|
||||
region_text = getHint(connected_region.name, world.clearer_hints).text
|
||||
region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text
|
||||
|
||||
if '#' not in region_text:
|
||||
region_text = '#%s#' % region_text
|
||||
|
@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None):
|
|||
if location.name in world.hint_text_overrides:
|
||||
location_text = world.hint_text_overrides[location.name]
|
||||
else:
|
||||
location_text = getHint(location.name, world.clearer_hints).text
|
||||
location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text
|
||||
if '#' not in location_text:
|
||||
location_text = '#%s#' % location_text
|
||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||
add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||
['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
|
||||
logging.getLogger('').debug('Placed always hint for %s.', location.name)
|
||||
|
@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
|
|||
('Goron Ruby', 'Red'),
|
||||
('Zora Sapphire', 'Blue'),
|
||||
]
|
||||
child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
|
||||
child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
|
||||
for (reward, color) in bossRewardsSpiritualStones:
|
||||
child_text += buildBossString(reward, color, world)
|
||||
child_text += getHint('Child Altar Text End', world.clearer_hints).text
|
||||
child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text
|
||||
child_text += '\x0B'
|
||||
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
|
||||
|
||||
# text that appears at altar as an adult.
|
||||
adult_text = '\x08'
|
||||
adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
|
||||
adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
|
||||
if include_rewards:
|
||||
bossRewardsMedallions = [
|
||||
('Light Medallion', 'Light Blue'),
|
||||
|
@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
|
|||
adult_text += '\x04'
|
||||
adult_text += buildGanonBossKeyString(world)
|
||||
else:
|
||||
adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
|
||||
adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text
|
||||
adult_text += '\x0B'
|
||||
update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
|
||||
|
||||
|
@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world):
|
|||
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
|
||||
else:
|
||||
location = world.hinted_dungeon_reward_locations[reward]
|
||||
location_text = HintArea.at(location).text(world.clearer_hints, preposition=True)
|
||||
location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True)
|
||||
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
|
||||
return str(text) + '\x04'
|
||||
|
||||
|
@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world):
|
|||
if world.bridge == 'open':
|
||||
string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
|
||||
else:
|
||||
item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
|
||||
item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text
|
||||
if world.bridge == 'medallions':
|
||||
item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
|
||||
elif world.bridge == 'stones':
|
||||
|
@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world):
|
|||
string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
|
||||
else:
|
||||
if world.shuffle_ganon_bosskey == 'on_lacs':
|
||||
item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
|
||||
item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text
|
||||
if world.lacs_condition == 'medallions':
|
||||
item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
|
||||
elif world.lacs_condition == 'stones':
|
||||
|
@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world):
|
|||
item_req_string = '#%s#' % item_req_string
|
||||
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
|
||||
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
|
||||
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
||||
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
|
||||
if world.shuffle_ganon_bosskey == 'medallions':
|
||||
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
|
||||
elif world.shuffle_ganon_bosskey == 'stones':
|
||||
|
@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world):
|
|||
item_req_string = '#%s#' % item_req_string
|
||||
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
|
||||
else:
|
||||
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
||||
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
|
||||
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
|
||||
return str(GossipText(string, ['Yellow'], prefix=''))
|
||||
|
||||
|
@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages):
|
|||
if location.player != world.player:
|
||||
player_text = world.multiworld.get_player_name(location.player) + "'s "
|
||||
if location.game == 'Ocarina of Time':
|
||||
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
|
||||
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None)
|
||||
else:
|
||||
area = location.name
|
||||
text = data['default_item_text'].format(area=rom_safe_text(player_text + area))
|
||||
elif 'default_item_fallback' in data:
|
||||
text = data['default_item_fallback']
|
||||
else:
|
||||
text = getHint('Validation Line', world.clearer_hints).text
|
||||
text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text
|
||||
location = world.get_location('Ganons Tower Boss Key Chest')
|
||||
text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#"
|
||||
text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#"
|
||||
for find, replace in data.get('replace', {}).items():
|
||||
text = text.replace(find, replace)
|
||||
|
||||
|
@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages):
|
|||
if hint_type in world.misc_hints:
|
||||
location = world.get_location(data['item_location'])
|
||||
item = location.item
|
||||
item_text = getHint(getItemGenericName(item), world.clearer_hints).text
|
||||
item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text
|
||||
if item.player != world.player:
|
||||
item_text += f' for {world.multiworld.get_player_name(item.player)}'
|
||||
text = data['location_text'].format(item=rom_safe_text(item_text))
|
||||
|
|
|
@ -295,16 +295,14 @@ random = None
|
|||
|
||||
def get_junk_pool(ootworld):
|
||||
junk_pool[:] = list(junk_pool_base)
|
||||
if ootworld.junk_ice_traps == 'on':
|
||||
if ootworld.options.junk_ice_traps == 'on':
|
||||
junk_pool.append(('Ice Trap', 10))
|
||||
elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
|
||||
elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']:
|
||||
junk_pool[:] = [('Ice Trap', 1)]
|
||||
return junk_pool
|
||||
|
||||
|
||||
def get_junk_item(count=1, pool=None, plando_pool=None):
|
||||
global random
|
||||
|
||||
def get_junk_item(rand, count=1, pool=None, plando_pool=None):
|
||||
if count < 1:
|
||||
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
|
||||
|
||||
|
@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None):
|
|||
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
|
||||
else:
|
||||
junk_items, junk_weights = zip(*junk_pool)
|
||||
return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
|
||||
return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count))
|
||||
|
||||
return return_pool
|
||||
|
||||
|
||||
def replace_max_item(items, item, max):
|
||||
def replace_max_item(items, item, max, rand):
|
||||
count = 0
|
||||
for i,val in enumerate(items):
|
||||
if val == item:
|
||||
if count >= max:
|
||||
items[i] = get_junk_item()[0]
|
||||
items[i] = get_junk_item(rand)[0]
|
||||
count += 1
|
||||
|
||||
|
||||
|
@ -375,7 +373,7 @@ def get_pool_core(world):
|
|||
pending_junk_pool.append('Kokiri Sword')
|
||||
if world.shuffle_ocarinas:
|
||||
pending_junk_pool.append('Ocarina')
|
||||
if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0):
|
||||
if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0):
|
||||
pending_junk_pool.append('Magic Bean Pack')
|
||||
if (world.gerudo_fortress != "open"
|
||||
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
|
||||
|
@ -450,7 +448,7 @@ def get_pool_core(world):
|
|||
else:
|
||||
item = deku_scrubs_items[location.vanilla_item]
|
||||
if isinstance(item, list):
|
||||
item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
|
||||
item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
|
||||
shuffle_item = True
|
||||
|
||||
# Kokiri Sword
|
||||
|
@ -489,7 +487,7 @@ def get_pool_core(world):
|
|||
# Cows
|
||||
elif location.vanilla_item == 'Milk':
|
||||
if world.shuffle_cows:
|
||||
item = get_junk_item()[0]
|
||||
item = get_junk_item(world.random)[0]
|
||||
shuffle_item = world.shuffle_cows
|
||||
if not shuffle_item:
|
||||
location.show_in_spoiler = False
|
||||
|
@ -508,13 +506,13 @@ def get_pool_core(world):
|
|||
item = 'Rutos Letter'
|
||||
ruto_bottles -= 1
|
||||
else:
|
||||
item = random.choice(normal_bottles)
|
||||
item = world.random.choice(normal_bottles)
|
||||
shuffle_item = True
|
||||
|
||||
# Magic Beans
|
||||
elif location.vanilla_item == 'Buy Magic Bean':
|
||||
if world.shuffle_beans:
|
||||
item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0]
|
||||
item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0]
|
||||
shuffle_item = world.shuffle_beans
|
||||
if not shuffle_item:
|
||||
location.show_in_spoiler = False
|
||||
|
@ -528,7 +526,7 @@ def get_pool_core(world):
|
|||
# Adult Trade Item
|
||||
elif location.vanilla_item == 'Pocket Egg':
|
||||
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
|
||||
item = random.choice(sorted(potential_trade_items))
|
||||
item = world.random.choice(sorted(potential_trade_items))
|
||||
world.selected_adult_trade_item = item
|
||||
shuffle_item = True
|
||||
|
||||
|
@ -541,7 +539,7 @@ def get_pool_core(world):
|
|||
shuffle_item = False
|
||||
location.show_in_spoiler = False
|
||||
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
|
||||
item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
|
||||
item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
|
||||
|
||||
# Freestanding Rupees and Hearts
|
||||
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
|
||||
|
@ -618,7 +616,7 @@ def get_pool_core(world):
|
|||
elif dungeon.name in world.key_rings and not dungeon.small_keys:
|
||||
item = dungeon.item_name("Small Key Ring")
|
||||
elif dungeon.name in world.key_rings:
|
||||
item = get_junk_item()[0]
|
||||
item = get_junk_item(world.random)[0]
|
||||
shuffle_item = True
|
||||
# Any other item in a dungeon.
|
||||
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
|
||||
|
@ -630,7 +628,7 @@ def get_pool_core(world):
|
|||
if shuffle_setting in ['remove', 'startwith']:
|
||||
world.multiworld.push_precollected(dungeon_collection[-1])
|
||||
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
|
||||
item = get_junk_item()[0]
|
||||
item = get_junk_item(world.random)[0]
|
||||
shuffle_item = True
|
||||
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
|
||||
dungeon_collection[-1].priority = True
|
||||
|
@ -658,9 +656,9 @@ def get_pool_core(world):
|
|||
shop_non_item_count = len(world.shop_prices)
|
||||
shop_item_count = shop_slots_count - shop_non_item_count
|
||||
|
||||
pool.extend(random.sample(remain_shop_items, shop_item_count))
|
||||
pool.extend(world.random.sample(remain_shop_items, shop_item_count))
|
||||
if shop_non_item_count:
|
||||
pool.extend(get_junk_item(shop_non_item_count))
|
||||
pool.extend(get_junk_item(world.random, shop_non_item_count))
|
||||
|
||||
# Extra rupees for shopsanity.
|
||||
if world.shopsanity not in ['off', '0']:
|
||||
|
@ -706,19 +704,19 @@ def get_pool_core(world):
|
|||
|
||||
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
|
||||
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
|
||||
pool.extend(get_junk_item())
|
||||
pool.extend(get_junk_item(world.random))
|
||||
else:
|
||||
placed_items['Gift from Sages'] = IGNORE_LOCATION
|
||||
world.get_location('Gift from Sages').show_in_spoiler = False
|
||||
|
||||
if world.junk_ice_traps == 'off':
|
||||
replace_max_item(pool, 'Ice Trap', 0)
|
||||
replace_max_item(pool, 'Ice Trap', 0, world.random)
|
||||
elif world.junk_ice_traps == 'onslaught':
|
||||
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
|
||||
replace_max_item(pool, item, 0)
|
||||
replace_max_item(pool, item, 0, world.random)
|
||||
|
||||
for item, maximum in item_difficulty_max[world.item_pool_value].items():
|
||||
replace_max_item(pool, item, maximum)
|
||||
replace_max_item(pool, item, maximum, world.random)
|
||||
|
||||
# world.distribution.alter_pool(world, pool)
|
||||
|
||||
|
@ -748,7 +746,7 @@ def get_pool_core(world):
|
|||
pending_item = pending_junk_pool.pop()
|
||||
if not junk_candidates:
|
||||
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
|
||||
junk_item = random.choice(junk_candidates)
|
||||
junk_item = world.random.choice(junk_candidates)
|
||||
junk_candidates.remove(junk_item)
|
||||
pool.remove(junk_item)
|
||||
pool.append(pending_item)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# text details: https://wiki.cloudmodding.com/oot/Text_Format
|
||||
|
||||
import random
|
||||
from .HintList import misc_item_hint_table, misc_location_hint_table
|
||||
from .TextBox import line_wrap
|
||||
from .Utils import find_last
|
||||
|
@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe
|
|||
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
# shuffles the messages in the game, making sure to keep various message types in their own group
|
||||
def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
|
||||
def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True):
|
||||
|
||||
permutation = [i for i, _ in enumerate(messages)]
|
||||
|
||||
|
@ -1002,7 +1001,7 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
|
|||
|
||||
def shuffle_group(group):
|
||||
group_permutation = [i for i, _ in enumerate(group)]
|
||||
random.shuffle(group_permutation)
|
||||
rand.shuffle(group_permutation)
|
||||
|
||||
for index_from, index_to in enumerate(group_permutation):
|
||||
permutation[group[index_to].index] = group[index_from].index
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
|
||||
|
||||
import random
|
||||
import os
|
||||
from .Utils import compare_version, data_path
|
||||
|
||||
|
@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence
|
|||
return sequences, target_sequences
|
||||
|
||||
|
||||
def shuffle_music(sequences, target_sequences, music_mapping, log):
|
||||
def shuffle_music(sequences, target_sequences, music_mapping, log, rand):
|
||||
sequence_dict = {}
|
||||
sequence_ids = []
|
||||
|
||||
|
@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log):
|
|||
# Shuffle the sequences
|
||||
if len(sequences) < len(target_sequences):
|
||||
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
|
||||
random.shuffle(sequence_ids)
|
||||
rand.shuffle(sequence_ids)
|
||||
|
||||
sequences = []
|
||||
for target_sequence in target_sequences:
|
||||
|
@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences):
|
|||
rom.write_byte(base, j.instrument_set)
|
||||
|
||||
|
||||
def shuffle_pointers_table(rom, ids, music_mapping, log):
|
||||
def shuffle_pointers_table(rom, ids, music_mapping, log, rand):
|
||||
# Read in all the Music data
|
||||
bgm_data = {}
|
||||
bgm_ids = []
|
||||
|
@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log):
|
|||
bgm_ids.append(bgm[0])
|
||||
|
||||
# shuffle data
|
||||
random.shuffle(bgm_ids)
|
||||
rand.shuffle(bgm_ids)
|
||||
|
||||
# Write Music data back in random ordering
|
||||
for bgm in ids:
|
||||
|
@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping):
|
|||
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
|
||||
# if ootworld.background_music == 'random_custom_only':
|
||||
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
|
||||
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random)
|
||||
|
||||
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
|
||||
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
|
||||
# if ootworld.fanfares == 'random_custom_only':
|
||||
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
|
||||
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random)
|
||||
|
||||
# if disabled_source_sequences:
|
||||
# log = disable_music(rom, disabled_source_sequences.values(), log)
|
||||
|
@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping):
|
|||
# rebuild_sequences(rom, sequences + fanfare_sequences)
|
||||
# else:
|
||||
if ootworld.background_music == 'randomized' or bgm_mapped:
|
||||
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
|
||||
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random)
|
||||
|
||||
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
|
||||
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
|
||||
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random)
|
||||
# end_else
|
||||
if disabled_target_sequences:
|
||||
log = disable_music(rom, disabled_target_sequences.values(), log)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import struct
|
||||
import random
|
||||
import io
|
||||
import array
|
||||
import zlib
|
||||
|
@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue):
|
|||
# xor_range is the range the XOR key will read from. This range is not
|
||||
# too important, but I tried to choose from a section that didn't really
|
||||
# have big gaps of 0s which we want to avoid.
|
||||
def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
|
||||
def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)):
|
||||
dma_start, dma_end = rom.get_dma_table_range()
|
||||
|
||||
# add header
|
||||
|
@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
|
|||
|
||||
# get random xor key. This range is chosen because it generally
|
||||
# doesn't have many sections of 0s
|
||||
xor_address = random.Random().randint(*xor_range)
|
||||
xor_address = rand.randint(*xor_range)
|
||||
patch_data.append_int32(xor_address)
|
||||
|
||||
new_buffer = copy.copy(rom.original.buffer)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import typing
|
||||
import random
|
||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections
|
||||
from dataclasses import dataclass
|
||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \
|
||||
PerGameCommonOptions, OptionGroup
|
||||
from .EntranceShuffle import entrance_shuffle_table
|
||||
from .LogicTricks import normalized_name_tricks
|
||||
from .ColorSFXOptions import *
|
||||
|
@ -1281,21 +1283,166 @@ class LogicTricks(OptionList):
|
|||
valid_keys_casefold = True
|
||||
|
||||
|
||||
# All options assembled into a single dict
|
||||
oot_options: typing.Dict[str, type(Option)] = {
|
||||
"plando_connections": OoTPlandoConnections,
|
||||
"logic_rules": Logic,
|
||||
"logic_no_night_tokens_without_suns_song": NightTokens,
|
||||
**open_options,
|
||||
**world_options,
|
||||
**bridge_options,
|
||||
**dungeon_items_options,
|
||||
**shuffle_options,
|
||||
**timesavers_options,
|
||||
**misc_options,
|
||||
**itempool_options,
|
||||
**cosmetic_options,
|
||||
**sfx_options,
|
||||
"logic_tricks": LogicTricks,
|
||||
"death_link": DeathLink,
|
||||
}
|
||||
@dataclass
|
||||
class OoTOptions(PerGameCommonOptions):
|
||||
plando_connections: OoTPlandoConnections
|
||||
death_link: DeathLink
|
||||
logic_rules: Logic
|
||||
logic_no_night_tokens_without_suns_song: NightTokens
|
||||
logic_tricks: LogicTricks
|
||||
open_forest: Forest
|
||||
open_kakariko: Gate
|
||||
open_door_of_time: DoorOfTime
|
||||
zora_fountain: Fountain
|
||||
gerudo_fortress: Fortress
|
||||
bridge: Bridge
|
||||
trials: Trials
|
||||
starting_age: StartingAge
|
||||
shuffle_interior_entrances: InteriorEntrances
|
||||
shuffle_grotto_entrances: GrottoEntrances
|
||||
shuffle_dungeon_entrances: DungeonEntrances
|
||||
shuffle_overworld_entrances: OverworldEntrances
|
||||
owl_drops: OwlDrops
|
||||
warp_songs: WarpSongs
|
||||
spawn_positions: SpawnPositions
|
||||
shuffle_bosses: BossEntrances
|
||||
# mix_entrance_pools: MixEntrancePools
|
||||
# decouple_entrances: DecoupleEntrances
|
||||
triforce_hunt: TriforceHunt
|
||||
triforce_goal: TriforceGoal
|
||||
extra_triforce_percentage: ExtraTriforces
|
||||
bombchus_in_logic: LogicalChus
|
||||
dungeon_shortcuts: DungeonShortcuts
|
||||
dungeon_shortcuts_list: DungeonShortcutsList
|
||||
mq_dungeons_mode: MQDungeons
|
||||
mq_dungeons_list: MQDungeonList
|
||||
mq_dungeons_count: MQDungeonCount
|
||||
# empty_dungeons_mode: EmptyDungeons
|
||||
# empty_dungeons_list: EmptyDungeonList
|
||||
# empty_dungeon_count: EmptyDungeonCount
|
||||
bridge_stones: BridgeStones
|
||||
bridge_medallions: BridgeMedallions
|
||||
bridge_rewards: BridgeRewards
|
||||
bridge_tokens: BridgeTokens
|
||||
bridge_hearts: BridgeHearts
|
||||
shuffle_mapcompass: ShuffleMapCompass
|
||||
shuffle_smallkeys: ShuffleKeys
|
||||
shuffle_hideoutkeys: ShuffleGerudoKeys
|
||||
shuffle_bosskeys: ShuffleBossKeys
|
||||
enhance_map_compass: EnhanceMC
|
||||
shuffle_ganon_bosskey: ShuffleGanonBK
|
||||
ganon_bosskey_medallions: GanonBKMedallions
|
||||
ganon_bosskey_stones: GanonBKStones
|
||||
ganon_bosskey_rewards: GanonBKRewards
|
||||
ganon_bosskey_tokens: GanonBKTokens
|
||||
ganon_bosskey_hearts: GanonBKHearts
|
||||
key_rings: KeyRings
|
||||
key_rings_list: KeyRingList
|
||||
shuffle_song_items: SongShuffle
|
||||
shopsanity: ShopShuffle
|
||||
shop_slots: ShopSlots
|
||||
shopsanity_prices: ShopPrices
|
||||
tokensanity: TokenShuffle
|
||||
shuffle_scrubs: ScrubShuffle
|
||||
shuffle_child_trade: ShuffleChildTrade
|
||||
shuffle_freestanding_items: ShuffleFreestanding
|
||||
shuffle_pots: ShufflePots
|
||||
shuffle_crates: ShuffleCrates
|
||||
shuffle_cows: ShuffleCows
|
||||
shuffle_beehives: ShuffleBeehives
|
||||
shuffle_kokiri_sword: ShuffleSword
|
||||
shuffle_ocarinas: ShuffleOcarinas
|
||||
shuffle_gerudo_card: ShuffleCard
|
||||
shuffle_beans: ShuffleBeans
|
||||
shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet
|
||||
shuffle_frog_song_rupees: ShuffleFrogRupees
|
||||
no_escape_sequence: SkipEscape
|
||||
no_guard_stealth: SkipStealth
|
||||
no_epona_race: SkipEponaRace
|
||||
skip_some_minigame_phases: SkipMinigamePhases
|
||||
complete_mask_quest: CompleteMaskQuest
|
||||
useful_cutscenes: UsefulCutscenes
|
||||
fast_chests: FastChests
|
||||
free_scarecrow: FreeScarecrow
|
||||
fast_bunny_hood: FastBunny
|
||||
plant_beans: PlantBeans
|
||||
chicken_count: ChickenCount
|
||||
big_poe_count: BigPoeCount
|
||||
fae_torch_count: FAETorchCount
|
||||
correct_chest_appearances: CorrectChestAppearance
|
||||
minor_items_as_major_chest: MinorInMajor
|
||||
invisible_chests: InvisibleChests
|
||||
correct_potcrate_appearances: CorrectPotCrateAppearance
|
||||
hints: Hints
|
||||
misc_hints: MiscHints
|
||||
hint_dist: HintDistribution
|
||||
text_shuffle: TextShuffle
|
||||
damage_multiplier: DamageMultiplier
|
||||
deadly_bonks: DeadlyBonks
|
||||
no_collectible_hearts: HeroMode
|
||||
starting_tod: StartingToD
|
||||
blue_fire_arrows: BlueFireArrows
|
||||
fix_broken_drops: FixBrokenDrops
|
||||
start_with_consumables: ConsumableStart
|
||||
start_with_rupees: RupeeStart
|
||||
item_pool_value: ItemPoolValue
|
||||
junk_ice_traps: IceTraps
|
||||
ice_trap_appearance: IceTrapVisual
|
||||
adult_trade_start: AdultTradeStart
|
||||
default_targeting: Targeting
|
||||
display_dpad: DisplayDpad
|
||||
dpad_dungeon_menu: DpadDungeonMenu
|
||||
correct_model_colors: CorrectColors
|
||||
background_music: BackgroundMusic
|
||||
fanfares: Fanfares
|
||||
ocarina_fanfares: OcarinaFanfares
|
||||
kokiri_color: kokiri_color
|
||||
goron_color: goron_color
|
||||
zora_color: zora_color
|
||||
silver_gauntlets_color: silver_gauntlets_color
|
||||
golden_gauntlets_color: golden_gauntlets_color
|
||||
mirror_shield_frame_color: mirror_shield_frame_color
|
||||
navi_color_default_inner: navi_color_default_inner
|
||||
navi_color_default_outer: navi_color_default_outer
|
||||
navi_color_enemy_inner: navi_color_enemy_inner
|
||||
navi_color_enemy_outer: navi_color_enemy_outer
|
||||
navi_color_npc_inner: navi_color_npc_inner
|
||||
navi_color_npc_outer: navi_color_npc_outer
|
||||
navi_color_prop_inner: navi_color_prop_inner
|
||||
navi_color_prop_outer: navi_color_prop_outer
|
||||
sword_trail_duration: SwordTrailDuration
|
||||
sword_trail_color_inner: sword_trail_color_inner
|
||||
sword_trail_color_outer: sword_trail_color_outer
|
||||
bombchu_trail_color_inner: bombchu_trail_color_inner
|
||||
bombchu_trail_color_outer: bombchu_trail_color_outer
|
||||
boomerang_trail_color_inner: boomerang_trail_color_inner
|
||||
boomerang_trail_color_outer: boomerang_trail_color_outer
|
||||
heart_color: heart_color
|
||||
magic_color: magic_color
|
||||
a_button_color: a_button_color
|
||||
b_button_color: b_button_color
|
||||
c_button_color: c_button_color
|
||||
start_button_color: start_button_color
|
||||
sfx_navi_overworld: sfx_navi_overworld
|
||||
sfx_navi_enemy: sfx_navi_enemy
|
||||
sfx_low_hp: sfx_low_hp
|
||||
sfx_menu_cursor: sfx_menu_cursor
|
||||
sfx_menu_select: sfx_menu_select
|
||||
sfx_nightfall: sfx_nightfall
|
||||
sfx_horse_neigh: sfx_horse_neigh
|
||||
sfx_hover_boots: sfx_hover_boots
|
||||
sfx_ocarina: SfxOcarina
|
||||
|
||||
|
||||
oot_option_groups: typing.List[OptionGroup] = [
|
||||
OptionGroup("Open", [option for option in open_options.values()]),
|
||||
OptionGroup("World", [*[option for option in world_options.values()],
|
||||
*[option for option in bridge_options.values()]]),
|
||||
OptionGroup("Shuffle", [option for option in shuffle_options.values()]),
|
||||
OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]),
|
||||
OptionGroup("Timesavers", [option for option in timesavers_options.values()]),
|
||||
OptionGroup("Misc", [option for option in misc_options.values()]),
|
||||
OptionGroup("Item Pool", [option for option in itempool_options.values()]),
|
||||
OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]),
|
||||
OptionGroup("SFX", [option for option in sfx_options.values()])
|
||||
]
|
||||
|
|
|
@ -208,8 +208,8 @@ def patch_rom(world, rom):
|
|||
|
||||
# Fix Ice Cavern Alcove Camera
|
||||
if not world.dungeon_mq['Ice Cavern']:
|
||||
rom.write_byte(0x2BECA25,0x01);
|
||||
rom.write_byte(0x2BECA2D,0x01);
|
||||
rom.write_byte(0x2BECA25,0x01)
|
||||
rom.write_byte(0x2BECA2D,0x01)
|
||||
|
||||
# Fix GS rewards to be static
|
||||
rom.write_int32(0xEA3934, 0)
|
||||
|
@ -944,7 +944,7 @@ def patch_rom(world, rom):
|
|||
|
||||
scene_table = 0x00B71440
|
||||
for scene in range(0x00, 0x65):
|
||||
scene_start = rom.read_int32(scene_table + (scene * 0x14));
|
||||
scene_start = rom.read_int32(scene_table + (scene * 0x14))
|
||||
add_scene_exits(scene_start)
|
||||
|
||||
return exit_table
|
||||
|
@ -1632,10 +1632,10 @@ def patch_rom(world, rom):
|
|||
reward_text = None
|
||||
elif getattr(location.item, 'looks_like_item', None) is not None:
|
||||
jabu_item = location.item.looks_like_item
|
||||
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text)
|
||||
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text)
|
||||
else:
|
||||
jabu_item = location.item
|
||||
reward_text = getHint(getItemGenericName(location.item), True).text
|
||||
reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text
|
||||
|
||||
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
|
||||
if reward_text is None:
|
||||
|
@ -1687,7 +1687,7 @@ def patch_rom(world, rom):
|
|||
|
||||
# Sets hooks for gossip stone changes
|
||||
|
||||
symbol = rom.sym("GOSSIP_HINT_CONDITION");
|
||||
symbol = rom.sym("GOSSIP_HINT_CONDITION")
|
||||
|
||||
if world.hints == 'none':
|
||||
rom.write_int32(symbol, 0)
|
||||
|
@ -2264,9 +2264,9 @@ def patch_rom(world, rom):
|
|||
|
||||
# text shuffle
|
||||
if world.text_shuffle == 'except_hints':
|
||||
permutation = shuffle_messages(messages, except_hints=True)
|
||||
permutation = shuffle_messages(messages, world.random, except_hints=True)
|
||||
elif world.text_shuffle == 'complete':
|
||||
permutation = shuffle_messages(messages, except_hints=False)
|
||||
permutation = shuffle_messages(messages, world.random, except_hints=False)
|
||||
|
||||
# update warp song preview text boxes
|
||||
update_warp_song_text(messages, world)
|
||||
|
@ -2358,7 +2358,7 @@ def patch_rom(world, rom):
|
|||
|
||||
# Write numeric seed truncated to 32 bits for rng seeding
|
||||
# Overwritten with new seed every time a new rng value is generated
|
||||
rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32)
|
||||
rng_seed = world.random.getrandbits(32)
|
||||
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
|
||||
# Static initial seed value for one-time random actions like the Hylian Shield discount
|
||||
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
|
||||
|
@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process
|
|||
room_count = rom.read_byte(scene_data + 1)
|
||||
room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
|
||||
for _ in range(0, room_count):
|
||||
room_data = rom.read_int32(room_list);
|
||||
room_data = rom.read_int32(room_list)
|
||||
|
||||
if not room_data in processed_rooms:
|
||||
actors.update(room_get_actors(rom, actor_func, room_data, scene))
|
||||
|
@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func):
|
|||
actors = {}
|
||||
scene_table = 0x00B71440
|
||||
for scene in range(0x00, 0x65):
|
||||
scene_data = rom.read_int32(scene_table + (scene * 0x14));
|
||||
scene_data = rom.read_int32(scene_table + (scene * 0x14))
|
||||
actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
|
||||
return actors
|
||||
|
||||
|
@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags):
|
|||
def remove_entrance_blockers(rom):
|
||||
def remove_entrance_blockers_do(rom, actor_id, actor, scene):
|
||||
if actor_id == 0x014E and scene == 97:
|
||||
actor_var = rom.read_int16(actor + 14);
|
||||
actor_var = rom.read_int16(actor + 14)
|
||||
if actor_var == 0xFF01:
|
||||
rom.write_int16(actor + 14, 0x0700)
|
||||
get_actor_list(rom, remove_entrance_blockers_do)
|
||||
|
@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
|||
purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
|
||||
else:
|
||||
if item_display.game == "Ocarina of Time":
|
||||
shop_item_name = getSimpleHintNoPrefix(item_display)
|
||||
shop_item_name = getSimpleHintNoPrefix(item_display, world.random)
|
||||
else:
|
||||
shop_item_name = item_display.name
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ def isliteral(expr):
|
|||
class Rule_AST_Transformer(ast.NodeTransformer):
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.multiworld = world
|
||||
self.world = world
|
||||
self.player = player
|
||||
self.events = set()
|
||||
# map Region -> rule ast string -> item name
|
||||
|
@ -86,9 +86,9 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
ctx=ast.Load()),
|
||||
args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)],
|
||||
keywords=[])
|
||||
elif node.id in self.multiworld.__dict__:
|
||||
elif node.id in self.world.__dict__:
|
||||
# Settings are constant
|
||||
return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body
|
||||
return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body
|
||||
elif node.id in State.__dict__:
|
||||
return self.make_call(node, node.id, [], [])
|
||||
elif node.id in self.kwarg_defaults or node.id in allowed_globals:
|
||||
|
@ -137,7 +137,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
|
||||
if isinstance(count, ast.Name):
|
||||
# Must be a settings constant
|
||||
count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body
|
||||
count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body
|
||||
|
||||
if iname in escaped_items:
|
||||
iname = escaped_items[iname]
|
||||
|
@ -182,7 +182,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
new_args = []
|
||||
for child in node.args:
|
||||
if isinstance(child, ast.Name):
|
||||
if child.id in self.multiworld.__dict__:
|
||||
if child.id in self.world.__dict__:
|
||||
# child = ast.Attribute(
|
||||
# value=ast.Attribute(
|
||||
# value=ast.Name(id='state', ctx=ast.Load()),
|
||||
|
@ -190,7 +190,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
# ctx=ast.Load()),
|
||||
# attr=child.id,
|
||||
# ctx=ast.Load())
|
||||
child = ast.Constant(getattr(self.multiworld, child.id))
|
||||
child = ast.Constant(getattr(self.world, child.id))
|
||||
elif child.id in rule_aliases:
|
||||
child = self.visit(child)
|
||||
elif child.id in escaped_items:
|
||||
|
@ -242,7 +242,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
# Fast check for json can_use
|
||||
if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
|
||||
and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name)
|
||||
and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__):
|
||||
and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__):
|
||||
return ast.NameConstant(node.left.id == node.comparators[0].id)
|
||||
|
||||
node.left = escape_or_string(node.left)
|
||||
|
@ -378,7 +378,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
# Requires the target regions have been defined in the world.
|
||||
def create_delayed_rules(self):
|
||||
for region_name, node, subrule_name in self.delayed_rules:
|
||||
region = self.multiworld.multiworld.get_region(region_name, self.player)
|
||||
region = self.world.multiworld.get_region(region_name, self.player)
|
||||
event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True)
|
||||
event.show_in_spoiler = False
|
||||
|
||||
|
@ -395,7 +395,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
set_rule(event, access_rule)
|
||||
region.locations.append(event)
|
||||
|
||||
self.multiworld.make_event_item(subrule_name, event)
|
||||
self.world.make_event_item(subrule_name, event)
|
||||
# Safeguard in case this is called multiple times per world
|
||||
self.delayed_rules.clear()
|
||||
|
||||
|
@ -448,7 +448,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
## Handlers for compile-time optimizations (former State functions)
|
||||
|
||||
def at_day(self, node):
|
||||
if self.multiworld.ensure_tod_access:
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||
|
@ -456,7 +456,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
return ast.NameConstant(True)
|
||||
|
||||
def at_dampe_time(self, node):
|
||||
if self.multiworld.ensure_tod_access:
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAMPE or (tod == NONE and (find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||
|
@ -464,10 +464,10 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
return ast.NameConstant(True)
|
||||
|
||||
def at_night(self, node):
|
||||
if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song:
|
||||
if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song:
|
||||
# Using visit here to resolve 'can_play' rule
|
||||
return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body)
|
||||
if self.multiworld.ensure_tod_access:
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||
|
@ -501,7 +501,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
|
|||
return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body
|
||||
|
||||
def current_spot_starting_age_access(self, node):
|
||||
return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node)
|
||||
return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node)
|
||||
|
||||
def has_bottle(self, node):
|
||||
return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body
|
||||
|
|
|
@ -10,7 +10,7 @@ from .LocationList import dungeon_song_locations
|
|||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
|
||||
from ..AutoWorld import LogicMixin
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class OOTLogic(LogicMixin):
|
||||
|
@ -132,17 +132,17 @@ class OOTLogic(LogicMixin):
|
|||
def set_rules(ootworld):
|
||||
logger = logging.getLogger('')
|
||||
|
||||
world = ootworld.multiworld
|
||||
multiworld = ootworld.multiworld
|
||||
player = ootworld.player
|
||||
|
||||
if ootworld.logic_rules != 'no_logic':
|
||||
if ootworld.triforce_hunt:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
|
||||
multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
|
||||
else:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||
multiworld.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||
|
||||
# ganon can only carry triforce
|
||||
world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
|
||||
multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
|
||||
|
||||
# is_child = ootworld.parser.parse_rule('is_child')
|
||||
guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
|
||||
|
@ -156,22 +156,22 @@ def set_rules(ootworld):
|
|||
if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon'
|
||||
and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'):
|
||||
# First room chest needs to be a small key. Make sure the boss key isn't placed here.
|
||||
location = world.get_location('Forest Temple MQ First Room Chest', player)
|
||||
location = multiworld.get_location('Forest Temple MQ First Room Chest', player)
|
||||
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
|
||||
|
||||
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)
|
||||
location = multiworld.get_location('Sheik in Ice Cavern', player)
|
||||
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)
|
||||
location = multiworld.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)
|
||||
add_rule(multiworld.get_location(name, player), guarantee_hint)
|
||||
|
||||
# TODO: re-add hints once they are working
|
||||
# if location.type == 'HintStone' and ootworld.hints == 'mask':
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import worlds.oot.Messages as Messages
|
||||
from . import Messages
|
||||
|
||||
# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
|
||||
# characters on a line reach this value.
|
||||
|
|
|
@ -20,7 +20,7 @@ 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
|
||||
from .Options import oot_options
|
||||
from .Options import OoTOptions, oot_option_groups
|
||||
from .Utils import data_path, read_json
|
||||
from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations
|
||||
from .DungeonList import dungeon_table, create_dungeons
|
||||
|
@ -30,12 +30,12 @@ from .Patches import OoTContainer, patch_rom
|
|||
from .N64Patch import create_patch_file
|
||||
from .Cosmetics import patch_cosmetics
|
||||
|
||||
from Utils import get_options
|
||||
from settings import get_settings
|
||||
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
|
||||
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
|
||||
from Fill import fill_restrictive, fast_fill, FillError
|
||||
from worlds.generic.Rules import exclusion_rules, add_item_rule
|
||||
from ..AutoWorld import World, AutoLogicRegister, WebWorld
|
||||
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
||||
|
||||
# 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)
|
||||
|
@ -128,6 +128,7 @@ class OOTWeb(WebWorld):
|
|||
)
|
||||
|
||||
tutorials = [setup, setup_es, setup_fr, setup_de]
|
||||
option_groups = oot_option_groups
|
||||
|
||||
|
||||
class OOTWorld(World):
|
||||
|
@ -137,7 +138,8 @@ class OOTWorld(World):
|
|||
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
|
||||
"""
|
||||
game: str = "Ocarina of Time"
|
||||
option_definitions: dict = oot_options
|
||||
options_dataclass = OoTOptions
|
||||
options: OoTOptions
|
||||
settings: typing.ClassVar[OOTSettings]
|
||||
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
|
||||
|
@ -195,15 +197,15 @@ class OOTWorld(World):
|
|||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
||||
rom = Rom(file=get_settings()['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)
|
||||
|
||||
for (option_name, option) in oot_options.items():
|
||||
result = getattr(self.multiworld, option_name)[self.player]
|
||||
for option_name in self.options_dataclass.type_hints:
|
||||
result = getattr(self.options, option_name)
|
||||
if isinstance(result, Range):
|
||||
option_value = int(result)
|
||||
elif isinstance(result, Toggle):
|
||||
|
@ -223,8 +225,8 @@ class OOTWorld(World):
|
|||
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
|
||||
self.starting_items = Counter()
|
||||
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.file_hash = [self.random.randint(0, 31) for i in range(5)]
|
||||
self.connect_name = ''.join(self.random.choices(printable, k=16))
|
||||
self.collectible_flag_addresses = {}
|
||||
|
||||
# Incompatible option handling
|
||||
|
@ -283,7 +285,7 @@ class OOTWorld(World):
|
|||
local_types.append('BossKey')
|
||||
if self.shuffle_ganon_bosskey != 'keysanity':
|
||||
local_types.append('GanonBossKey')
|
||||
self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types)
|
||||
self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types)
|
||||
|
||||
# If any songs are itemlinked, set songs_as_items
|
||||
for group in self.multiworld.groups.values():
|
||||
|
@ -297,7 +299,7 @@ class OOTWorld(World):
|
|||
# Determine skipped trials in GT
|
||||
# This needs to be done before the logic rules in GT are parsed
|
||||
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
|
||||
chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
|
||||
chosen_trials = self.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 tricks in logic
|
||||
|
@ -311,8 +313,8 @@ class OOTWorld(World):
|
|||
|
||||
# No Logic forces all tricks on, prog balancing off and beatable-only
|
||||
elif self.logic_rules == 'no_logic':
|
||||
self.multiworld.progression_balancing[self.player].value = False
|
||||
self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
|
||||
self.options.progression_balancing.value = False
|
||||
self.options.accessibility.value = Accessibility.option_minimal
|
||||
for trick in normalized_name_tricks.values():
|
||||
setattr(self, trick['name'], True)
|
||||
|
||||
|
@ -333,8 +335,8 @@ class OOTWorld(World):
|
|||
|
||||
# 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_count[self.player].randomized
|
||||
self.trials_random = self.options.trials.randomized
|
||||
self.mq_dungeons_random = self.options.mq_dungeons_count.randomized
|
||||
self.easier_fire_arrow_entry = self.fae_torch_count < 24
|
||||
|
||||
if self.misc_hints:
|
||||
|
@ -393,8 +395,8 @@ class OOTWorld(World):
|
|||
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)))
|
||||
self.key_rings = self.random.sample(keyring_dungeons,
|
||||
self.random.randint(0, len(keyring_dungeons)))
|
||||
|
||||
# Determine which dungeons are MQ. Not compatible with glitched logic.
|
||||
mq_dungeons = set()
|
||||
|
@ -405,7 +407,7 @@ class OOTWorld(World):
|
|||
elif self.mq_dungeons_mode == 'specific':
|
||||
mq_dungeons = self.mq_dungeons_specific
|
||||
elif self.mq_dungeons_mode == 'count':
|
||||
mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count)
|
||||
mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count)
|
||||
else:
|
||||
self.mq_dungeons_mode = 'count'
|
||||
self.mq_dungeons_count = 0
|
||||
|
@ -425,8 +427,8 @@ class OOTWorld(World):
|
|||
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)))
|
||||
self.dungeon_shortcuts = self.random.sample(shortcut_dungeons,
|
||||
self.random.randint(0, len(shortcut_dungeons)))
|
||||
# == 'choice', leave as previous
|
||||
else:
|
||||
self.dungeon_shortcuts = set()
|
||||
|
@ -576,7 +578,7 @@ class OOTWorld(World):
|
|||
new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region)
|
||||
new_exit.vanilla_connected_region = exit
|
||||
new_exit.rule_string = rule
|
||||
if self.multiworld.logic_rules != 'none':
|
||||
if self.options.logic_rules != 'no_logic':
|
||||
self.parser.parse_spot_rule(new_exit)
|
||||
if new_exit.never:
|
||||
logger.debug('Dropping unreachable exit: %s', new_exit.name)
|
||||
|
@ -607,7 +609,7 @@ class OOTWorld(World):
|
|||
elif self.shuffle_scrubs == 'random':
|
||||
# this is a random value between 0-99
|
||||
# average value is ~33 rupees
|
||||
price = int(self.multiworld.random.betavariate(1, 2) * 99)
|
||||
price = int(self.random.betavariate(1, 2) * 99)
|
||||
|
||||
# Set price in the dictionary as well as the location.
|
||||
self.scrub_prices[scrub_item] = price
|
||||
|
@ -624,7 +626,7 @@ class OOTWorld(World):
|
|||
self.shop_prices = {}
|
||||
for region in self.regions:
|
||||
if self.shopsanity == 'random':
|
||||
shop_item_count = self.multiworld.random.randint(0, 4)
|
||||
shop_item_count = self.random.randint(0, 4)
|
||||
else:
|
||||
shop_item_count = int(self.shopsanity)
|
||||
|
||||
|
@ -632,17 +634,17 @@ class OOTWorld(World):
|
|||
if location.type == 'Shop':
|
||||
if location.name[-1:] in shop_item_indexes[:shop_item_count]:
|
||||
if self.shopsanity_prices == 'normal':
|
||||
self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5
|
||||
self.shop_prices[location.name] = int(self.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)
|
||||
self.shop_prices[location.name] = self.random.randrange(0,100,5)
|
||||
elif self.shopsanity_prices == 'adults_wallet':
|
||||
self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5)
|
||||
self.shop_prices[location.name] = self.random.randrange(0,201,5)
|
||||
elif self.shopsanity_prices == 'giants_wallet':
|
||||
self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5)
|
||||
self.shop_prices[location.name] = self.random.randrange(0,501,5)
|
||||
elif self.shopsanity_prices == 'tycoons_wallet':
|
||||
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
|
||||
self.shop_prices[location.name] = self.random.randrange(0,1000,5)
|
||||
|
||||
|
||||
# Fill boss prizes
|
||||
|
@ -667,8 +669,8 @@ class OOTWorld(World):
|
|||
|
||||
while bossCount:
|
||||
bossCount -= 1
|
||||
self.multiworld.random.shuffle(prizepool)
|
||||
self.multiworld.random.shuffle(prize_locs)
|
||||
self.random.shuffle(prizepool)
|
||||
self.random.shuffle(prize_locs)
|
||||
item = prizepool.pop()
|
||||
loc = prize_locs.pop()
|
||||
loc.place_locked_item(item)
|
||||
|
@ -778,7 +780,7 @@ class OOTWorld(World):
|
|||
# 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.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool)))
|
||||
if self.start_with_consumables:
|
||||
self.starting_items['Deku Sticks'] = 30
|
||||
self.starting_items['Deku Nuts'] = 40
|
||||
|
@ -881,7 +883,7 @@ class OOTWorld(World):
|
|||
# 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)
|
||||
self.random.shuffle(locations)
|
||||
|
||||
# Set up initial state
|
||||
state = CollectionState(self.multiworld)
|
||||
|
@ -910,7 +912,7 @@ class OOTWorld(World):
|
|||
if isinstance(locations, list):
|
||||
for item in stage_items:
|
||||
self.pre_fill_items.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
self.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
else:
|
||||
|
@ -923,7 +925,7 @@ class OOTWorld(World):
|
|||
if isinstance(locations, list):
|
||||
for item in dungeon_items:
|
||||
self.pre_fill_items.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
self.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
|
||||
|
@ -964,7 +966,7 @@ class OOTWorld(World):
|
|||
|
||||
while tries:
|
||||
try:
|
||||
self.multiworld.random.shuffle(song_locations)
|
||||
self.random.shuffle(song_locations)
|
||||
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)")
|
||||
|
@ -996,7 +998,7 @@ class OOTWorld(World):
|
|||
'Buy Goron Tunic': 1,
|
||||
'Buy Zora Tunic': 1,
|
||||
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
|
||||
self.multiworld.random.shuffle(shop_locations)
|
||||
self.random.shuffle(shop_locations)
|
||||
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)
|
||||
|
@ -1028,7 +1030,7 @@ class OOTWorld(World):
|
|||
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))
|
||||
junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill))
|
||||
exclusion_rules(self.multiworld, self.player, junk_fill_locations)
|
||||
|
||||
# Locations which are not sendable must be converted to events
|
||||
|
@ -1074,13 +1076,13 @@ class OOTWorld(World):
|
|||
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
|
||||
self.trap_appearances = {}
|
||||
for loc_id in trap_location_ids:
|
||||
self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name)
|
||||
self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name)
|
||||
|
||||
# Seed hint RNG, used for ganon text lines also
|
||||
self.hint_rng = self.multiworld.per_slot_randoms[self.player]
|
||||
self.hint_rng = self.random
|
||||
|
||||
outfile_name = self.multiworld.get_out_file_name_base(self.player)
|
||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
||||
rom = Rom(file=get_settings()['oot_options']['rom_file'])
|
||||
try:
|
||||
if self.hints != 'none':
|
||||
buildWorldGossipHints(self)
|
||||
|
@ -1092,7 +1094,7 @@ class OOTWorld(World):
|
|||
finally:
|
||||
self.collectible_flags_available.set()
|
||||
rom.update_header()
|
||||
patch_data = create_patch_file(rom)
|
||||
patch_data = create_patch_file(rom, self.random)
|
||||
rom.restore()
|
||||
|
||||
apz5 = OoTContainer(patch_data, outfile_name, output_directory,
|
||||
|
@ -1399,7 +1401,7 @@ class OOTWorld(World):
|
|||
return all_state
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return get_junk_item(count=1, pool=get_junk_pool(self))[0]
|
||||
return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0]
|
||||
|
||||
|
||||
def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool:
|
||||
|
|
Loading…
Reference in New Issue