diff --git a/OoTClient.py b/OoTClient.py index b3c58612..22420e0e 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -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) diff --git a/data/lua/OOT/oot_connector.lua b/data/lua/OOT/oot_connector.lua index a82bcdcb..39499b21 100644 --- a/data/lua/OOT/oot_connector.lua +++ b/data/lua/OOT/oot_connector.lua @@ -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}) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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 diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index cfe1a5da..c9910e81 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -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') @@ -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') diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 3c3f5cc3..37b24c88 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -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(): diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index e1b9ae7b..524fa659 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -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') diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index 06164091..77e1d1c2 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -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): diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py index 829ec702..5a965ea0 100644 --- a/worlds/oot/LocationList.py +++ b/worlds/oot/LocationList.py @@ -919,6 +919,24 @@ location_groups = { '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'] diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index deee5587..f8696a16 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -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" @@ -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 @@ -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 diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 31e57311..4dcf47d4 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -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) @@ -1326,7 +1326,7 @@ def patch_rom(world, rom): 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) diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 46b8b69c..3f7a2fa1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -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) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 2f351506..c089a632 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1,7 +1,9 @@ import logging import threading import copy +from typing import Optional, List, AbstractSet # remove when 3.8 support is dropped from collections import Counter, deque +from string import printable logger = logging.getLogger("Ocarina of Time") @@ -15,7 +17,7 @@ from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .RuleParser import Rule_AST_Transformer from .Options import oot_options from .Utils import data_path, read_json -from .LocationList import business_scrubs, set_drop_location_names +from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations from .DungeonList import dungeon_table, create_dungeons from .LogicTricks import normalized_name_tricks from .Rom import Rom @@ -27,10 +29,10 @@ from .HintList import getRequiredHints from .SaveContext import SaveContext from Utils import get_options, output_path -from BaseClasses import MultiWorld, CollectionState, RegionType, Tutorial +from BaseClasses import MultiWorld, CollectionState, RegionType, Tutorial, LocationProgressType from Options import Range, Toggle, OptionList -from Fill import fill_restrictive, FillError -from worlds.generic.Rules import exclusion_rules +from Fill import fill_restrictive, fast_fill, FillError +from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld location_id_offset = 67000 @@ -117,11 +119,6 @@ class OOTWorld(World): rom = Rom(file=get_options()['oot_options']['rom_file']) def generate_early(self): - # Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly - if len(bytes(self.multiworld.get_player_name(self.player), 'ascii')) > 16: - raise Exception( - f"OoT: Player {self.player}'s name ({self.multiworld.get_player_name(self.player)}) must be ASCII-compatible") - self.parser = Rule_AST_Transformer(self, self.player) for (option_name, option) in oot_options.items(): @@ -140,8 +137,9 @@ class OOTWorld(World): self.regions = [] # internal cache of regions for this world, used later self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() - self.starting_songs = False # whether starting_items contains a song + 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.item_name_groups = { "medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", @@ -182,6 +180,31 @@ class OOTWorld(World): if self.triforce_hunt: self.shuffle_ganon_bosskey = 'remove' + # If songs/keys locked to own world by settings, add them to local_items + local_types = [] + if self.shuffle_song_items != 'any': + local_types.append('Song') + if self.shuffle_mapcompass != 'keysanity': + local_types += ['Map', 'Compass'] + if self.shuffle_smallkeys != 'keysanity': + local_types.append('SmallKey') + if self.shuffle_fortresskeys != 'keysanity': + local_types.append('HideoutSmallKey') + if self.shuffle_bosskeys != 'keysanity': + 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) + + # If any songs are itemlinked, set songs_as_items + for group in self.multiworld.groups.values(): + if self.songs_as_items or group['game'] != self.game or self.player not in group['players']: + continue + for item_name in group['item_pool']: + if oot_is_item_of_type(item_name, 'Song'): + self.songs_as_items = True + break + # 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'] @@ -221,6 +244,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[self.player].randomized # Hint stuff self.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text @@ -501,7 +526,7 @@ class OOTWorld(World): else: self.starting_items[item.name] += 1 if item.type == 'Song': - self.starting_songs = True + self.songs_as_items = True # Call the junk fill and get a replacement if item in self.itempool: self.itempool.remove(item) @@ -595,24 +620,6 @@ class OOTWorld(World): def pre_fill(self): - # 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 get_names(items): for item in items: yield item.name @@ -740,21 +747,23 @@ class OOTWorld(World): # Place shop items # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items if self.shopsanity != 'off': - shop_items = list( - filter(lambda item: item.player == self.player and item.type == 'Shop', self.multiworld.itempool)) + shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop' + and item.advancement, self.multiworld.itempool)) + shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop' + and not item.advancement, self.multiworld.itempool)) shop_locations = list( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.multiworld.get_unfilled_locations(player=self.player))) - shop_items.sort(key=lambda item: { - 'Buy Deku Shield': 3 * int(self.open_forest == 'closed'), - 'Buy Goron Tunic': 2, - 'Buy Zora Tunic': 2 - }.get(item.name, - int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk + shop_prog.sort(key=lambda item: { + 'Buy Deku Shield': 2 * int(self.open_forest == 'closed'), + '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) - for item in shop_items: + for item in shop_prog + shop_junk: self.multiworld.itempool.remove(item) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_items, True, True) + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True) + fast_fill(self.multiworld, shop_junk, shop_locations) set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it. @@ -801,8 +810,83 @@ class OOTWorld(World): loc.address = None # Handle item-linked dungeon items and songs - def stage_pre_fill(cls): - pass + @classmethod + def stage_pre_fill(cls, multiworld: MultiWorld): + + def gather_locations(item_type: str, players: AbstractSet[int], dungeon: str = '') -> Optional[List[OOTLocation]]: + type_to_setting = { + 'Song': 'shuffle_song_items', + 'Map': 'shuffle_mapcompass', + 'Compass': 'shuffle_mapcompass', + 'SmallKey': 'shuffle_smallkeys', + 'BossKey': 'shuffle_bosskeys', + 'HideoutSmallKey': 'shuffle_fortresskeys', + 'GanonBossKey': 'shuffle_ganon_bosskey', + } + fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players} + locations = [] + if item_type == 'Song': + if any(map(lambda v: v == 'any', fill_opts.values())): + return None + for player, option in fill_opts.items(): + if option == 'song': + condition = lambda location: location.type == 'Song' + elif option == 'dungeon': + condition = lambda location: location.name in dungeon_song_locations + locations += filter(condition, multiworld.get_unfilled_locations(player=player)) + else: + if any(map(lambda v: v == 'keysanity', fill_opts.values())): + return None + for player, option in fill_opts.items(): + if option == 'dungeon': + condition = lambda location: getattr(location.parent_region.dungeon, 'name', None) == dungeon + elif option == 'overworld': + condition = lambda location: location.parent_region.dungeon is None + elif option == 'any_dungeon': + condition = lambda location: location.parent_region.dungeon is not None + locations += filter(condition, multiworld.get_unfilled_locations(player=player)) + + return locations + + special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] + for group_id, group in multiworld.groups.items(): + if group['game'] != cls.game: + continue + group_items = [item for item in multiworld.itempool if item.player == group_id] + for fill_stage in special_fill_types: + group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items)) + if not group_stage_items: + continue + if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']: + # No need to subdivide by dungeon name + locations = gather_locations(fill_stage, group['players']) + if isinstance(locations, list): + for item in group_stage_items: + multiworld.itempool.remove(item) + multiworld.random.shuffle(locations) + fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, + single_player_placement=False, lock=True) + if fill_stage == 'Song': + # We don't want song locations to contain progression unless it's a song + # or it was marked as priority. + # We do this manually because we'd otherwise have to either + # iterate twice or do many function calls. + for loc in locations: + if loc.progress_type == LocationProgressType.DEFAULT: + loc.progress_type = LocationProgressType.EXCLUDED + add_item_rule(loc, lambda i: not (i.advancement or i.useful)) + else: + # Perform the fill task once per dungeon + for dungeon_info in dungeon_table: + dungeon_name = dungeon_info['name'] + locations = gather_locations(fill_stage, group['players'], dungeon=dungeon_name) + if isinstance(locations, list): + group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items)) + for item in group_dungeon_items: + multiworld.itempool.remove(item) + multiworld.random.shuffle(locations) + fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, + single_player_placement=False, lock=True) def generate_output(self, output_directory: str): if self.hints != 'none': @@ -855,9 +939,9 @@ class OOTWorld(World): # 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): + def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): def hint_type_players(hint_type: str) -> set: - return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time") + return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} try: @@ -868,27 +952,27 @@ class OOTWorld(World): items_by_region = {} for player in barren_hint_players: items_by_region[player] = {} - for r in world.worlds[player].regions: + for r in multiworld.worlds[player].regions: items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True} - for d in world.worlds[player].dungeons: + for d in multiworld.worlds[player].dungeons: items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True} del (items_by_region[player]["Link's Pocket"]) del (items_by_region[player][None]) if item_hint_players: # loop once over all locations to gather major items. Check oot locations for barren/woth if needed - for loc in world.get_locations(): + for loc in multiworld.get_locations(): player = loc.item.player - autoworld = world.worlds[player] + autoworld = multiworld.worlds[player] if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item'])) - or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])): + or (loc.player in item_hint_players and loc.name in multiworld.worlds[loc.player].added_hint_types['item'])): autoworld.major_item_locations.append(loc) if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or (oot_is_item_of_type(loc.item, 'Song') or - (oot_is_item_of_type(loc.item, 'SmallKey') and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'BossKey') and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'GanonBossKey') and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): + (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): if loc.player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[loc.player][hint_area]['weight'] += 1 @@ -896,35 +980,38 @@ class OOTWorld(World): items_by_region[loc.player][hint_area]['is_barren'] = False if loc.player in woth_hint_players and loc.item.advancement: # Skip item at location and see if game is still beatable - state = CollectionState(world) + state = CollectionState(multiworld) state.locations_checked.add(loc) - if not world.can_beat_game(state): - world.worlds[loc.player].required_locations.append(loc) + if not multiworld.can_beat_game(state): + multiworld.worlds[loc.player].required_locations.append(loc) elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth for player in (barren_hint_players | woth_hint_players): - for loc in world.worlds[player].get_locations(): + for loc in multiworld.worlds[player].get_locations(): if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): if player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[player][hint_area]['weight'] += 1 - if loc.item.advancement: + if loc.item.advancement or loc.item.useful: items_by_region[player][hint_area]['is_barren'] = False if player in woth_hint_players and loc.item.advancement: - state = CollectionState(world) + state = CollectionState(multiworld) state.locations_checked.add(loc) - if not world.can_beat_game(state): - world.worlds[player].required_locations.append(loc) + if not multiworld.can_beat_game(state): + multiworld.worlds[player].required_locations.append(loc) for player in barren_hint_players: - world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() + multiworld.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']} except Exception as e: raise e finally: - for autoworld in world.get_game_worlds("Ocarina of Time"): + for autoworld in multiworld.get_game_worlds("Ocarina of Time"): autoworld.hint_data_available.set() def modify_multidata(self, multidata: dict): + # Replace connect name + multidata['connect_names'][self.connect_name] = multidata['connect_names'][self.multiworld.player_name[self.player]] + hint_entrances = set() for entrance in entrance_shuffle_table: hint_entrances.add(entrance[1][0])