Skip to content

Boss room shuffle #1468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions EntranceShuffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,72 @@ def build_one_way_targets(world, types_to_include, exclude=(), target_region_nam
('Extra', ('ZR Top of Waterfall -> Zora River', { 'index': 0x0199 })),
]

def _add_boss_entrances():
# Compute this at load time to save a lot of duplication
dungeon_data = {}
for type, forward, *reverse in entrance_shuffle_table:
if type != 'Dungeon':
continue
if not reverse:
continue
name, forward = forward
reverse = reverse[0][1]
if 'blue_warp' not in reverse:
continue
dungeon_data[name] = {
'dungeon_index': forward['index'],
'exit_index': reverse['index'],
'exit_blue_warp': reverse['blue_warp']
}

for type, source, target, boss, dungeon, index, rindex, addresses in [
(
'ChildBoss', 'Deku Tree Boss Door', 'Gohma Boss Room', 'Queen Gohma',
'KF Outside Deku Tree -> Deku Tree Lobby',
0x040f, 0x0252, [ 0xB06292, 0xBC6162, 0xBC60AE ]
),
(
'ChildBoss', 'Dodongos Cavern Boss Door', 'King Dodongo Boss Room', 'King Dodongo',
'Death Mountain -> Dodongos Cavern Beginning',
0x040b, 0x00c5, [ 0xB062B6, 0xBC616E ]
),
(
'ChildBoss', 'Jabu Jabus Belly Boss Door', 'Barinade Boss Room', 'Barinade',
'Zoras Fountain -> Jabu Jabus Belly Beginning',
0x0301, 0x0407, [ 0xB062C2, 0xBC60C2 ]
),
(
'AdultBoss', 'Forest Temple Boss Door', 'Phantom Ganon Boss Room', 'Phantom Ganon',
'SFM Forest Temple Entrance Ledge -> Forest Temple Lobby',
0x000c, 0x024E, [ 0xB062CE, 0xBC6182 ]
),
(
'AdultBoss', 'Fire Temple Boss Door', 'Volvagia Boss Room', 'Volvagia',
'DMC Fire Temple Entrance -> Fire Temple Lower',
0x0305, 0x0175, [ 0xB062DA, 0xBC60CE ]
),
(
'AdultBoss', 'Water Temple Boss Door', 'Morpha Boss Room', 'Morpha',
'Lake Hylia -> Water Temple Lobby',
0x0417, 0x0423, [ 0xB062E6, 0xBC6196 ]
),
(
'AdultBoss', 'Spirit Temple Boss Door', 'Twinrova Boss Room', 'Twinrova',
'Desert Colossus -> Spirit Temple Lobby',
0x008D, 0x02F5, [ 0xB062F2, 0xBC6122 ]
),
(
'AdultBoss', 'Shadow Temple Boss Door', 'Bongo Bongo Boss Room', 'Bongo Bongo',
'Graveyard Warp Pad Region -> Shadow Temple Entryway',
0x0413, 0x02B2, [ 0xB062FE, 0xBC61AA ]
)
]:
d = {'index': index, 'patch_addresses': addresses, 'boss': boss}
d.update(dungeon_data[dungeon])
entrance_shuffle_table.append(
(type, (f"{source} -> {target}", d), (f"{target} -> {source}", {'index': rindex}))
)
_add_boss_entrances()

# Basically, the entrances in the list above that go to:
# - DMC Central Local (child access for the bean and skull)
Expand Down Expand Up @@ -396,6 +462,13 @@ def shuffle_random_entrances(worlds):
if not worlds[0].settings.shuffle_dungeon_entrances and not worlds[0].settings.shuffle_overworld_entrances:
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']

if worlds[0].settings.shuffle_bosses == 'full':
entrance_pools['Boss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
entrance_pools['Boss'] += world.get_shufflable_entrances(type='AdultBoss', only_primary=True)
elif worlds[0].settings.shuffle_bosses == 'limited':
entrance_pools['ChildBoss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
entrance_pools['AdultBoss'] = world.get_shufflable_entrances(type='AdultBoss', only_primary=True)

if worlds[0].settings.shuffle_dungeon_entrances:
entrance_pools['Dungeon'] = world.get_shufflable_entrances(type='Dungeon', only_primary=True)
# The fill algorithm will already make sure gohma is reachable, however it can end up putting
Expand Down Expand Up @@ -493,6 +566,11 @@ def shuffle_random_entrances(worlds):
for pool_type, entrance_pool in entrance_pools.items():
shuffle_entrance_pool(world, worlds, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable)

if pool_type in ('Boss', 'ChildBoss', 'AdultBoss'):
for entrance in entrance_pool:
entrance.connected_region.change_dungeon(entrance.parent_region.dungeon)


# Multiple checks after shuffling entrances to make sure everything went fine
max_search = Search.max_explore([world.state for world in worlds], complete_itempool)

Expand Down
10 changes: 6 additions & 4 deletions Hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,8 @@ def buildWorldGossipHints(spoiler, world, checkedLocations=None):

# builds text that is displayed at the temple of time altar for child and adult, rewards pulled based off of item in a fixed order.
def buildAltarHints(world, messages, include_rewards=True, include_wincons=True):
boss_map = world.reverse_boss_map()

# text that appears at altar as a child.
child_text = '\x08'
if include_rewards:
Expand All @@ -1165,7 +1167,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
]
child_text += getHint('Spiritual Stone Text Start', world.settings.clearer_hints).text + '\x04'
for (reward, color) in bossRewardsSpiritualStones:
child_text += buildBossString(reward, color, world)
child_text += buildBossString(reward, color, world, boss_map)
child_text += getHint('Child Altar Text End', world.settings.clearer_hints).text
child_text += '\x0B'
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
Expand All @@ -1183,7 +1185,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
('Spirit Medallion', 'Yellow'),
]
for (reward, color) in bossRewardsMedallions:
adult_text += buildBossString(reward, color, world)
adult_text += buildBossString(reward, color, world, boss_map)
if include_wincons:
adult_text += buildBridgeReqsString(world)
adult_text += '\x04'
Expand All @@ -1195,11 +1197,11 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)


# pulls text string from hintlist for reward after sending the location to hintlist.
def buildBossString(reward, color, world):
def buildBossString(reward, color, world, boss_map):
for location in world.get_filled_locations():
if location.item.name == reward:
item_icon = chr(location.item.special['item_id'])
location_text = getHint(location.name, world.settings.clearer_hints).text
location_text = getHint(boss_map.get(location.name, location.name), world.settings.clearer_hints).text
return str(GossipText("\x08\x13%s%s" % (item_icon, location_text), [color], prefix='')) + '\x04'
return ''

Expand Down
10 changes: 7 additions & 3 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,17 @@ def build_world_graphs(settings, window=dummy_window()):
window.update_progress(0 + 1*(id + 1)/settings.world_count)
logger.info('Creating Overworld')

# Load common json rule files (those used regardless of MQ status)
if settings.logic_rules == 'glitched':
overworld_data = os.path.join(data_path('Glitched World'), 'Overworld.json')
path = 'Glitched World'
else:
overworld_data = os.path.join(data_path('World'), 'Overworld.json')
path = 'World'
path = data_path(path)

for filename in ('Overworld.json', 'Bosses.json'):
world.load_regions_from_json(os.path.join(path, filename))

# Compile the json rules based on settings
world.load_regions_from_json(overworld_data)
create_dungeons(world)
world.create_internal_locations()

Expand Down
88 changes: 78 additions & 10 deletions Patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,56 @@ def add_scene_exits(scene_start, offset = 0):

return exit_table

if (world.settings.shuffle_bosses != 'off'):
# Credit to rattus128 for this ASM block.
# Gohma's save/death warp is optimized to use immediate 0 for the
# deku tree respawn. Use the delay slot before the switch table
# to hold Gohmas jump entrance as actual data so we can substitute
# the entrance index later.
rom.write_int32(0xB06290, 0x240E0000) #li t6, 0
rom.write_int32(0xB062B0, 0xAE0E0000) #sw t6, 0(s0)
rom.write_int32(0xBC60AC, 0x24180000) #li t8, 0
rom.write_int32(0xBC6160, 0x24180000) #li t8, 0
rom.write_int32(0xBC6168, 0xAD380000) #sw t8, 0(t1)

# Credit to engineer124
# Update the Jabu-Jabu Boss Exit to actually useful coordinates (and to load the correct room)
rom.write_int16(0x273E08E, 0xF7F4) # Z coordinate of Jabu Boss Door Spawn
rom.write_byte(0x273E27B, 0x05) # Set Spawn Room to be correct

def set_entrance_updates(entrances):
blue_warp_remaps = {}
if (world.settings.shuffle_bosses != 'off'):
# Connect lake hylia fill exit to revisit exit
rom.write_int16(0xAC995A, 0x060C)

# First pass for boss shuffle
# We'll need to iterate more than once, so make a copy so we can iterate more than once.
entrances = list(entrances)
for entrance in entrances:
if entrance.type not in('ChildBoss', 'AdultBoss') or not entrance.replaces or 'patch_addresses' not in entrance.data:
continue
if entrance == entrance.replaces:
# This can happen if something is plando'd vanilla.
continue

new_boss = entrance.replaces.data
original_boss = entrance.data

# Fixup save/quit and death warping entrance IDs on bosses.
for address in new_boss['patch_addresses']:
rom.write_int16(address, original_boss['dungeon_index'])

# Update blue warps.
# If dungeons are shuffled, we'll this in the next step -- that's fine.
copy_entrance_record(original_boss['exit_index'], new_boss['exit_blue_warp'], 2)
copy_entrance_record(original_boss['exit_blue_warp'] + 2, new_boss['exit_blue_warp'] + 2, 2)

# If dungeons are shuffled but their bosses are moved, they're going to refer to the wrong blue warp
# slots. Create a table to remap them for later.
blue_warp_remaps[original_boss['exit_blue_warp']] = new_boss['exit_blue_warp']

# Boss shuffle done(?)
for entrance in entrances:
new_entrance = entrance.data
replaced_entrance = entrance.replaces.data
Expand All @@ -836,9 +884,16 @@ def set_entrance_updates(entrances):
for address in new_entrance.get('addresses', []):
rom.write_int16(address, replaced_entrance['index'])

patch_value = replaced_entrance.get('patch_value')
if patch_value is not None:
for address in new_entrance['patch_addresses']:
rom.write_int16(address, patch_value)

if "blue_warp" in new_entrance:
blue_warp = new_entrance["blue_warp"]
blue_warp = blue_warp_remaps.get(blue_warp, blue_warp)
if "blue_warp" in replaced_entrance:
blue_out_data = replaced_entrance["blue_warp"]
blue_out_data = replaced_entrance["blue_warp"]
else:
blue_out_data = replaced_entrance["index"]
# Blue warps have multiple hardcodes leading to them. The good news is
Expand All @@ -849,8 +904,8 @@ def set_entrance_updates(entrances):
# vanilla as it never took you to the exit and the lake fill is handled
# above by removing the cutscene completely. Child has problems with Adult
# blue warps, so always use the return entrance if a child.
copy_entrance_record(blue_out_data + 2, new_entrance["blue_warp"] + 2, 2)
copy_entrance_record(replaced_entrance["index"], new_entrance["blue_warp"], 2)
copy_entrance_record(blue_out_data + 2, blue_warp + 2, 2)
copy_entrance_record(replaced_entrance["index"], blue_warp, 2)

exit_table = generate_exit_lookup_table()

Expand Down Expand Up @@ -903,7 +958,10 @@ def set_entrance_updates(entrances):
# Purge temp flags on entrance to spirit from colossus through the front door.
rom.write_byte(0x021862E3, 0xC2)

if world.settings.shuffle_overworld_entrances or world.settings.shuffle_dungeon_entrances:
if (
world.settings.shuffle_overworld_entrances or world.settings.shuffle_dungeon_entrances
or (world.settings.shuffle_bosses != 'off')
):
# Remove deku sprout and drop player at SFM after forest completion
rom.write_int16(0xAC9F96, 0x0608)

Expand Down Expand Up @@ -1406,6 +1464,8 @@ def set_entrance_updates(entrances):
new_message = "\x08What should I do!?\x01My \x05\x41Cuccos\x05\x40 have all flown away!\x04You, little boy, please!\x01Please gather at least \x05\x41%d Cuccos\x05\x40\x01for me.\x02" % world.settings.chicken_count
update_message_by_id(messages, 0x5036, new_message)

boss_map = world.get_boss_map()

# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
reward_text = {'Kokiri Emerald': "\x05\x42Kokiri Emerald\x05\x40",
'Goron Ruby': "\x05\x41Goron Ruby\x05\x40",
Expand All @@ -1417,7 +1477,7 @@ def set_entrance_updates(entrances):
'Shadow Medallion': "\x05\x45Shadow Medallion\x05\x40",
'Light Medallion': "\x05\x44Light Medallion\x05\x40"
}
new_message = "\x1a\x08Princess Ruto got the \x01%s!\x09\x01\x14\x02But\x14\x00 why Princess Ruto?\x02" % reward_text[world.get_location('Barinade').item.name]
new_message = "\x1a\x08Princess Ruto got the \x01%s!\x09\x01\x14\x02But\x14\x00 why Princess Ruto?\x02" % reward_text[world.get_location(boss_map['Barinade']).item.name]
update_message_by_id(messages, 0x4050, new_message)

# use faster jabu elevator
Expand Down Expand Up @@ -1828,7 +1888,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
update_message_by_id(messages, map_id, map_message)
else:
dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
dungeon_reward = reward_list[world.get_location(boss_name).item.name]
dungeon_reward = reward_list[world.get_location(boss_map[boss_name]).item.name]
if world.settings.world_count > 1:
compass_message = "\x13\x75\x08\x05\x42\x0F\x05\x40 found the \x05\x41Compass\x05\x40\x01for %s\x05\x40!\x09" % (dungeon_name)
else:
Expand All @@ -1846,13 +1906,20 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
rom.write_int16(0xE2ADB6, 0x7057)
buildAltarHints(world, messages, include_rewards=world.settings.misc_hints and not world.settings.enhance_map_compass, include_wincons=world.settings.misc_hints)

# Set Dungeon Reward actors in Jabu Jabu to be accurate
jabu_actor_type = world.get_location('Barinade').item.special['actor_type']
# Set Dungeon Reward Actor in Jabu Jabu to be accurate
# Vanilla and MQ Jabu Jabu addresses are the same for this object and actor
jabu_item = world.get_location(boss_map['Barinade']).item
jabu_stone_object = jabu_item.special['object_id']
rom.write_int16(0x277D068, jabu_stone_object)
rom.write_int16(0x277D168, jabu_stone_object)
jabu_stone_type = jabu_item.special['actor_type']
rom.write_byte(0x277D0BB, jabu_stone_type)
rom.write_byte(0x277D19B, jabu_stone_type)
jabu_actor_type = jabu_item.special['actor_type']
set_jabu_stone_actors(rom, jabu_actor_type)
# Also set the right object for the actor, since medallions and stones require different objects
# MQ is handled separately, as we include both objects in the object list in mqu.json (Scene 2, Room 6)
if not world.dungeon_mq['Jabu Jabus Belly']:
jabu_stone_object = world.get_location('Barinade').item.special['object_id']
rom.write_int16(0x277D068, jabu_stone_object)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be redundant with the code added above. Why is a change needed here anyway, besides inserting the boss_map indirection?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a backport from Roman's fork. Boss Shuffle began there and I ported it to Dev branch since I felt it more likely to be accepted here and then merged into Roman's that way. I think I didn't realize that Dev didn't have it this way when rebasing.

Here is the original source: https://github.com/Roman971/OoT-Randomizer/blob/Dev-R/Patches.py#L1863..L1880

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code added above was a leftover on my branch (I'm guessing due to some merge conflict), and I just removed it from Dev-R. I think you should be able to just adjust the existing code to use boss_map and omit the rest.

Here is the commit where I removed it for reference: Roman971@ba15f86

rom.write_int16(0x277D168, jabu_stone_object)

Expand Down Expand Up @@ -2316,9 +2383,10 @@ def configure_dungeon_info(rom, world):
mq_enable = (world.settings.mq_dungeons_random or world.settings.mq_dungeons != 0 and world.settings.mq_dungeons != 12)
enhance_map_compass = world.settings.enhance_map_compass

boss_map = world.get_boss_map()
bosses = ['Queen Gohma', 'King Dodongo', 'Barinade', 'Phantom Ganon',
'Volvagia', 'Morpha', 'Twinrova', 'Bongo Bongo']
dungeon_rewards = [boss_reward_index(world, boss) for boss in bosses]
dungeon_rewards = [boss_reward_index(world, boss_map[boss]) for boss in bosses]

codes = ['Deku Tree', 'Dodongos Cavern', 'Jabu Jabus Belly', 'Forest Temple',
'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple',
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ do that.
* New cosmetic setting `Disable Battle Music` turns off the battle music from being near enemies, allowing the background music to continue uninterrupted.
* New setting `Plant Magic Beans` automatically plants all the Magic Beans from the start.
* New setting `Key Rings` which can be enabled per-dungeon to replace all of its individual Small Keys into a singular Small Key Ring containing all the small keys for that dungeon.
* New setting `Shuffle Boss Entrances` allows boss rooms to be shuffled between dungeons. This is only available in glitchless logic.

* **Gameplay**
* Shortened the animation for equipping magic arrows.
Expand Down
9 changes: 9 additions & 0 deletions Region.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ def get_scene(self):
return None


def change_dungeon(self, new_dungeon):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for boss rooms to be tagged with a dungeon at all? I think it could be more straightforward to not consider boss rooms as part of any dungeon's hint area, and instead relying on finding the dungeon as the nearest hint area in the same way that interiors do. This would also make hints behave well in mixed pools boss shuffle if that ever becomes a thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, can_fill() in Region.py relies on .dungeon being set to know if a location qualifies for settings like "small keys in any dungeon"

Updating change_dungeon to accept None would be a step towards allowing mixed pools, however -- but as this branch doesn't support mixed pools at all it seems a little out-of-scope for the moment.

# Change the dungeon of this region, removing it from the old dungeon list and adding it to the new one.
if new_dungeon == self.dungeon:
return
self.dungeon.regions.remove(self)
self.dungeon = new_dungeon
new_dungeon.regions.append(self)


def __str__(self):
return str(self.__unicode__())

Expand Down
Loading