Core: change Region caching to on_change from on-miss-strategy (#2366)

This commit is contained in:
Fabian Dill 2023-10-29 19:47:37 +01:00 committed by GitHub
parent d9b076a687
commit 3e0d1d4e1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 265 additions and 202 deletions

View File

@ -1,14 +1,15 @@
from __future__ import annotations
import copy
import itertools
import functools
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import ChainMap, Counter, deque
from collections.abc import Collection
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
@ -47,7 +48,6 @@ class ThreadBarrierProxy:
class MultiWorld():
debug_types = False
player_name: Dict[int, str]
_region_cache: Dict[int, Dict[str, Region]]
difficulty_requirements: dict
required_medallions: dict
dark_room_logic: Dict[int, str]
@ -57,7 +57,7 @@ class MultiWorld():
plando_connections: List
worlds: Dict[int, auto_world]
groups: Dict[int, Group]
regions: List[Region]
regions: RegionManager
itempool: List[Item]
is_race: bool = False
precollected_items: Dict[int, List[Item]]
@ -92,6 +92,34 @@ class MultiWorld():
def __getitem__(self, player) -> bool:
return self.rule(player)
class RegionManager:
region_cache: Dict[int, Dict[str, Region]]
entrance_cache: Dict[int, Dict[str, Entrance]]
location_cache: Dict[int, Dict[str, Location]]
def __init__(self, players: int):
self.region_cache = {player: {} for player in range(1, players+1)}
self.entrance_cache = {player: {} for player in range(1, players+1)}
self.location_cache = {player: {} for player in range(1, players+1)}
def __iadd__(self, other: Iterable[Region]):
self.extend(other)
return self
def append(self, region: Region):
self.region_cache[region.player][region.name] = region
def extend(self, regions: Iterable[Region]):
for region in regions:
self.region_cache[region.player][region.name] = region
def __iter__(self) -> Iterator[Region]:
for regions in self.region_cache.values():
yield from regions.values()
def __len__(self):
return sum(len(regions) for regions in self.region_cache.values())
def __init__(self, players: int):
# world-local random state is saved for multiple generations running concurrently
self.random = ThreadBarrierProxy(random.Random())
@ -100,16 +128,12 @@ class MultiWorld():
self.glitch_triforce = False
self.algorithm = 'balanced'
self.groups = {}
self.regions = []
self.regions = self.RegionManager(players)
self.shops = []
self.itempool = []
self.seed = None
self.seed_name: str = "Unavailable"
self.precollected_items = {player: [] for player in self.player_ids}
self._cached_entrances = None
self._cached_locations = None
self._entrance_cache = {}
self._location_cache: Dict[Tuple[str, int], Location] = {}
self.required_locations = []
self.light_world_light_cone = False
self.dark_world_light_cone = False
@ -137,7 +161,6 @@ class MultiWorld():
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {})
set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open')
@ -199,7 +222,6 @@ class MultiWorld():
self.game[new_id] = game
self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players)
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
@ -333,41 +355,17 @@ class MultiWorld():
def world_name_lookup(self):
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
def _recache(self):
"""Rebuild world cache"""
self._cached_locations = None
for region in self.regions:
player = region.player
self._region_cache[player][region.name] = region
for exit in region.exits:
self._entrance_cache[exit.name, player] = exit
for r_location in region.locations:
self._location_cache[r_location.name, player] = r_location
def get_regions(self, player: Optional[int] = None) -> Collection[Region]:
return self.regions if player is None else self._region_cache[player].values()
return self.regions if player is None else self.regions.region_cache[player].values()
def get_region(self, regionname: str, player: int) -> Region:
try:
return self._region_cache[player][regionname]
except KeyError:
self._recache()
return self._region_cache[player][regionname]
def get_region(self, region_name: str, player: int) -> Region:
return self.regions.region_cache[player][region_name]
def get_entrance(self, entrance: str, player: int) -> Entrance:
try:
return self._entrance_cache[entrance, player]
except KeyError:
self._recache()
return self._entrance_cache[entrance, player]
def get_entrance(self, entrance_name: str, player: int) -> Entrance:
return self.regions.entrance_cache[player][entrance_name]
def get_location(self, location: str, player: int) -> Location:
try:
return self._location_cache[location, player]
except KeyError:
self._recache()
return self._location_cache[location, player]
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
@ -428,28 +426,22 @@ class MultiWorld():
logging.debug('Placed %s at %s', item, location)
def get_entrances(self) -> List[Entrance]:
if self._cached_entrances is None:
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
return self._cached_entrances
def clear_entrance_cache(self):
self._cached_entrances = None
def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]:
if player is not None:
return self.regions.entrance_cache[player].values()
return Utils.RepeatableChain(tuple(self.regions.entrance_cache[player].values()
for player in self.regions.entrance_cache))
def register_indirect_condition(self, region: Region, entrance: Entrance):
"""Report that access to this Region can result in unlocking this Entrance,
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
self.indirect_connections.setdefault(region, set()).add(entrance)
def get_locations(self, player: Optional[int] = None) -> List[Location]:
if self._cached_locations is None:
self._cached_locations = [location for region in self.regions for location in region.locations]
def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
if player is not None:
return [location for location in self._cached_locations if location.player == player]
return self._cached_locations
def clear_location_cache(self):
self._cached_locations = None
return self.regions.location_cache[player].values()
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
for player in self.regions.location_cache))
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
return [location for location in self.get_locations(player) if location.item is None]
@ -471,16 +463,17 @@ class MultiWorld():
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
else:
valid_locations = location_names
relevant_cache = self.regions.location_cache[player]
for location_name in valid_locations:
location = self._location_cache.get((location_name, player), None)
if location is not None and location.item is None:
location = relevant_cache.get(location_name, None)
if location and location.item is None:
yield location
def unlocks_new_location(self, item: Item) -> bool:
temp_state = self.state.copy()
temp_state.collect(item, True)
for location in self.get_unfilled_locations():
for location in self.get_unfilled_locations(item.player):
if temp_state.can_reach(location) and not self.state.can_reach(location):
return True
@ -820,15 +813,83 @@ class Region:
locations: List[Location]
entrance_type: ClassVar[Type[Entrance]] = Entrance
class Register(MutableSequence):
region_manager: MultiWorld.RegionManager
def __init__(self, region_manager: MultiWorld.RegionManager):
self._list = []
self.region_manager = region_manager
def __getitem__(self, index: int) -> Location:
return self._list.__getitem__(index)
def __setitem__(self, index: int, value: Location) -> None:
raise NotImplementedError()
def __len__(self) -> int:
return self._list.__len__()
# This seems to not be needed, but that's a bit suspicious.
# def __del__(self):
# self.clear()
def copy(self):
return self._list.copy()
class LocationRegister(Register):
def __delitem__(self, index: int) -> None:
location: Location = self._list.__getitem__(index)
self._list.__delitem__(index)
del(self.region_manager.location_cache[location.player][location.name])
def insert(self, index: int, value: Location) -> None:
self._list.insert(index, value)
self.region_manager.location_cache[value.player][value.name] = value
class EntranceRegister(Register):
def __delitem__(self, index: int) -> None:
entrance: Entrance = self._list.__getitem__(index)
self._list.__delitem__(index)
del(self.region_manager.entrance_cache[entrance.player][entrance.name])
def insert(self, index: int, value: Entrance) -> None:
self._list.insert(index, value)
self.region_manager.entrance_cache[value.player][value.name] = value
_locations: LocationRegister[Location]
_exits: EntranceRegister[Entrance]
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
self.entrances = []
self.exits = []
self.locations = []
self._exits = self.EntranceRegister(multiworld.regions)
self._locations = self.LocationRegister(multiworld.regions)
self.multiworld = multiworld
self._hint_text = hint
self.player = player
def get_locations(self):
return self._locations
def set_locations(self, new):
if new is self._locations:
return
self._locations.clear()
self._locations.extend(new)
locations = property(get_locations, set_locations)
def get_exits(self):
return self._exits
def set_exits(self, new):
if new is self._exits:
return
self._exits.clear()
self._exits.extend(new)
exits = property(get_exits, set_exits)
def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]:
state.update_reachable_regions(self.player)

View File

@ -122,10 +122,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()
logger.info('Calculating Access Rules.')
for player in world.player_ids:
@ -233,7 +229,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region)
locations = region.locations = []
locations = region.locations
for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
@ -267,7 +263,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
if any(world.item_links.values()):
world._recache()
world._all_state = None
logger.info("Running Item Plando")

View File

@ -5,6 +5,7 @@ import json
import typing
import builtins
import os
import itertools
import subprocess
import sys
import pickle
@ -903,3 +904,17 @@ def visualize_regions(root_region: Region, file_name: str, *,
with open(file_name, "wt", encoding="utf-8") as f:
f.write("\n".join(uml))
class RepeatableChain:
def __init__(self, iterable: typing.Iterable):
self.iterable = iterable
def __iter__(self):
return itertools.chain.from_iterable(self.iterable)
def __bool__(self):
return any(sub_iterable for sub_iterable in self.iterable)
def __len__(self):
return sum(len(iterable) for iterable in self.iterable)

View File

@ -284,7 +284,7 @@ class WorldTestBase(unittest.TestCase):
# basically a shortened reimplementation of this method from core, in order to force the check is done
def fulfills_accessibility() -> bool:
locations = self.multiworld.get_locations(1).copy()
locations = list(self.multiworld.get_locations(1))
state = CollectionState(self.multiworld)
while locations:
sphere: typing.List[Location] = []

View File

@ -36,7 +36,6 @@ class TestBase(unittest.TestCase):
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
multiworld._recache()
region_count = len(multiworld.get_regions())
location_count = len(multiworld.get_locations())
@ -46,14 +45,12 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
multiworld._recache()
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during generate_basic")
multiworld._recache()
call_all(multiworld, "pre_fill")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during pre_fill")

View File

@ -293,7 +293,6 @@ def generate_itempool(world):
loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc)
multiworld.clear_location_cache()
multiworld.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True

View File

@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch items
for location in world.get_locations():
if location.player != player or location.address is None or location.shop_slot is not None:
for location in world.get_locations(player):
if location.address is None or location.shop_slot is not None:
continue
itemid = location.item.code if location.item is not None else 0x5A
@ -2247,7 +2247,7 @@ def write_strings(rom, world, player):
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations)
all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player]
all_entrances = list(world.get_entrances(player))
local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.

View File

@ -197,8 +197,13 @@ def global_rules(world, player):
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player))
try:
old_man_sq = world.get_entrance('Old Man S&Q', player)
except KeyError:
pass # it doesn't exist, should be dungeon-only unittests
else:
old_man = world.get_location("Old Man", player)
set_rule(old_man_sq, lambda state: old_man.can_reach(state))
set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
@ -1526,16 +1531,16 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# Helper functions to determine if the moon pearl is required
if inverted:
def is_bunny(region):
return region.is_light_world
return region and region.is_light_world
def is_link(region):
return region.is_dark_world
return region and region.is_dark_world
else:
def is_bunny(region):
return region.is_dark_world
return region and region.is_dark_world
def is_link(region):
return region.is_light_world
return region and region.is_light_world
def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or
@ -1603,21 +1608,20 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
return options_to_access_rule(possible_options)
# Add requirements for bunny-impassible caves if link is a bunny in them
for region in [world.get_region(name, player) for name in bunny_impassable_caves]:
for region in (world.get_region(name, player) for name in bunny_impassable_caves):
if not is_bunny(region):
continue
rule = get_rule_to_add(region)
for exit in region.exits:
add_rule(exit, rule)
for region_exit in region.exits:
add_rule(region_exit, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if is_bunny(paradox_shop):
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
for entrance in world.get_entrances():
if entrance.player == player and is_bunny(entrance.connected_region):
for entrance in world.get_entrances(player):
if is_bunny(entrance.connected_region):
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():

View File

@ -348,7 +348,6 @@ def create_shops(world, player: int):
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
loc.shop_slot_disabled = True
shop.region.locations.append(loc)
world.clear_location_cache()
class ShopData(NamedTuple):
@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player):
if shop.type == ShopType.TakeAny:
loc.shop_slot_disabled = True
shop.region.locations.append(loc)
world.clear_location_cache()
loc.shop_slot = i

View File

@ -585,27 +585,26 @@ class ALTTPWorld(World):
for player in checks_in_area:
checks_in_area[player]["Total"] = 0
for location in multiworld.get_locations():
if location.game == cls.game and type(location.address) is int:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
else:
assert False, "Unknown Location area."
# TODO: remove Total as it's duplicated data and breaks consistent typing
checks_in_area[location.player]["Total"] += 1
for location in multiworld.get_locations(player):
if location.game == cls.game and type(location.address) is int:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
else:
assert False, "Unknown Location area."
# TODO: remove Total as it's duplicated data and breaks consistent typing
checks_in_area[location.player]["Total"] += 1
multidata["checks_in_area"].update(checks_in_area)

View File

@ -1,5 +1,5 @@
from BaseClasses import CollectionState, ItemClassification
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory

View File

@ -69,8 +69,8 @@ class ChecksFinderWorld(World):
def create_regions(self):
menu = Region("Menu", self.player, self.multiworld)
board = Region("Board", self.player, self.multiworld)
board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
connection = Entrance(self.player, "New Board", menu)
menu.exits.append(connection)

View File

@ -219,7 +219,7 @@ def create_regions_from_ladxr(player, multiworld, logic):
r = LinksAwakeningRegion(
name=name, ladxr_region=l, hint="", player=player, world=multiworld)
r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items]
r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items]
regions[l] = r
for ladxr_location in logic.location_list:

View File

@ -231,9 +231,7 @@ class LinksAwakeningWorld(World):
# Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False
for r in self.multiworld.get_regions():
if r.player != self.player:
continue
for r in self.multiworld.get_regions(self.player):
if r.dungeon_index != item.item_data.dungeon_index:
continue
for loc in r.locations:
@ -269,10 +267,7 @@ class LinksAwakeningWorld(World):
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
for r in self.multiworld.get_regions():
if r.player != self.player:
continue
for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations
if r.dungeon_index:
self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations

View File

@ -54,12 +54,12 @@ def create_regions(world: MultiWorld, player: int):
world.regions.append(boss_region)
region_final_boss = Region("Final Boss", player, world)
region_final_boss.locations = [MeritousLocation(
region_final_boss.locations += [MeritousLocation(
player, "Wervyn Anixil", None, region_final_boss)]
world.regions.append(region_final_boss)
region_tfb = Region("True Final Boss", player, world)
region_tfb.locations = [MeritousLocation(
region_tfb.locations += [MeritousLocation(
player, "Wervyn Anixil?", None, region_tfb)]
world.regions.append(region_tfb)

View File

@ -195,7 +195,8 @@ class OOTWorld(World):
setattr(self, option_name, option_value)
self.shop_prices = {}
self.regions = [] # internal cache of regions for this world, used later
self.regions = [] # internal caches of regions for this world, used later
self._regions_cache = {}
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
self.songs_as_items = False
@ -526,6 +527,10 @@ class OOTWorld(World):
# We still need to fill the location even if ALR is off.
logger.debug('Unreachable location: %s', new_location.name)
new_location.player = self.player
# Change some attributes of Drop locations
if new_location.type == 'Drop':
new_location.name = new_region.name + ' ' + new_location.name
new_location.show_in_spoiler = False
new_region.locations.append(new_location)
if 'events' in region:
for event, rule in region['events'].items():
@ -555,7 +560,8 @@ class OOTWorld(World):
self.multiworld.regions.append(new_region)
self.regions.append(new_region)
self.multiworld._recache()
self._regions_cache[new_region.name] = new_region
# self.multiworld._recache()
def set_scrub_prices(self):
# Get Deku Scrub Locations
@ -622,7 +628,7 @@ class OOTWorld(World):
'Twinrova',
'Links Pocket'
)
boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards']))
boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names]
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
@ -636,7 +642,6 @@ class OOTWorld(World):
item = prizepool.pop()
loc = prize_locs.pop()
loc.place_locked_item(item)
self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc
def create_item(self, name: str, allow_arbitrary_name: bool = False):
@ -671,7 +676,7 @@ class OOTWorld(World):
self.multiworld.regions.append(menu)
self.load_regions_from_json(overworld_data_path)
self.load_regions_from_json(bosses_data_path)
start.connect(self.multiworld.get_region('Root', self.player))
start.connect(self.get_region('Root'))
create_dungeons(self)
self.parser.create_delayed_rules()
@ -682,16 +687,11 @@ class OOTWorld(World):
# Bind entrances to vanilla
for region in self.regions:
for exit in region.exits:
exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player))
exit.connect(self.get_region(exit.vanilla_connected_region))
def create_items(self):
# Uniquely rename drop locations for each region and erase them from the spoiler
set_drop_location_names(self)
# Generate itempool
generate_itempool(self)
# Add dungeon rewards
rewardlist = sorted(list(self.item_name_groups['rewards']))
self.itempool += map(self.create_item, rewardlist)
junk_pool = get_junk_pool(self)
removed_items = []
@ -769,7 +769,7 @@ class OOTWorld(World):
# Kill unreachable events that can't be gotten even with all items
# Make sure to only kill actual internal events, not in-game "events"
all_state = self.multiworld.get_all_state(False)
all_state = self.multiworld.get_all_state(use_cache=True)
all_locations = self.get_locations()
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
@ -781,7 +781,6 @@ class OOTWorld(World):
bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player)
if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
bigpoe.parent_region.locations.remove(bigpoe)
self.multiworld.clear_location_cache()
# If fast scarecrow then we need to kill the Pierre location as it will be unreachable
if self.free_scarecrow:
@ -997,6 +996,7 @@ class OOTWorld(World):
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
single_player_placement=False, lock=True, allow_excluded=True)
def generate_output(self, output_directory: str):
if self.hints != 'none':
self.hint_data_available.wait()
@ -1032,30 +1032,6 @@ class OOTWorld(World):
player_name=self.multiworld.get_player_name(self.player))
apz5.write()
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(reverse=True, key=lambda x: x.name)
all_entrances.sort(reverse=True, key=lambda x: x.type)
if not self.decouple_entrances:
while all_entrances:
loadzone = all_entrances.pop()
if loadzone.type != 'Overworld':
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
else:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
else:
reverse = loadzone.replaces.reverse
if reverse in all_entrances:
all_entrances.remove(reverse)
self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
else:
for entrance in all_entrances:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod
@ -1135,6 +1111,7 @@ class OOTWorld(World):
for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
autoworld.hint_data_available.set()
def fill_slot_data(self):
self.collectible_flags_available.wait()
return {
@ -1142,6 +1119,7 @@ class OOTWorld(World):
'collectible_flag_offsets': self.collectible_flag_offsets
}
def modify_multidata(self, multidata: dict):
# Replace connect name
@ -1156,6 +1134,7 @@ class OOTWorld(World):
continue
multidata["precollected_items"][self.player].remove(item_id)
def extend_hint_information(self, er_hint_data: dict):
er_hint_data[self.player] = {}
@ -1202,6 +1181,7 @@ class OOTWorld(World):
er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
@ -1211,6 +1191,32 @@ class OOTWorld(World):
for k, v in self.shop_prices.items():
spoiler_handle.write(f"{k}: {v} Rupees\n")
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(reverse=True, key=lambda x: x.name)
all_entrances.sort(reverse=True, key=lambda x: x.type)
if not self.decouple_entrances:
while all_entrances:
loadzone = all_entrances.pop()
if loadzone.type != 'Overworld':
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
else:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
else:
reverse = loadzone.replaces.reverse
if reverse in all_entrances:
all_entrances.remove(reverse)
self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
else:
for entrance in all_entrances:
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Key ring handling:
# Key rings are multiple items glued together into one, so we need to give
# the appropriate number of keys in the collection state when they are
@ -1242,9 +1248,8 @@ class OOTWorld(World):
return False
def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and
(type == None or entrance.type == type) and
(not only_primary or entrance.primary))]
return [entrance for entrance in self.multiworld.get_entrances(self.player) if (
(type == None or entrance.type == type) and (not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
@ -1258,8 +1263,13 @@ class OOTWorld(World):
def get_location(self, location):
return self.multiworld.get_location(location, self.player)
def get_region(self, region):
return self.multiworld.get_region(region, self.player)
def get_region(self, region_name):
try:
return self._regions_cache[region_name]
except KeyError:
ret = self.multiworld.get_region(region_name, self.player)
self._regions_cache[region_name] = ret
return ret
def get_entrance(self, entrance):
return self.multiworld.get_entrance(entrance, self.player)

View File

@ -445,13 +445,9 @@ class PokemonRedBlueWorld(World):
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
evolutions_region = self.multiworld.get_region("Evolution", self.player)
clear_cache = False
for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
clear_cache = True
if clear_cache:
self.multiworld.clear_location_cache()
if self.multiworld.old_man[self.player] == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
@ -559,7 +555,6 @@ class PokemonRedBlueWorld(World):
else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
self.multiworld._recache()
if self.multiworld.door_shuffle[self.player] == "decoupled":
swept_state = self.multiworld.state.copy()

View File

@ -546,10 +546,8 @@ def generate_output(self, output_directory: str):
write_quizzes(self, data, random)
for location in self.multiworld.get_locations():
if location.player != self.player:
continue
elif location.party_data:
for location in self.multiworld.get_locations(self.player):
if location.party_data:
for party in location.party_data:
if not isinstance(party["party_address"], list):
addresses = [rom_addresses[party["party_address"]]]

View File

@ -96,8 +96,7 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
# a long enough run to have enough director credits for scavengers and
# help prevent being stuck in the same stages until that point.)
for location in multiworld.get_locations():
if location.player != player: continue # ignore all checks that don't belong to this player
for location in multiworld.get_locations(player):
if "Scavenger" in location.name:
add_rule(location, lambda state: state.has("Stage_5", player))
# Regions

View File

@ -294,7 +294,7 @@ class SMWorld(World):
for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions:
src_region = self.multiworld.get_region(src.Name, self.player)
dest_region = self.multiworld.get_region(dest.Name, self.player)
if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache):
if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]:
src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region))
srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player)
srcDestEntrance.connect(dest_region)
@ -563,8 +563,8 @@ class SMWorld(World):
multiWorldItems: List[ByteEdit] = []
idx = 0
vanillaItemTypesCount = 21
for itemLoc in self.multiworld.get_locations():
if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class:
for itemLoc in self.multiworld.get_locations(self.player):
if "Boss" not in locationsDict[itemLoc.name].Class:
SMZ3NameToSMType = {
"ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb",
"Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster",

View File

@ -417,7 +417,7 @@ class SoEWorld(World):
flags += option.to_flag()
with open(placement_file, "wb") as f: # generate placement file
for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()):
for location in self.multiworld.get_locations(self.player):
item = location.item
assert item is not None, "Can't handle unfilled location"
if item.code is None or location.address is None:

View File

@ -193,7 +193,7 @@ class UndertaleWorld(World):
def create_regions(self):
def UndertaleRegion(region_name: str, exits=[]):
ret = Region(region_name, self.player, self.multiworld)
ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name and
(loc_name not in exclusion_table["NoStats"] or

View File

@ -228,8 +228,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
if item.player == player and item.code and item.advancement
}
loc_in_this_world = {
location.name for location in multiworld.get_locations()
if location.player == player and location.address
location.name for location in multiworld.get_locations(player)
if location.address
}
always_locations = [

View File

@ -329,23 +329,22 @@ class ZillionWorld(World):
empty = zz_items[4]
multi_item = empty # a different patcher method differentiates empty from ap multi item
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
for loc in self.multiworld.get_locations():
if loc.player == self.player:
z_loc = cast(ZillionLocation, loc)
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
if z_loc.item is None:
self.logger.warn("generate_output location has no item - is that ok?")
z_loc.zz_loc.item = empty
elif z_loc.item.player == self.player:
z_item = cast(ZillionItem, z_loc.item)
z_loc.zz_loc.item = z_item.zz_item
else: # another player's item
# print(f"put multi item in {z_loc.zz_loc.name}")
z_loc.zz_loc.item = multi_item
multi_items[z_loc.zz_loc.name] = (
z_loc.item.name,
self.multiworld.get_player_name(z_loc.item.player)
)
for loc in self.multiworld.get_locations(self.player):
z_loc = cast(ZillionLocation, loc)
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
if z_loc.item is None:
self.logger.warn("generate_output location has no item - is that ok?")
z_loc.zz_loc.item = empty
elif z_loc.item.player == self.player:
z_item = cast(ZillionItem, z_loc.item)
z_loc.zz_loc.item = z_item.zz_item
else: # another player's item
# print(f"put multi item in {z_loc.zz_loc.name}")
z_loc.zz_loc.item = multi_item
multi_items[z_loc.zz_loc.name] = (
z_loc.item.name,
self.multiworld.get_player_name(z_loc.item.player)
)
# debug_zz_loc_ids.sort()
# for name, id_ in debug_zz_loc_ids.items():
# print(id_)