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
This commit is contained in:
espeon65536 2022-11-02 01:32:08 -07:00 committed by GitHub
parent 95378233fc
commit a6e1e14fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 245 additions and 80 deletions

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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():

View File

@ -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')

View File

@ -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):

View File

@ -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']

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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])