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:
parent
95378233fc
commit
a6e1e14fee
13
OoTClient.py
13
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Reference in New Issue