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):
|
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
|
# 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:
|
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
|
||||||
await ctx.update_death_link(True)
|
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)
|
return scene_check(scene_offset, bit_to_check, 0x10)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Why is there an extra offset of 3 for temp context checks? Who knows.
|
||||||
local cow_check = function(scene_offset, bit_to_check)
|
local cow_check = function(scene_offset, bit_to_check)
|
||||||
return scene_check(scene_offset, bit_to_check, 0xC)
|
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
|
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)
|
local great_fairy_magic_check = function(scene_offset, bit_to_check)
|
||||||
return scene_check(scene_offset, bit_to_check, 0x4)
|
return scene_check(scene_offset, bit_to_check, 0x4)
|
||||||
or check_temp_context({scene_offset, 0x05, bit_to_check})
|
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})
|
or check_temp_context({scene_offset, 0x00, 0x16})
|
||||||
end
|
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
|
--Helper method to resolve skulltula lookup location
|
||||||
local function skulltula_scene_to_array_index(i)
|
local function skulltula_scene_to_array_index(i)
|
||||||
return (i + 3) - 2 * (i % 4)
|
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 Freestanding PoH"] = on_the_ground_check(0x60, 0x1E)
|
||||||
checks["DMT Chest"] = chest_check(0x60, 0x01)
|
checks["DMT Chest"] = chest_check(0x60, 0x01)
|
||||||
checks["DMT Storms Grotto Chest"] = chest_check(0x3E, 0x17)
|
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 Biggoron"] = big_goron_sword_check()
|
||||||
checks["DMT Cow Grotto Cow"] = cow_check(0x3E, 0x18)
|
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 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 Child"] = info_table_check(0x22, 0x6)
|
||||||
checks["GC Rolling Goron as Adult"] = info_table_check(0x20, 0x1)
|
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 Left Chest"] = chest_check(0x62, 0x00)
|
||||||
checks["GC Maze Right Chest"] = chest_check(0x62, 0x01)
|
checks["GC Maze Right Chest"] = chest_check(0x62, 0x01)
|
||||||
checks["GC Maze Center Chest"] = chest_check(0x62, 0x02)
|
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 Volcano Freestanding PoH"] = on_the_ground_check(0x61, 0x08)
|
||||||
checks["DMC Wall Freestanding PoH"] = on_the_ground_check(0x61, 0x02)
|
checks["DMC Wall Freestanding PoH"] = on_the_ground_check(0x61, 0x02)
|
||||||
checks["DMC Upper Grotto Chest"] = chest_check(0x3E, 0x1A)
|
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"] = scrub_sanity_check(0x61, 0x6)
|
||||||
checks["DMC Deku Scrub Grotto Left"] = scrub_sanity_check(0x23, 0x1)
|
checks["DMC Deku Scrub Grotto Left"] = scrub_sanity_check(0x23, 0x1)
|
||||||
|
@ -961,7 +974,7 @@ end
|
||||||
|
|
||||||
local read_haunted_wasteland_checks = function()
|
local read_haunted_wasteland_checks = function()
|
||||||
local checks = {}
|
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 Chest"] = chest_check(0x5E, 0x00)
|
||||||
checks["Wasteland GS"] = skulltula_check(0x15, 0x1)
|
checks["Wasteland GS"] = skulltula_check(0x15, 0x1)
|
||||||
return checks
|
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]):
|
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')
|
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
|
# Check if all locations are reachable if not NL
|
||||||
if locations_to_ensure_reachable:
|
if ootworld.logic_rules != 'no_logic' and locations_to_ensure_reachable:
|
||||||
for loc in locations_to_ensure_reachable:
|
for loc in locations_to_ensure_reachable:
|
||||||
if not all_state.can_reach(loc, 'Location', player):
|
if not all_state.can_reach(loc, 'Location', player):
|
||||||
raise EntranceShuffleError(f'{loc} is unreachable')
|
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')
|
raise EntranceShuffleError('Goron City Shop not accessible as adult')
|
||||||
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
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')
|
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
|
# Add trial hints, only if hint copies > 0
|
||||||
if hint_dist['trial'][1] > 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)
|
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)
|
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:
|
elif world.trials < 6 and world.trials > 3:
|
||||||
for trial,skipped in world.skipped_trials.items():
|
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'
|
placed_items['Hideout Gerudo Membership Card'] = 'Ice Trap'
|
||||||
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
|
skip_in_spoiler_locations.append('Hideout Gerudo Membership Card')
|
||||||
else:
|
else:
|
||||||
|
card = world.create_item('Gerudo Membership Card')
|
||||||
|
world.multiworld.push_precollected(card)
|
||||||
placed_items['Hideout Gerudo Membership Card'] = 'Gerudo Membership 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':
|
if world.shuffle_gerudo_card and world.item_pool_value == 'plentiful':
|
||||||
pending_junk_pool.append('Gerudo Membership Card')
|
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):
|
def oot_is_item_of_type(item, item_type):
|
||||||
if not isinstance(item, OOTItem):
|
if isinstance(item, OOTItem):
|
||||||
return False
|
return item.type == item_type
|
||||||
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):
|
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)],
|
'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):
|
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']
|
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 typing
|
||||||
|
import random
|
||||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink
|
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink
|
||||||
from .LogicTricks import normalized_name_tricks
|
from .LogicTricks import normalized_name_tricks
|
||||||
from .ColorSFXOptions import *
|
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):
|
class Logic(Choice):
|
||||||
"""Set the logic used for the generator."""
|
"""Set the logic used for the generator."""
|
||||||
display_name = "Logic Rules"
|
display_name = "Logic Rules"
|
||||||
|
@ -70,7 +95,7 @@ class Bridge(Choice):
|
||||||
default = 3
|
default = 3
|
||||||
|
|
||||||
|
|
||||||
class Trials(Range):
|
class Trials(TrackRandomRange):
|
||||||
"""Set the number of required trials in Ganon's Castle."""
|
"""Set the number of required trials in Ganon's Castle."""
|
||||||
display_name = "Ganon's Trials Count"
|
display_name = "Ganon's Trials Count"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
|
@ -173,7 +198,7 @@ class LogicalChus(Toggle):
|
||||||
display_name = "Bombchus Considered in Logic"
|
display_name = "Bombchus Considered in Logic"
|
||||||
|
|
||||||
|
|
||||||
class MQDungeons(Range):
|
class MQDungeons(TrackRandomRange):
|
||||||
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
|
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
|
||||||
display_name = "Number of MQ Dungeons"
|
display_name = "Number of MQ Dungeons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
|
|
|
@ -168,7 +168,7 @@ def patch_rom(world, rom):
|
||||||
rom.write_bytes(0x1FC0CF8, Block_code)
|
rom.write_bytes(0x1FC0CF8, Block_code)
|
||||||
|
|
||||||
# songs as items flag
|
# 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:
|
if songs_as_items:
|
||||||
rom.write_byte(rom.sym('SONGS_AS_ITEMS'), 1)
|
rom.write_byte(rom.sym('SONGS_AS_ITEMS'), 1)
|
||||||
|
@ -1326,7 +1326,7 @@ def patch_rom(world, rom):
|
||||||
override_table = get_override_table(world)
|
override_table = get_override_table(world)
|
||||||
rom.write_bytes(rom.sym('cfg_item_overrides'), get_override_table_bytes(override_table))
|
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_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:
|
if world.death_link:
|
||||||
rom.write_byte(rom.sym('DEATH_LINK'), 0x01)
|
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)
|
location = world.get_location('Forest Temple MQ First Room Chest', player)
|
||||||
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.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.
|
# 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.
|
# This is required if map/compass included, or any_dungeon shuffle.
|
||||||
location = world.get_location('Sheik in Ice Cavern', player)
|
location = world.get_location('Sheik in Ice Cavern', player)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import copy
|
import copy
|
||||||
|
from typing import Optional, List, AbstractSet # remove when 3.8 support is dropped
|
||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
|
from string import printable
|
||||||
|
|
||||||
logger = logging.getLogger("Ocarina of Time")
|
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 .RuleParser import Rule_AST_Transformer
|
||||||
from .Options import oot_options
|
from .Options import oot_options
|
||||||
from .Utils import data_path, read_json
|
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 .DungeonList import dungeon_table, create_dungeons
|
||||||
from .LogicTricks import normalized_name_tricks
|
from .LogicTricks import normalized_name_tricks
|
||||||
from .Rom import Rom
|
from .Rom import Rom
|
||||||
|
@ -27,10 +29,10 @@ from .HintList import getRequiredHints
|
||||||
from .SaveContext import SaveContext
|
from .SaveContext import SaveContext
|
||||||
|
|
||||||
from Utils import get_options, output_path
|
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 Options import Range, Toggle, OptionList
|
||||||
from Fill import fill_restrictive, FillError
|
from Fill import fill_restrictive, fast_fill, FillError
|
||||||
from worlds.generic.Rules import exclusion_rules
|
from worlds.generic.Rules import exclusion_rules, add_item_rule
|
||||||
from ..AutoWorld import World, AutoLogicRegister, WebWorld
|
from ..AutoWorld import World, AutoLogicRegister, WebWorld
|
||||||
|
|
||||||
location_id_offset = 67000
|
location_id_offset = 67000
|
||||||
|
@ -117,11 +119,6 @@ class OOTWorld(World):
|
||||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
||||||
|
|
||||||
def generate_early(self):
|
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)
|
self.parser = Rule_AST_Transformer(self, self.player)
|
||||||
|
|
||||||
for (option_name, option) in oot_options.items():
|
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.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.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
|
||||||
self.starting_items = Counter()
|
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.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 = {
|
self.item_name_groups = {
|
||||||
"medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion",
|
"medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion",
|
||||||
|
@ -182,6 +180,31 @@ class OOTWorld(World):
|
||||||
if self.triforce_hunt:
|
if self.triforce_hunt:
|
||||||
self.shuffle_ganon_bosskey = 'remove'
|
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
|
# Determine skipped trials in GT
|
||||||
# This needs to be done before the logic rules in GT are parsed
|
# This needs to be done before the logic rules in GT are parsed
|
||||||
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
|
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
|
||||||
|
@ -221,6 +244,8 @@ class OOTWorld(World):
|
||||||
|
|
||||||
# Set internal names used by the OoT generator
|
# Set internal names used by the OoT generator
|
||||||
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
|
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
|
# Hint stuff
|
||||||
self.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text
|
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:
|
else:
|
||||||
self.starting_items[item.name] += 1
|
self.starting_items[item.name] += 1
|
||||||
if item.type == 'Song':
|
if item.type == 'Song':
|
||||||
self.starting_songs = True
|
self.songs_as_items = True
|
||||||
# Call the junk fill and get a replacement
|
# Call the junk fill and get a replacement
|
||||||
if item in self.itempool:
|
if item in self.itempool:
|
||||||
self.itempool.remove(item)
|
self.itempool.remove(item)
|
||||||
|
@ -595,24 +620,6 @@ class OOTWorld(World):
|
||||||
|
|
||||||
def pre_fill(self):
|
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):
|
def get_names(items):
|
||||||
for item in items:
|
for item in items:
|
||||||
yield item.name
|
yield item.name
|
||||||
|
@ -740,21 +747,23 @@ class OOTWorld(World):
|
||||||
# Place shop items
|
# 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
|
# 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':
|
if self.shopsanity != 'off':
|
||||||
shop_items = list(
|
shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
|
||||||
filter(lambda item: item.player == self.player and item.type == 'Shop', self.multiworld.itempool))
|
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(
|
shop_locations = list(
|
||||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||||
self.multiworld.get_unfilled_locations(player=self.player)))
|
self.multiworld.get_unfilled_locations(player=self.player)))
|
||||||
shop_items.sort(key=lambda item: {
|
shop_prog.sort(key=lambda item: {
|
||||||
'Buy Deku Shield': 3 * int(self.open_forest == 'closed'),
|
'Buy Deku Shield': 2 * int(self.open_forest == 'closed'),
|
||||||
'Buy Goron Tunic': 2,
|
'Buy Goron Tunic': 1,
|
||||||
'Buy Zora Tunic': 2
|
'Buy Zora Tunic': 1,
|
||||||
}.get(item.name,
|
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
|
||||||
int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
|
|
||||||
self.multiworld.random.shuffle(shop_locations)
|
self.multiworld.random.shuffle(shop_locations)
|
||||||
for item in shop_items:
|
for item in shop_prog + shop_junk:
|
||||||
self.multiworld.itempool.remove(item)
|
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
|
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.
|
# 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
|
loc.address = None
|
||||||
|
|
||||||
# Handle item-linked dungeon items and songs
|
# Handle item-linked dungeon items and songs
|
||||||
def stage_pre_fill(cls):
|
@classmethod
|
||||||
pass
|
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):
|
def generate_output(self, output_directory: str):
|
||||||
if self.hints != 'none':
|
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.
|
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
|
||||||
@classmethod
|
@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:
|
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}
|
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -868,27 +952,27 @@ class OOTWorld(World):
|
||||||
items_by_region = {}
|
items_by_region = {}
|
||||||
for player in barren_hint_players:
|
for player in barren_hint_players:
|
||||||
items_by_region[player] = {}
|
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}
|
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}
|
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]["Link's Pocket"])
|
||||||
del (items_by_region[player][None])
|
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
|
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
|
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']))
|
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)
|
autoworld.major_item_locations.append(loc)
|
||||||
|
|
||||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
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, '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, 'SmallKey') and multiworld.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, 'HideoutSmallKey') and multiworld.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, 'BossKey') and multiworld.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, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
|
||||||
if loc.player in barren_hint_players:
|
if loc.player in barren_hint_players:
|
||||||
hint_area = get_hint_area(loc)
|
hint_area = get_hint_area(loc)
|
||||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
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
|
items_by_region[loc.player][hint_area]['is_barren'] = False
|
||||||
if loc.player in woth_hint_players and loc.item.advancement:
|
if loc.player in woth_hint_players and loc.item.advancement:
|
||||||
# Skip item at location and see if game is still beatable
|
# Skip item at location and see if game is still beatable
|
||||||
state = CollectionState(world)
|
state = CollectionState(multiworld)
|
||||||
state.locations_checked.add(loc)
|
state.locations_checked.add(loc)
|
||||||
if not world.can_beat_game(state):
|
if not multiworld.can_beat_game(state):
|
||||||
world.worlds[loc.player].required_locations.append(loc)
|
multiworld.worlds[loc.player].required_locations.append(loc)
|
||||||
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
|
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 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 loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
|
||||||
if player in barren_hint_players:
|
if player in barren_hint_players:
|
||||||
hint_area = get_hint_area(loc)
|
hint_area = get_hint_area(loc)
|
||||||
items_by_region[player][hint_area]['weight'] += 1
|
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
|
items_by_region[player][hint_area]['is_barren'] = False
|
||||||
if player in woth_hint_players and loc.item.advancement:
|
if player in woth_hint_players and loc.item.advancement:
|
||||||
state = CollectionState(world)
|
state = CollectionState(multiworld)
|
||||||
state.locations_checked.add(loc)
|
state.locations_checked.add(loc)
|
||||||
if not world.can_beat_game(state):
|
if not multiworld.can_beat_game(state):
|
||||||
world.worlds[player].required_locations.append(loc)
|
multiworld.worlds[player].required_locations.append(loc)
|
||||||
for player in barren_hint_players:
|
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']}
|
if info['is_barren']}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
finally:
|
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()
|
autoworld.hint_data_available.set()
|
||||||
|
|
||||||
def modify_multidata(self, multidata: dict):
|
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()
|
hint_entrances = set()
|
||||||
for entrance in entrance_shuffle_table:
|
for entrance in entrance_shuffle_table:
|
||||||
hint_entrances.add(entrance[1][0])
|
hint_entrances.add(entrance[1][0])
|
||||||
|
|
Loading…
Reference in New Issue