Merge pull request #141 from espeon65536/oot

Ocarina of Time updates
This commit is contained in:
Fabian Dill
2021-11-25 17:57:31 +00:00
committed by GitHub
20 changed files with 9647 additions and 9061 deletions

View File

@@ -1,6 +1,6 @@
class Dungeon(object):
def __init__(self, world, name, hint, boss_key, small_keys, dungeon_items):
def __init__(self, world, name, hint, font_color, boss_key, small_keys, dungeon_items):
def to_array(obj):
if obj == None:
return []
@@ -12,6 +12,7 @@ class Dungeon(object):
self.world = world
self.name = name
self.hint_text = hint
self.font_color = font_color
self.regions = []
self.boss_key = to_array(boss_key)
self.small_keys = to_array(small_keys)
@@ -28,7 +29,7 @@ class Dungeon(object):
new_small_keys = [item.copy(new_world) for item in self.small_keys]
new_dungeon_items = [item.copy(new_world) for item in self.dungeon_items]
new_dungeon = Dungeon(new_world, self.name, self.hint, new_boss_key, new_small_keys, new_dungeon_items)
new_dungeon = Dungeon(new_world, self.name, self.hint_text, self.font_color, new_boss_key, new_small_keys, new_dungeon_items)
return new_dungeon

View File

@@ -7,6 +7,8 @@ from .Utils import data_path
dungeon_table = [
{
'name': 'Deku Tree',
'hint': 'the Deku Tree',
'font_color': 'Green',
'boss_key': 0,
'small_key': 0,
'small_key_mq': 0,
@@ -15,6 +17,7 @@ dungeon_table = [
{
'name': 'Dodongos Cavern',
'hint': 'Dodongo\'s Cavern',
'font_color': 'Red',
'boss_key': 0,
'small_key': 0,
'small_key_mq': 0,
@@ -23,6 +26,7 @@ dungeon_table = [
{
'name': 'Jabu Jabus Belly',
'hint': 'Jabu Jabu\'s Belly',
'font_color': 'Blue',
'boss_key': 0,
'small_key': 0,
'small_key_mq': 0,
@@ -30,6 +34,8 @@ dungeon_table = [
},
{
'name': 'Forest Temple',
'hint': 'the Forest Temple',
'font_color': 'Green',
'boss_key': 1,
'small_key': 5,
'small_key_mq': 6,
@@ -37,6 +43,8 @@ dungeon_table = [
},
{
'name': 'Bottom of the Well',
'hint': 'the Bottom of the Well',
'font_color': 'Pink',
'boss_key': 0,
'small_key': 3,
'small_key_mq': 2,
@@ -44,6 +52,8 @@ dungeon_table = [
},
{
'name': 'Fire Temple',
'hint': 'the Fire Temple',
'font_color': 'Red',
'boss_key': 1,
'small_key': 8,
'small_key_mq': 5,
@@ -51,6 +61,8 @@ dungeon_table = [
},
{
'name': 'Ice Cavern',
'hint': 'the Ice Cavern',
'font_color': 'Blue',
'boss_key': 0,
'small_key': 0,
'small_key_mq': 0,
@@ -58,6 +70,8 @@ dungeon_table = [
},
{
'name': 'Water Temple',
'hint': 'the Water Temple',
'font_color': 'Blue',
'boss_key': 1,
'small_key': 6,
'small_key_mq': 2,
@@ -65,6 +79,8 @@ dungeon_table = [
},
{
'name': 'Shadow Temple',
'hint': 'the Shadow Temple',
'font_color': 'Pink',
'boss_key': 1,
'small_key': 5,
'small_key_mq': 6,
@@ -72,6 +88,8 @@ dungeon_table = [
},
{
'name': 'Gerudo Training Grounds',
'hint': 'the Gerudo Training Grounds',
'font_color': 'Yellow',
'boss_key': 0,
'small_key': 9,
'small_key_mq': 3,
@@ -79,6 +97,8 @@ dungeon_table = [
},
{
'name': 'Spirit Temple',
'hint': 'the Spirit Temple',
'font_color': 'Yellow',
'boss_key': 1,
'small_key': 5,
'small_key_mq': 7,
@@ -100,6 +120,7 @@ def create_dungeons(ootworld):
for dungeon_info in dungeon_table:
name = dungeon_info['name']
hint = dungeon_info['hint'] if 'hint' in dungeon_info else name
font_color = dungeon_info['font_color'] if 'font_color' in dungeon_info else 'White'
if ootworld.logic_rules == 'glitchless':
if not ootworld.dungeon_mq[name]:
@@ -125,5 +146,5 @@ def create_dungeons(ootworld):
for item in dungeon_items:
item.priority = True
ootworld.dungeons.append(Dungeon(ootworld, name, hint, boss_keys, small_keys, dungeon_items))
ootworld.dungeons.append(Dungeon(ootworld, name, hint, font_color, boss_keys, small_keys, dungeon_items))

View File

@@ -1,7 +1,7 @@
from itertools import chain
import logging
from worlds.generic.Rules import set_rule
from worlds.generic.Rules import set_rule, add_rule
from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
@@ -29,12 +29,13 @@ def assume_entrance_pool(entrance_pool, ootworld):
assumed_pool = []
for entrance in entrance_pool:
assumed_forward = entrance.assume_reachable()
if entrance.reverse != None:
if entrance.reverse != None and not ootworld.decouple_entrances:
assumed_return = entrance.reverse.assume_reachable()
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
# In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region
set_rule(assumed_return, lambda state, **kwargs: False)
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
# In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region
set_rule(assumed_return, lambda state, **kwargs: False)
assumed_forward.bind_two_way(assumed_return)
assumed_pool.append(assumed_forward)
return assumed_pool
@@ -308,6 +309,8 @@ entrance_shuffle_table = [
('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }),
('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })),
('Overworld', ('GV Lower Stream -> Lake Hylia', { 'index': 0x0219 })),
('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })),
('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })),
@@ -376,15 +379,24 @@ def shuffle_random_entrances(ootworld):
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
if ootworld.open_forest == 'closed':
entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
if ootworld.decouple_entrances:
entrance_pools['DungeonReverse'] = [entrance.reverse for entrance in entrance_pools['Dungeon']]
if ootworld.shuffle_interior_entrances != 'off':
entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True)
if ootworld.shuffle_special_interior_entrances:
entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True)
if ootworld.decouple_entrances:
entrance_pools['InteriorReverse'] = [entrance.reverse for entrance in entrance_pools['Interior']]
if ootworld.shuffle_grotto_entrances:
entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True)
entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True)
if ootworld.decouple_entrances:
entrance_pools['GrottoGraveReverse'] = [entrance.reverse for entrance in entrance_pools['GrottoGrave']]
if ootworld.shuffle_overworld_entrances:
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld')
exclude_overworld_reverse = ootworld.mix_entrance_pools == 'all' and not ootworld.decouple_entrances
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld', only_primary=exclude_overworld_reverse)
if not ootworld.decouple_entrances:
entrance_pools['Overworld'].remove(world.get_entrance('GV Lower Stream -> Lake Hylia', player))
# Mark shuffled entrances
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
@@ -392,6 +404,16 @@ def shuffle_random_entrances(ootworld):
if entrance.reverse:
entrance.reverse.shuffled = True
# Combine all entrance pools if mixing
if ootworld.mix_entrance_pools == 'all':
entrance_pools = {'Mixed': list(chain.from_iterable(entrance_pools.values()))}
elif ootworld.mix_entrance_pools == 'indoor':
if ootworld.shuffle_overworld_entrances:
ow_pool = entrance_pools['Overworld']
entrance_pools = {'Mixed': list(filter(lambda entrance: entrance.type != 'Overworld', chain.from_iterable(entrance_pools.values())))}
if ootworld.shuffle_overworld_entrances:
entrance_pools['Overworld'] = ow_pool
# Build target entrance pools
one_way_target_entrance_pools = {}
for pool_type, entrance_pool in one_way_entrance_pools.items():
@@ -403,7 +425,9 @@ def shuffle_random_entrances(ootworld):
elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable?
# Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
# Disconnect one-way entrances for priority placement
for entrance in chain.from_iterable(one_way_entrance_pools.values()):
entrance.disconnect()
@@ -419,7 +443,52 @@ def shuffle_random_entrances(ootworld):
if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0
# Plando entrances?
# Plando entrances
if world.plando_connections[player]:
rollbacks = []
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
for conn in world.plando_connections[player]:
try:
entrance = ootworld.get_entrance(conn.entrance)
exit = ootworld.get_entrance(conn.exit)
if entrance is None:
raise EntranceShuffleError(f"Could not find entrance to plando: {conn.entrance}")
if exit is None:
raise EntranceShuffleError(f"Could not find entrance to plando: {conn.exit}")
target_region = exit.name.split(' -> ')[1]
target_parent = exit.parent_region.name
pool_type = entrance.type
matched_targets_to_region = list(filter(lambda target: target.connected_region and target.connected_region.name == target_region,
all_targets[pool_type]))
target = next(filter(lambda target: target.replaces.parent_region.name == target_parent, matched_targets_to_region))
replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state)
if conn.direction == 'both' and entrance.reverse and ootworld.decouple_entrances:
replace_entrance(ootworld, entrance.reverse, target.reverse, rollbacks, locations_to_ensure_reachable, all_state, none_state)
except EntranceShuffleError as e:
raise RuntimeError(f"Failed to plando OoT entrances. Reason: {e}")
except StopIteration:
raise RuntimeError(f"Could not find entrance to plando: {conn.entrance} => {conn.exit}")
finally:
for (entrance, target) in rollbacks:
confirm_replacement(entrance, target)
# Check placed one way entrances and trim.
# The placed entrances are already pointing at their new regions.
placed_entrances = [entrance for entrance in chain.from_iterable(one_way_entrance_pools.values())
if entrance.replaces is not None]
replaced_entrances = [entrance.replaces for entrance in placed_entrances]
# Remove replaced entrances so we don't place two in one target.
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
if remaining_target.replaces and remaining_target.replaces in replaced_entrances:
delete_target_entrance(remaining_target)
# Remove priority targets if any placed entrances point at their region(s).
for key, (regions, _) in priority_entrance_table.items():
if key in one_way_priorities:
for entrance in placed_entrances:
if entrance.connected_region and entrance.connected_region.name in regions:
del one_way_priorities[key]
break
# Place priority entrances
shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools, locations_to_ensure_reachable, all_state, none_state, retry_count=2)
@@ -619,24 +688,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
time_travel_state.collect(ootworld.create_item('Time Travel'), event=True)
time_travel_state._oot_update_age_reachable_regions(player)
# For various reasons, we don't want the player to end up through certain entrances as the wrong age
# Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age
# This means we need to hard check that none of the relevant entrances are ever reachable as that age
# This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop)
# Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well
CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side']
ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds']
for entrance in ootworld.get_shufflable_entrances():
if entrance.shuffled and entrance.replaces:
if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access')
if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access')
else:
if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as child')
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
if not ootworld.decouple_entrances:
for entrance in ootworld.get_shufflable_entrances():
if entrance.shuffled and entrance.replaces:
if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access')
if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access')
else:
if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as child')
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
# Check if all locations are reachable if not beatable-only or game is not yet complete
if locations_to_ensure_reachable:
@@ -645,7 +715,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')
if ootworld.shuffle_interior_entrances and (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
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_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
@@ -733,14 +804,14 @@ def get_entrance_replacing(region, entrance_name, player):
def change_connections(entrance, target):
entrance.connect(target.disconnect())
entrance.replaces = target.replaces
if entrance.reverse:
if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
target.replaces.reverse.connect(entrance.reverse.assumed.disconnect())
target.replaces.reverse.replaces = entrance.reverse
def restore_connections(entrance, target):
target.connect(entrance.disconnect())
entrance.replaces = None
if entrance.reverse:
if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
entrance.reverse.assumed.connect(target.replaces.reverse.disconnect())
target.replaces.reverse.replaces = None
@@ -757,7 +828,7 @@ def check_entrances_compatibility(entrance, target, rollbacks):
def confirm_replacement(entrance, target):
delete_target_entrance(target)
logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}')
if entrance.reverse:
if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
replaced_reverse = target.replaces.reverse
delete_target_entrance(entrance.reverse.assumed)
logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}')

View File

@@ -11,7 +11,7 @@ import json
from enum import Enum
from .HintList import getHint, getHintGroup, Hint, hintExclusions
from .Messages import update_message_by_id
from .Messages import COLOR_MAP, update_message_by_id
from .TextBox import line_wrap
from .Utils import data_path, read_json
@@ -266,17 +266,6 @@ def getSimpleHintNoPrefix(item):
def colorText(gossip_text):
colorMap = {
'White': '\x40',
'Red': '\x41',
'Green': '\x42',
'Blue': '\x43',
'Light Blue': '\x44',
'Pink': '\x45',
'Yellow': '\x46',
'Black': '\x47',
}
text = gossip_text.text
colors = list(gossip_text.colors) if gossip_text.colors is not None else []
color = 'White'
@@ -292,7 +281,7 @@ def colorText(gossip_text):
splitText[1] = splitText[1][len(prefix):]
break
splitText[1] = '\x05' + colorMap[color] + splitText[1] + '\x05\x40'
splitText[1] = '\x05' + COLOR_MAP[color] + splitText[1] + '\x05\x40'
text = ''.join(splitText)
return text
@@ -649,9 +638,9 @@ def buildWorldGossipHints(world, checkedLocations=None):
if checkedLocations is None:
checkedLocations = {player: set() for player in world.world.player_ids}
# If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting
# If Ganondorf hints Light Arrows and is reachable without them, add to checkedLocations to prevent extra hinting
# Can only be forced with vanilla bridge or trials
if world.bridge != 'vanilla' and world.trials == 0:
if world.bridge != 'vanilla' and world.trials == 0 and world.misc_hints:
try:
light_arrow_location = world.world.find_item("Light Arrows", world.player)
checkedLocations[light_arrow_location.player].add(light_arrow_location.name)

View File

@@ -1329,9 +1329,10 @@ def get_pool_core(world):
# We can resolve this by starting with some extra keys
if world.dungeon_mq['Spirit Temple']:
# Yes somehow you need 3 keys. This dungeon is bonkers
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
items = [world.create_item('Small Key (Spirit Temple)') for i in range(3)]
for item in items:
world.world.push_precollected(item)
world.remove_from_start_inventory.append(item.name)
#if not world.dungeon_mq['Fire Temple']:
# world.state.collect(ItemFactory('Small Key (Fire Temple)'))
if world.shuffle_bosskeys == 'vanilla':

View File

@@ -1,5 +1,6 @@
# text details: https://wiki.cloudmodding.com/oot/Text_Format
import logging
import random
from .TextBox import line_wrap
@@ -316,6 +317,17 @@ KEYSANITY_MESSAGES = {
0x00A9: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
}
COLOR_MAP = {
'White': '\x40',
'Red': '\x41',
'Green': '\x42',
'Blue': '\x43',
'Light Blue': '\x44',
'Pink': '\x45',
'Yellow': '\x46',
'Black': '\x47',
}
MISC_MESSAGES = {
0x507B: (bytearray(
b"\x08I tell you, I saw him!\x04" \
@@ -995,3 +1007,30 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
]))
return permutation
# Update warp song text boxes for ER
def update_warp_song_text(messages, ootworld):
msg_list = {
0x088D: 'Minuet of Forest Warp -> Sacred Forest Meadow',
0x088E: 'Bolero of Fire Warp -> DMC Central Local',
0x088F: 'Serenade of Water Warp -> Lake Hylia',
0x0890: 'Requiem of Spirit Warp -> Desert Colossus',
0x0891: 'Nocturne of Shadow Warp -> Graveyard Warp Pad Region',
0x0892: 'Prelude of Light Warp -> Temple of Time',
}
for id, entr in msg_list.items():
destination = ootworld.world.get_entrance(entr, ootworld.player).connected_region
if destination.pretty_name:
destination_name = destination.pretty_name
elif destination.hint_text:
destination_name = destination.hint_text
elif destination.dungeon:
destination_name = destination.dungeon.hint
else:
destination_name = destination.name
color = COLOR_MAP[destination.font_color or 'White']
new_msg = f"\x08\x05{color}Warp to {destination_name}?\x05\40\x09\x01\x01\x1b\x05{color}OK\x01No\x05\40"
update_message_by_id(messages, id, new_msg)

View File

@@ -96,6 +96,7 @@ class StartingAge(Choice):
class InteriorEntrances(Choice):
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop."""
displayname = "Shuffle Interior Entrances"
option_off = 0
option_simple = 1
option_all = 2
@@ -105,26 +106,46 @@ class InteriorEntrances(Choice):
class GrottoEntrances(Toggle):
"""Shuffles grotto and grave entrances."""
displayname = "Shuffle Grotto/Grave Entrances"
class DungeonEntrances(Toggle):
"""Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages."""
displayname = "Shuffle Dungeon Entrances"
class OverworldEntrances(Toggle):
"""Shuffles overworld loading zones."""
displayname = "Shuffle Overworld Entrances"
class OwlDrops(Toggle):
"""Randomizes owl drops from Lake Hylia or Death Mountain Trail as child."""
displayname = "Randomize Owl Drops"
class WarpSongs(Toggle):
"""Randomizes warp song destinations."""
displayname = "Randomize Warp Songs"
class SpawnPositions(Toggle):
"""Randomizes the starting position on loading a save. Consistent between savewarps."""
displayname = "Randomize Spawn Positions"
class MixEntrancePools(Choice):
"""Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" mixes them in."""
displayname = "Mix Entrance Pools"
option_off = 0
option_indoor = 1
option_all = 2
alias_false = 0
class DecoupleEntrances(Toggle):
"""Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if overworld is shuffled."""
displayname = "Decouple Entrances"
class TriforceHunt(Toggle):
@@ -170,6 +191,8 @@ world_options: typing.Dict[str, type(Option)] = {
"owl_drops": OwlDrops,
"warp_songs": WarpSongs,
"spawn_positions": SpawnPositions,
"mix_entrance_pools": MixEntrancePools,
"decouple_entrances": DecoupleEntrances,
"triforce_hunt": TriforceHunt,
"triforce_goal": TriforceGoal,
"extra_triforce_percentage": ExtraTriforces,
@@ -540,6 +563,11 @@ class Hints(Choice):
alias_false = 0
class MiscHints(DefaultOnToggle):
"""Controls whether the Temple of Time altar gives dungeon prize info and whether Ganondorf hints the Light Arrows."""
displayname = "Misc Hints"
class HintDistribution(Choice):
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
displayname = "Hint Distribution"
@@ -607,6 +635,7 @@ class RupeeStart(Toggle):
misc_options: typing.Dict[str, type(Option)] = {
"correct_chest_sizes": CSMC,
"hints": Hints,
"misc_hints": MiscHints,
"hint_dist": HintDistribution,
"text_shuffle": TextShuffle,
"damage_multiplier": DamageMultiplier,

View File

@@ -9,7 +9,7 @@ from .LocationList import business_scrubs
from .Hints import writeGossipStoneHints, buildAltarHints, \
buildGanonText, getSimpleHintNoPrefix
from .Utils import data_path
from .Messages import read_messages, update_message_by_id, read_shop_items, \
from .Messages import read_messages, update_message_by_id, read_shop_items, update_warp_song_text, \
write_shop_items, remove_unused_messages, make_player_message, \
add_item_messages, repack_messages, shuffle_messages, \
get_message_by_id
@@ -1007,6 +1007,12 @@ def patch_rom(world, rom):
# Archipelago forces this item to be local so it can always be given to the player. Usually it's a song so it's no problem.
item = world.get_location('Song from Impa').item
save_context.give_raw_item(item.name)
if item.name == 'Slingshot':
save_context.give_raw_item("Deku Seeds (30)")
elif item.name == 'Bow':
save_context.give_raw_item("Arrows (30)")
elif item.name == 'Bomb Bag':
save_context.give_raw_item("Bombs (20)")
save_context.write_bits(0x0ED7, 0x04) # "Obtained Malon's Item"
save_context.write_bits(0x0ED7, 0x08) # "Woke Talon in castle"
save_context.write_bits(0x0ED7, 0x10) # "Talon has fled castle"
@@ -1634,7 +1640,7 @@ def patch_rom(world, rom):
rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
else:
if location.item.advancement:
if not location.item.advancement:
rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@@ -1650,7 +1656,7 @@ def patch_rom(world, rom):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
else:
if location.item.advancement:
if not location.item.advancement:
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
@@ -1741,6 +1747,10 @@ def patch_rom(world, rom):
elif world.text_shuffle == 'complete':
permutation = shuffle_messages(messages, except_hints=False)
# If Warp Song ER is on, update text boxes
if world.warp_songs:
update_warp_song_text(messages, world)
repack_messages(rom, messages, permutation)
# output a text dump, for testing...

View File

@@ -38,6 +38,8 @@ class OOTRegion(Region):
self.provides_time = TimeOfDay.NONE
self.scene = None
self.dungeon = None
self.pretty_name = None
self.font_color = None
def get_scene(self):
if self.scene:

View File

@@ -17,7 +17,7 @@ double_cache_prevention = threading.Lock()
class Rom(BigStream):
original = None
def __init__(self, file=None):
def __init__(self, file=None, force_use=False):
super().__init__([])
self.changed_address = {}
@@ -34,22 +34,25 @@ class Rom(BigStream):
self.symbols = {name: int(addr, 16) for name, addr in symbols.items()}
# If decompressed file already exists, read from it
if os.path.exists(decomp_file):
file = decomp_file
if not force_use:
if os.path.exists(decomp_file):
file = decomp_file
if file == '':
# if not specified, try to read from the previously decompressed rom
file = decomp_file
try:
if file == '':
# if not specified, try to read from the previously decompressed rom
file = decomp_file
try:
self.read_rom(file)
except FileNotFoundError:
# could not find the decompressed rom either
raise FileNotFoundError('Must specify path to base ROM')
else:
self.read_rom(file)
except FileNotFoundError:
# could not find the decompressed rom either
raise FileNotFoundError('Must specify path to base ROM')
else:
self.read_rom(file)
# decompress rom, or check if it's already decompressed
self.decompress_rom_file(file, decomp_file)
self.decompress_rom_file(file, decomp_file, force_use)
# Add file to maximum size
self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
@@ -69,7 +72,7 @@ class Rom(BigStream):
new_rom.force_patch = copy.copy(self.force_patch)
return new_rom
def decompress_rom_file(self, file, decomp_file):
def decompress_rom_file(self, file, decomp_file, skip_crc_check):
validCRC = [
[0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
[0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed
@@ -79,7 +82,7 @@ class Rom(BigStream):
# Validate ROM file
file_name = os.path.splitext(file)
romCRC = list(self.buffer[0x10:0x18])
if romCRC not in validCRC:
if romCRC not in validCRC and not skip_crc_check:
# Bad CRC validation
raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
elif len(self.buffer) < 0x2000000 or len(self.buffer) > (0x4000000) or file_name[1].lower() not in ['.z64',

View File

@@ -4,7 +4,7 @@ import subprocess
import Utils
from functools import lru_cache
__version__ = Utils.__version__ + ' f.LUM'
__version__ = '6.1.0 f.LUM'
def data_path(*args):

View File

@@ -191,7 +191,6 @@ class OOTWorld(World):
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
# Hint stuff
self.misc_hints = True # this is just always on
self.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text
self.gossip_hints = {}
self.required_locations = []
@@ -276,6 +275,10 @@ class OOTWorld(World):
for region in region_json:
new_region = OOTRegion(region['region_name'], RegionType.Generic, None, self.player)
new_region.world = self.world
if 'pretty_name' in region:
new_region.pretty_name = region['pretty_name']
if 'font_color' in region:
new_region.font_color = region['font_color']
if 'scene' in region:
new_region.scene = region['scene']
if 'hint' in region:
@@ -513,20 +516,6 @@ class OOTWorld(World):
else:
break
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
set_rules(self)
set_entrances_based_rules(self)
@@ -790,6 +779,24 @@ class OOTWorld(World):
create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5'))
rom.restore()
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
if not self.decouple_entrances:
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
else:
for entrance in all_entrances:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod
def stage_generate_output(cls, world: MultiWorld, output_directory: str):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"AUDIO_THREAD_INFO": "03482FC0",
"AUDIO_THREAD_INFO_MEM_SIZE": "03482FDC",
"AUDIO_THREAD_INFO_MEM_START": "03482FD8",
"AUDIO_THREAD_MEM_START": "0348EF50",
"AUDIO_THREAD_MEM_START": "0348EF80",
"BOMBCHUS_IN_LOGIC": "03480CBC",
"CFG_A_BUTTON_COLOR": "03480854",
"CFG_A_NOTE_COLOR": "03480872",
@@ -38,7 +38,7 @@
"CFG_TEXT_CURSOR_COLOR": "03480866",
"CHAIN_HBA_REWARDS": "03483950",
"CHEST_SIZE_MATCH_CONTENTS": "034826F0",
"COMPLETE_MASK_QUEST": "0348B1D1",
"COMPLETE_MASK_QUEST": "0348B201",
"COOP_CONTEXT": "03480020",
"COOP_VERSION": "03480020",
"COSMETIC_CONTEXT": "03480844",
@@ -47,13 +47,13 @@
"DEATH_LINK": "0348002A",
"DEBUG_OFFSET": "034828A0",
"DISABLE_TIMERS": "03480CDC",
"DPAD_TEXTURE": "0348D750",
"DPAD_TEXTURE": "0348D780",
"DUNGEONS_SHUFFLED": "03480CDE",
"EXTENDED_OBJECT_TABLE": "03480C9C",
"EXTERN_DAMAGE_MULTIPLYER": "03482CB1",
"FAST_BUNNY_HOOD_ENABLED": "03480CE0",
"FAST_CHESTS": "03480CD6",
"FONT_TEXTURE": "0348C288",
"FONT_TEXTURE": "0348C2B8",
"FREE_SCARECROW_ENABLED": "03480CCC",
"GET_CHEST_OVERRIDE_COLOR_WRAPPER": "03482720",
"GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826F4",
@@ -69,17 +69,17 @@
"LACS_CONDITION_COUNT": "03480CD2",
"MALON_GAVE_ICETRAP": "0348368C",
"MALON_TEXT_ID": "03480CDB",
"MAX_RUPEES": "0348B1D3",
"MAX_RUPEES": "0348B203",
"MOVED_ADULT_KING_ZORA": "03482FFC",
"NO_ESCAPE_SEQUENCE": "0348B19C",
"NO_ESCAPE_SEQUENCE": "0348B1CC",
"NO_FOG_STATE": "03480CDD",
"OCARINAS_SHUFFLED": "03480CD5",
"OPEN_KAKARIKO": "0348B1D2",
"OPEN_KAKARIKO": "0348B202",
"OUTGOING_ITEM": "03480030",
"OUTGOING_KEY": "0348002C",
"OUTGOING_PLAYER": "03480032",
"OVERWORLD_SHUFFLED": "03480CDF",
"PAYLOAD_END": "0348EF50",
"PAYLOAD_END": "0348EF80",
"PAYLOAD_START": "03480000",
"PLAYED_WARP_SONG": "03481210",
"PLAYER_ID": "03480024",
@@ -97,88 +97,88 @@
"SPEED_MULTIPLIER": "03482760",
"START_TWINROVA_FIGHT": "0348307C",
"TIME_TRAVEL_SAVED_EQUIPS": "03481A64",
"TRIFORCE_ICON_TEXTURE": "0348DF50",
"TRIFORCE_ICON_TEXTURE": "0348DF80",
"TWINROVA_ACTION_TIMER": "03483080",
"WINDMILL_SONG_ID": "03480CD9",
"WINDMILL_TEXT_ID": "03480CDA",
"a_button": "0348B160",
"a_note_b": "0348B14C",
"a_note_font_glow_base": "0348B134",
"a_note_font_glow_max": "0348B130",
"a_note_g": "0348B150",
"a_note_glow_base": "0348B13C",
"a_note_glow_max": "0348B138",
"a_note_r": "0348B154",
"active_item_action_id": "0348B1B4",
"active_item_fast_chest": "0348B1A4",
"active_item_graphic_id": "0348B1A8",
"active_item_object_id": "0348B1AC",
"active_item_row": "0348B1B8",
"active_item_text_id": "0348B1B0",
"active_override": "0348B1C0",
"active_override_is_outgoing": "0348B1BC",
"b_button": "0348B15C",
"beating_dd": "0348B168",
"beating_no_dd": "0348B170",
"c_button": "0348B158",
"c_note_b": "0348B140",
"c_note_font_glow_base": "0348B124",
"c_note_font_glow_max": "0348B120",
"c_note_g": "0348B144",
"c_note_glow_base": "0348B12C",
"c_note_glow_max": "0348B128",
"c_note_r": "0348B148",
"cfg_dungeon_info_enable": "0348B0EC",
"cfg_dungeon_info_mq_enable": "0348B190",
"cfg_dungeon_info_mq_need_map": "0348B18C",
"cfg_dungeon_info_reward_enable": "0348B0E8",
"cfg_dungeon_info_reward_need_altar": "0348B184",
"cfg_dungeon_info_reward_need_compass": "0348B188",
"cfg_dungeon_is_mq": "0348B1F0",
"cfg_dungeon_rewards": "03489EE4",
"cfg_file_select_hash": "0348B198",
"cfg_item_overrides": "0348B244",
"defaultDDHeart": "0348B174",
"defaultHeart": "0348B17C",
"dpad_sprite": "0348A058",
"dummy_actor": "0348B1C8",
"dungeon_count": "0348B0F0",
"dungeons": "03489F08",
"empty_dlist": "0348B108",
"extern_ctxt": "03489FA4",
"font_sprite": "0348A068",
"freecam_modes": "03489C60",
"hash_sprites": "0348B0FC",
"hash_symbols": "03489FB8",
"heap_next": "0348B1EC",
"heart_sprite": "03489FF8",
"icon_sprites": "03489E24",
"item_digit_sprite": "0348A018",
"item_overrides_count": "0348B1CC",
"item_table": "0348A0E0",
"items_sprite": "0348A088",
"key_rupee_clock_sprite": "0348A028",
"last_fog_distance": "0348B0F4",
"linkhead_skull_sprite": "0348A008",
"medal_colors": "03489EF4",
"medals_sprite": "0348A098",
"normal_dd": "0348B164",
"normal_no_dd": "0348B16C",
"object_slots": "0348C244",
"pending_freezes": "0348B1D0",
"pending_item_queue": "0348B22C",
"quest_items_sprite": "0348A078",
"rupee_colors": "03489E30",
"satisified_pending_frames": "0348B1A0",
"scene_fog_distance": "0348B0F8",
"setup_db": "0348A0B8",
"song_note_sprite": "0348A038",
"stones_sprite": "0348A0A8",
"text_cursor_border_base": "0348B114",
"text_cursor_border_max": "0348B110",
"text_cursor_inner_base": "0348B11C",
"text_cursor_inner_max": "0348B118",
"triforce_hunt_enabled": "0348B1E0",
"triforce_pieces_requied": "0348B182",
"triforce_sprite": "0348A048"
"a_button": "0348B190",
"a_note_b": "0348B17C",
"a_note_font_glow_base": "0348B164",
"a_note_font_glow_max": "0348B160",
"a_note_g": "0348B180",
"a_note_glow_base": "0348B16C",
"a_note_glow_max": "0348B168",
"a_note_r": "0348B184",
"active_item_action_id": "0348B1E4",
"active_item_fast_chest": "0348B1D4",
"active_item_graphic_id": "0348B1D8",
"active_item_object_id": "0348B1DC",
"active_item_row": "0348B1E8",
"active_item_text_id": "0348B1E0",
"active_override": "0348B1F0",
"active_override_is_outgoing": "0348B1EC",
"b_button": "0348B18C",
"beating_dd": "0348B198",
"beating_no_dd": "0348B1A0",
"c_button": "0348B188",
"c_note_b": "0348B170",
"c_note_font_glow_base": "0348B154",
"c_note_font_glow_max": "0348B150",
"c_note_g": "0348B174",
"c_note_glow_base": "0348B15C",
"c_note_glow_max": "0348B158",
"c_note_r": "0348B178",
"cfg_dungeon_info_enable": "0348B11C",
"cfg_dungeon_info_mq_enable": "0348B1C0",
"cfg_dungeon_info_mq_need_map": "0348B1BC",
"cfg_dungeon_info_reward_enable": "0348B118",
"cfg_dungeon_info_reward_need_altar": "0348B1B4",
"cfg_dungeon_info_reward_need_compass": "0348B1B8",
"cfg_dungeon_is_mq": "0348B220",
"cfg_dungeon_rewards": "03489F14",
"cfg_file_select_hash": "0348B1C8",
"cfg_item_overrides": "0348B274",
"defaultDDHeart": "0348B1A4",
"defaultHeart": "0348B1AC",
"dpad_sprite": "0348A088",
"dummy_actor": "0348B1F8",
"dungeon_count": "0348B120",
"dungeons": "03489F38",
"empty_dlist": "0348B138",
"extern_ctxt": "03489FD4",
"font_sprite": "0348A098",
"freecam_modes": "03489C90",
"hash_sprites": "0348B12C",
"hash_symbols": "03489FE8",
"heap_next": "0348B21C",
"heart_sprite": "0348A028",
"icon_sprites": "03489E54",
"item_digit_sprite": "0348A048",
"item_overrides_count": "0348B1FC",
"item_table": "0348A110",
"items_sprite": "0348A0B8",
"key_rupee_clock_sprite": "0348A058",
"last_fog_distance": "0348B124",
"linkhead_skull_sprite": "0348A038",
"medal_colors": "03489F24",
"medals_sprite": "0348A0C8",
"normal_dd": "0348B194",
"normal_no_dd": "0348B19C",
"object_slots": "0348C274",
"pending_freezes": "0348B200",
"pending_item_queue": "0348B25C",
"quest_items_sprite": "0348A0A8",
"rupee_colors": "03489E60",
"satisified_pending_frames": "0348B1D0",
"scene_fog_distance": "0348B128",
"setup_db": "0348A0E8",
"song_note_sprite": "0348A068",
"stones_sprite": "0348A0D8",
"text_cursor_border_base": "0348B144",
"text_cursor_border_max": "0348B140",
"text_cursor_inner_base": "0348B14C",
"text_cursor_inner_max": "0348B148",
"triforce_hunt_enabled": "0348B210",
"triforce_pieces_requied": "0348B1B2",
"triforce_sprite": "0348A078"
}