Skip to content

Commit a6e1e14

Browse files
authored
Ocarina of Time: Itemlinks and bugfixes (#1157)
* 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
1 parent 9537823 commit a6e1e14

11 files changed

+245
-80
lines changed

OoTClient.py

+13
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ def get_payload(ctx: OoTContext):
133133

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

136+
# Refuse to do anything if ROM is detected as changed
137+
if ctx.auth and payload['playerName'] != ctx.auth:
138+
logger.warning("ROM change detected. Disconnecting and reconnecting...")
139+
ctx.deathlink_enabled = False
140+
ctx.deathlink_client_override = False
141+
ctx.finished_game = False
142+
ctx.location_table = {}
143+
ctx.deathlink_pending = False
144+
ctx.deathlink_sent_this_death = False
145+
ctx.auth = payload['playerName']
146+
await ctx.send_connect()
147+
return
148+
136149
# Turn on deathlink if it is on, and if the client hasn't overriden it
137150
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
138151
await ctx.update_death_link(True)

data/lua/OOT/oot_connector.lua

+19-6
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ local scrub_sanity_check = function(scene_offset, bit_to_check)
7777
return scene_check(scene_offset, bit_to_check, 0x10)
7878
end
7979

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

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

104+
-- Medigoron reports 0x00620028 to 0x40002C
105+
local medigoron_check = function(scene_offset, bit_to_check)
106+
return scene_check(scene_offset, bit_to_check, 0xC)
107+
or check_temp_context({scene_offset, 0x00, 0x28})
108+
end
109+
110+
-- Bombchu salesman reports 0x005E0003 to 0x40002C
111+
local salesman_check = function(scene_offset, bit_to_check)
112+
return scene_check(scene_offset, bit_to_check, 0xC)
113+
or check_temp_context({scene_offset, 0x00, 0x03})
114+
end
115+
103116
--Helper method to resolve skulltula lookup location
104117
local function skulltula_scene_to_array_index(i)
105118
return (i + 3) - 2 * (i % 4)
@@ -575,7 +588,7 @@ local read_death_mountain_trail_checks = function()
575588
checks["DMT Freestanding PoH"] = on_the_ground_check(0x60, 0x1E)
576589
checks["DMT Chest"] = chest_check(0x60, 0x01)
577590
checks["DMT Storms Grotto Chest"] = chest_check(0x3E, 0x17)
578-
checks["DMT Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x18)
591+
checks["DMT Great Fairy Reward"] = great_fairy_magic_check(0x3B, 0x18) or check_temp_context({0xFF, 0x05, 0x13})
579592
checks["DMT Biggoron"] = big_goron_sword_check()
580593
checks["DMT Cow Grotto Cow"] = cow_check(0x3E, 0x18)
581594

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

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

962975
local read_haunted_wasteland_checks = function()
963976
local checks = {}
964-
checks["Wasteland Bombchu Salesman"] = on_the_ground_check(0x5E, 0x01)
977+
checks["Wasteland Bombchu Salesman"] = salesman_check(0x5E, 0x01)
965978
checks["Wasteland Chest"] = chest_check(0x5E, 0x00)
966979
checks["Wasteland GS"] = skulltula_check(0x15, 0x1)
967980
return checks

worlds/oot/EntranceShuffle.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
738738
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
739739
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
740740

741-
# Check if all locations are reachable if not beatable-only or game is not yet complete
742-
if locations_to_ensure_reachable:
741+
# Check if all locations are reachable if not NL
742+
if ootworld.logic_rules != 'no_logic' and locations_to_ensure_reachable:
743743
for loc in locations_to_ensure_reachable:
744744
if not all_state.can_reach(loc, 'Location', player):
745745
raise EntranceShuffleError(f'{loc} is unreachable')
@@ -796,6 +796,10 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
796796
raise EntranceShuffleError('Goron City Shop not accessible as adult')
797797
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
798798
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
799+
if ootworld.open_forest == 'closed':
800+
# Ensure that Kokiri Shop is reachable as child with no items
801+
if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
802+
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
799803

800804

801805

worlds/oot/Hints.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -741,9 +741,9 @@ def buildWorldGossipHints(world, checkedLocations=None):
741741

742742
# Add trial hints, only if hint copies > 0
743743
if hint_dist['trial'][1] > 0:
744-
if world.trials == 6:
744+
if world.trials_random and world.trials == 6:
745745
add_hint(world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True)
746-
elif world.trials == 0:
746+
elif world.trials_random and world.trials == 0:
747747
add_hint(world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True)
748748
elif world.trials < 6 and world.trials > 3:
749749
for trial,skipped in world.skipped_trials.items():

worlds/oot/ItemPool.py

+3
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,10 @@ def get_pool_core(world):
11001100
placed_items['Hideout Gerudo Membership Card'] = 'Ice Trap'
11011101
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
11021102
else:
1103+
card = world.create_item('Gerudo Membership Card')
1104+
world.multiworld.push_precollected(card)
11031105
placed_items['Hideout Gerudo Membership Card'] = 'Gerudo Membership Card'
1106+
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
11041107
if world.shuffle_gerudo_card and world.item_pool_value == 'plentiful':
11051108
pending_junk_pool.append('Gerudo Membership Card')
11061109

worlds/oot/Items.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ def ap_id_to_oot_data(ap_id):
2323

2424

2525
def oot_is_item_of_type(item, item_type):
26-
if not isinstance(item, OOTItem):
27-
return False
28-
return item.type == item_type
26+
if isinstance(item, OOTItem):
27+
return item.type == item_type
28+
if isinstance(item, str):
29+
return item in item_table and item_table[item][0] == item_type
30+
return False
2931

3032

3133
class OOTItem(Item):

worlds/oot/LocationList.py

+18
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,24 @@ def shop_address(shop_id, shelf_id):
919919
'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)],
920920
}
921921

922+
# relevant for both dungeon item fill and song fill
923+
dungeon_song_locations = [
924+
"Deku Tree Queen Gohma Heart",
925+
"Dodongos Cavern King Dodongo Heart",
926+
"Jabu Jabus Belly Barinade Heart",
927+
"Forest Temple Phantom Ganon Heart",
928+
"Fire Temple Volvagia Heart",
929+
"Water Temple Morpha Heart",
930+
"Shadow Temple Bongo Bongo Heart",
931+
"Spirit Temple Twinrova Heart",
932+
"Song from Impa",
933+
"Sheik in Ice Cavern",
934+
# only one exists
935+
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest",
936+
# only one exists
937+
"Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest",
938+
]
939+
922940

923941
def location_is_viewable(loc_name, correct_chest_sizes):
924942
return correct_chest_sizes and loc_name in location_groups['Chest'] or loc_name in location_groups['CanSee']

worlds/oot/Options.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import typing
2+
import random
23
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink
34
from .LogicTricks import normalized_name_tricks
45
from .ColorSFXOptions import *
56

67

8+
class TrackRandomRange(Range):
9+
"""Overrides normal from_any behavior to track whether the option was randomized at generation time."""
10+
supports_weighting = False
11+
randomized: bool = False
12+
13+
@classmethod
14+
def from_any(cls, data: typing.Any) -> Range:
15+
if type(data) is list:
16+
val = random.choices(data)[0]
17+
ret = super().from_any(val)
18+
if not isinstance(val, int) or len(data) > 1:
19+
ret.randomized = True
20+
return ret
21+
if type(data) is not dict:
22+
return super().from_any(data)
23+
if any(data.values()):
24+
val = random.choices(list(data.keys()), weights=list(map(int, data.values())))[0]
25+
ret = super().from_any(val)
26+
if not isinstance(val, int) or len(list(filter(bool, map(int, data.values())))) > 1:
27+
ret.randomized = True
28+
return ret
29+
raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.")
30+
31+
732
class Logic(Choice):
833
"""Set the logic used for the generator."""
934
display_name = "Logic Rules"
@@ -70,7 +95,7 @@ class Bridge(Choice):
7095
default = 3
7196

7297

73-
class Trials(Range):
98+
class Trials(TrackRandomRange):
7499
"""Set the number of required trials in Ganon's Castle."""
75100
display_name = "Ganon's Trials Count"
76101
range_start = 0
@@ -173,7 +198,7 @@ class LogicalChus(Toggle):
173198
display_name = "Bombchus Considered in Logic"
174199

175200

176-
class MQDungeons(Range):
201+
class MQDungeons(TrackRandomRange):
177202
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
178203
display_name = "Number of MQ Dungeons"
179204
range_start = 0

worlds/oot/Patches.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def patch_rom(world, rom):
168168
rom.write_bytes(0x1FC0CF8, Block_code)
169169

170170
# songs as items flag
171-
songs_as_items = (world.shuffle_song_items != 'song') or world.starting_songs
171+
songs_as_items = (world.shuffle_song_items != 'song') or world.songs_as_items
172172

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

13311331
if world.death_link:
13321332
rom.write_byte(rom.sym('DEATH_LINK'), 0x01)

worlds/oot/Rules.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def set_rules(ootworld):
135135
location = world.get_location('Forest Temple MQ First Room Chest', player)
136136
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
137137

138-
if ootworld.shuffle_song_items == 'song' and not ootworld.starting_songs:
138+
if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items:
139139
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
140140
# This is required if map/compass included, or any_dungeon shuffle.
141141
location = world.get_location('Sheik in Ice Cavern', player)

0 commit comments

Comments
 (0)