Skip to content

Commit

Permalink
Ocarina of Time: Itemlinks and bugfixes (ArchipelagoMW#1157)
Browse files Browse the repository at this point in the history
* OoT: ER improvements
Include dungeon rewards in itempool to allow for ER improvement
Better validate_world function by checking for multi-entrance incompatibility more efficiently
Fix some generation failures by ensuring all entrances placed with logic
Introduce bias to some interior entrance placement to improve generation rate

* OoT: fix overworld ER spoiler information

* OoT: rewrite dungeon item placement algorithm
in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items.

* OoT: auto-send more locations
Now should autosend cows, DMT/DMC great fairies, medigoron, and bombchu salesman
This should be every check autosending. these ones are super weird for some reason and didn't get fixed with the others

* OoT: add items forced local by settings to AP's local_items

* OoT: fast-fill shop junk items

* OoT: ensure that Kokiri Shop is always reachable immediately in closed forest
hence Deku Shield can be bought to leave the forest

* OoT: randomize internal connect name
Connect name is now a random 16-character string.
This should prevent any issues with connecting to a room with the wrong ROM with probability almost 1.

* OoT: introduce TrackRandomRange for trials hint and mq dungeon maps

* OoT: enable proper itemlinking of songs and dungeon items, with restricted placements according to player settings

* OoT: barren hint oversight fix

* OoT: allow NL + ER to roll properly

* OoT: 3.8 compatibility
set and list builtins don't have proper typing support until 3.9, apparently

* OoT: remove Gerudo Membership Card location from the pool if fortress open and card not randomized
another long-standing bug squished

* OoT: exclude locations in the itemlink song fill if they aren't also priority

* OoT: prevent data bleed when client isn't closed between different game connections
I don't understand why people keep doing this

* OoT: linter appeasement
it was a real error though

* fixing merge conflicts is hard

* oot merge update #2

* OoT: removed accidentally duplicated code
  • Loading branch information
espeon65536 authored Nov 2, 2022
1 parent 9537823 commit a6e1e14
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 80 deletions.
13 changes: 13 additions & 0 deletions OoTClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ def get_payload(ctx: OoTContext):

async def parse_payload(payload: dict, ctx: OoTContext, force: bool):

# Refuse to do anything if ROM is detected as changed
if ctx.auth and payload['playerName'] != ctx.auth:
logger.warning("ROM change detected. Disconnecting and reconnecting...")
ctx.deathlink_enabled = False
ctx.deathlink_client_override = False
ctx.finished_game = False
ctx.location_table = {}
ctx.deathlink_pending = False
ctx.deathlink_sent_this_death = False
ctx.auth = payload['playerName']
await ctx.send_connect()
return

# Turn on deathlink if it is on, and if the client hasn't overriden it
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
await ctx.update_death_link(True)
Expand Down
25 changes: 19 additions & 6 deletions data/lua/OOT/oot_connector.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ local scrub_sanity_check = function(scene_offset, bit_to_check)
return scene_check(scene_offset, bit_to_check, 0x10)
end

-- Why is there an extra offset of 3 for temp context checks? Who knows.
local cow_check = function(scene_offset, bit_to_check)
return scene_check(scene_offset, bit_to_check, 0xC)
or check_temp_context({scene_offset, 0x00, bit_to_check})
or check_temp_context({scene_offset, 0x00, bit_to_check - 0x03})
end

-- Haven't been able to get DMT and DMC fairy to send instantly
-- DMT and DMC fairies are weird, their temp context check is special-coded for them
local great_fairy_magic_check = function(scene_offset, bit_to_check)
return scene_check(scene_offset, bit_to_check, 0x4)
or check_temp_context({scene_offset, 0x05, bit_to_check})
Expand All @@ -100,6 +101,18 @@ local bean_sale_check = function(scene_offset, bit_to_check)
or check_temp_context({scene_offset, 0x00, 0x16})
end

-- Medigoron reports 0x00620028 to 0x40002C
local medigoron_check = function(scene_offset, bit_to_check)
return scene_check(scene_offset, bit_to_check, 0xC)
or check_temp_context({scene_offset, 0x00, 0x28})
end

-- Bombchu salesman reports 0x005E0003 to 0x40002C
local salesman_check = function(scene_offset, bit_to_check)
return scene_check(scene_offset, bit_to_check, 0xC)
or check_temp_context({scene_offset, 0x00, 0x03})
end

--Helper method to resolve skulltula lookup location
local function skulltula_scene_to_array_index(i)
return (i + 3) - 2 * (i % 4)
Expand Down Expand Up @@ -575,7 +588,7 @@ local read_death_mountain_trail_checks = function()
checks["DMT Freestanding PoH"] = on_the_ground_check(0x60, 0x1E)
checks["DMT Chest"] = chest_check(0x60, 0x01)
checks["DMT Storms Grotto Chest"] = chest_check(0x3E, 0x17)
checks["DMT Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x18)
checks["DMT Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x18) or check_temp_context({0xFF, 0x05, 0x13})
checks["DMT Biggoron"] = big_goron_sword_check()
checks["DMT Cow Grotto Cow"] = cow_check(0x3E, 0x18)

Expand All @@ -592,7 +605,7 @@ local read_goron_city_checks = function()
checks["GC Pot Freestanding PoH"] = on_the_ground_check(0x62, 0x1F)
checks["GC Rolling Goron as Child"] = info_table_check(0x22, 0x6)
checks["GC Rolling Goron as Adult"] = info_table_check(0x20, 0x1)
checks["GC Medigoron"] = on_the_ground_check(0x62, 0x1)
checks["GC Medigoron"] = medigoron_check(0x62, 0x1)
checks["GC Maze Left Chest"] = chest_check(0x62, 0x00)
checks["GC Maze Right Chest"] = chest_check(0x62, 0x01)
checks["GC Maze Center Chest"] = chest_check(0x62, 0x02)
Expand All @@ -614,7 +627,7 @@ local read_death_mountain_crater_checks = function()
checks["DMC Volcano Freestanding PoH"] = on_the_ground_check(0x61, 0x08)
checks["DMC Wall Freestanding PoH"] = on_the_ground_check(0x61, 0x02)
checks["DMC Upper Grotto Chest"] = chest_check(0x3E, 0x1A)
checks["DMC Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x10)
checks["DMC Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x10) or check_temp_context({0xFF, 0x05, 0x14})

checks["DMC Deku Scrub"] = scrub_sanity_check(0x61, 0x6)
checks["DMC Deku Scrub Grotto Left"] = scrub_sanity_check(0x23, 0x1)
Expand Down Expand Up @@ -961,7 +974,7 @@ end

local read_haunted_wasteland_checks = function()
local checks = {}
checks["Wasteland Bombchu Salesman"] = on_the_ground_check(0x5E, 0x01)
checks["Wasteland Bombchu Salesman"] = salesman_check(0x5E, 0x01)
checks["Wasteland Chest"] = chest_check(0x5E, 0x00)
checks["Wasteland GS"] = skulltula_check(0x15, 0x1)
return checks
Expand Down
8 changes: 6 additions & 2 deletions worlds/oot/EntranceShuffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
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:
# Check if all locations are reachable if not NL
if ootworld.logic_rules != 'no_logic' and locations_to_ensure_reachable:
for loc in locations_to_ensure_reachable:
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')
Expand Down Expand Up @@ -796,6 +796,10 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
raise EntranceShuffleError('Goron City Shop not accessible as adult')
if world.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]:
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')



Expand Down
4 changes: 2 additions & 2 deletions worlds/oot/Hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,9 +741,9 @@ def buildWorldGossipHints(world, checkedLocations=None):

# Add trial hints, only if hint copies > 0
if hint_dist['trial'][1] > 0:
if world.trials == 6:
if world.trials_random and world.trials == 6:
add_hint(world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True)
elif world.trials == 0:
elif world.trials_random and world.trials == 0:
add_hint(world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True)
elif world.trials < 6 and world.trials > 3:
for trial,skipped in world.skipped_trials.items():
Expand Down
3 changes: 3 additions & 0 deletions worlds/oot/ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,10 @@ def get_pool_core(world):
placed_items['Hideout Gerudo Membership Card'] = 'Ice Trap'
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
else:
card = world.create_item('Gerudo Membership Card')
world.multiworld.push_precollected(card)
placed_items['Hideout Gerudo Membership Card'] = 'Gerudo Membership Card'
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
if world.shuffle_gerudo_card and world.item_pool_value == 'plentiful':
pending_junk_pool.append('Gerudo Membership Card')

Expand Down
8 changes: 5 additions & 3 deletions worlds/oot/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ def ap_id_to_oot_data(ap_id):


def oot_is_item_of_type(item, item_type):
if not isinstance(item, OOTItem):
return False
return item.type == item_type
if isinstance(item, OOTItem):
return item.type == item_type
if isinstance(item, str):
return item in item_table and item_table[item][0] == item_type
return False


class OOTItem(Item):
Expand Down
18 changes: 18 additions & 0 deletions worlds/oot/LocationList.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,24 @@ def shop_address(shop_id, shelf_id):
'Dungeon': [name for (name, data) in location_table.items() if data[5] is not None and any(dungeon in data[5] for dungeon in dungeons)],
}

# relevant for both dungeon item fill and song fill
dungeon_song_locations = [
"Deku Tree Queen Gohma Heart",
"Dodongos Cavern King Dodongo Heart",
"Jabu Jabus Belly Barinade Heart",
"Forest Temple Phantom Ganon Heart",
"Fire Temple Volvagia Heart",
"Water Temple Morpha Heart",
"Shadow Temple Bongo Bongo Heart",
"Spirit Temple Twinrova Heart",
"Song from Impa",
"Sheik in Ice Cavern",
# only one exists
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest",
# only one exists
"Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest",
]


def location_is_viewable(loc_name, correct_chest_sizes):
return correct_chest_sizes and loc_name in location_groups['Chest'] or loc_name in location_groups['CanSee']
Expand Down
29 changes: 27 additions & 2 deletions worlds/oot/Options.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import typing
import random
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink
from .LogicTricks import normalized_name_tricks
from .ColorSFXOptions import *


class TrackRandomRange(Range):
"""Overrides normal from_any behavior to track whether the option was randomized at generation time."""
supports_weighting = False
randomized: bool = False

@classmethod
def from_any(cls, data: typing.Any) -> Range:
if type(data) is list:
val = random.choices(data)[0]
ret = super().from_any(val)
if not isinstance(val, int) or len(data) > 1:
ret.randomized = True
return ret
if type(data) is not dict:
return super().from_any(data)
if any(data.values()):
val = random.choices(list(data.keys()), weights=list(map(int, data.values())))[0]
ret = super().from_any(val)
if not isinstance(val, int) or len(list(filter(bool, map(int, data.values())))) > 1:
ret.randomized = True
return ret
raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.")


class Logic(Choice):
"""Set the logic used for the generator."""
display_name = "Logic Rules"
Expand Down Expand Up @@ -70,7 +95,7 @@ class Bridge(Choice):
default = 3


class Trials(Range):
class Trials(TrackRandomRange):
"""Set the number of required trials in Ganon's Castle."""
display_name = "Ganon's Trials Count"
range_start = 0
Expand Down Expand Up @@ -173,7 +198,7 @@ class LogicalChus(Toggle):
display_name = "Bombchus Considered in Logic"


class MQDungeons(Range):
class MQDungeons(TrackRandomRange):
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
display_name = "Number of MQ Dungeons"
range_start = 0
Expand Down
4 changes: 2 additions & 2 deletions worlds/oot/Patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def patch_rom(world, rom):
rom.write_bytes(0x1FC0CF8, Block_code)

# songs as items flag
songs_as_items = (world.shuffle_song_items != 'song') or world.starting_songs
songs_as_items = (world.shuffle_song_items != 'song') or world.songs_as_items

if songs_as_items:
rom.write_byte(rom.sym('SONGS_AS_ITEMS'), 1)
Expand Down Expand Up @@ -1326,7 +1326,7 @@ def set_entrance_updates(entrances):
override_table = get_override_table(world)
rom.write_bytes(rom.sym('cfg_item_overrides'), get_override_table_bytes(override_table))
rom.write_byte(rom.sym('PLAYER_ID'), min(world.player, 255)) # Write player ID
rom.write_bytes(rom.sym('AP_PLAYER_NAME'), bytearray(world.multiworld.get_player_name(world.player), 'ascii'))
rom.write_bytes(rom.sym('AP_PLAYER_NAME'), bytearray(world.connect_name, encoding='ascii'))

if world.death_link:
rom.write_byte(rom.sym('DEATH_LINK'), 0x01)
Expand Down
2 changes: 1 addition & 1 deletion worlds/oot/Rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def set_rules(ootworld):
location = world.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)

if ootworld.shuffle_song_items == 'song' and not ootworld.starting_songs:
if ootworld.shuffle_song_items == 'song' 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)
Expand Down
Loading

0 comments on commit a6e1e14

Please sign in to comment.