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 from __future__ import annotations
import copy import copy
import itertools
import functools import functools
import logging import logging
import random import random
import secrets import secrets
import typing # this can go away when Python 3.8 support is dropped import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace from argparse import Namespace
from collections import ChainMap, Counter, deque from collections import Counter, deque
from collections.abc import Collection from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar Type, ClassVar
@ -47,7 +48,6 @@ class ThreadBarrierProxy:
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_name: Dict[int, str] player_name: Dict[int, str]
_region_cache: Dict[int, Dict[str, Region]]
difficulty_requirements: dict difficulty_requirements: dict
required_medallions: dict required_medallions: dict
dark_room_logic: Dict[int, str] dark_room_logic: Dict[int, str]
@ -57,7 +57,7 @@ class MultiWorld():
plando_connections: List plando_connections: List
worlds: Dict[int, auto_world] worlds: Dict[int, auto_world]
groups: Dict[int, Group] groups: Dict[int, Group]
regions: List[Region] regions: RegionManager
itempool: List[Item] itempool: List[Item]
is_race: bool = False is_race: bool = False
precollected_items: Dict[int, List[Item]] precollected_items: Dict[int, List[Item]]
@ -92,6 +92,34 @@ class MultiWorld():
def __getitem__(self, player) -> bool: def __getitem__(self, player) -> bool:
return self.rule(player) 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): def __init__(self, players: int):
# world-local random state is saved for multiple generations running concurrently # world-local random state is saved for multiple generations running concurrently
self.random = ThreadBarrierProxy(random.Random()) self.random = ThreadBarrierProxy(random.Random())
@ -100,16 +128,12 @@ class MultiWorld():
self.glitch_triforce = False self.glitch_triforce = False
self.algorithm = 'balanced' self.algorithm = 'balanced'
self.groups = {} self.groups = {}
self.regions = [] self.regions = self.RegionManager(players)
self.shops = [] self.shops = []
self.itempool = [] self.itempool = []
self.seed = None self.seed = None
self.seed_name: str = "Unavailable" self.seed_name: str = "Unavailable"
self.precollected_items = {player: [] for player in self.player_ids} 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.required_locations = []
self.light_world_light_cone = False self.light_world_light_cone = False
self.dark_world_light_cone = False self.dark_world_light_cone = False
@ -137,7 +161,6 @@ class MultiWorld():
def set_player_attr(attr, val): def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {})
set_player_attr('shuffle', "vanilla") set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches") set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open') set_player_attr('mode', 'open')
@ -199,7 +222,6 @@ class MultiWorld():
self.game[new_id] = game self.game[new_id] = game
self.player_types[new_id] = NetUtils.SlotType.group self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game] world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players) 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]) 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): def world_name_lookup(self):
return {self.player_name[player_id]: player_id for player_id in self.player_ids} 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]: 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: def get_region(self, region_name: str, player: int) -> Region:
try: return self.regions.region_cache[player][region_name]
return self._region_cache[player][regionname]
except KeyError:
self._recache()
return self._region_cache[player][regionname]
def get_entrance(self, entrance: str, player: int) -> Entrance: def get_entrance(self, entrance_name: str, player: int) -> Entrance:
try: return self.regions.entrance_cache[player][entrance_name]
return self._entrance_cache[entrance, player]
except KeyError:
self._recache()
return self._entrance_cache[entrance, player]
def get_location(self, location: str, player: int) -> Location: def get_location(self, location_name: str, player: int) -> Location:
try: return self.regions.location_cache[player][location_name]
return self._location_cache[location, player]
except KeyError:
self._recache()
return self._location_cache[location, player]
def get_all_state(self, use_cache: bool) -> CollectionState: def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None) cached = getattr(self, "_all_state", None)
@ -428,28 +426,22 @@ class MultiWorld():
logging.debug('Placed %s at %s', item, location) logging.debug('Placed %s at %s', item, location)
def get_entrances(self) -> List[Entrance]: def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]:
if self._cached_entrances is None: if player is not None:
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances] return self.regions.entrance_cache[player].values()
return self._cached_entrances return Utils.RepeatableChain(tuple(self.regions.entrance_cache[player].values()
for player in self.regions.entrance_cache))
def clear_entrance_cache(self):
self._cached_entrances = None
def register_indirect_condition(self, region: Region, entrance: Entrance): def register_indirect_condition(self, region: Region, entrance: Entrance):
"""Report that access to this Region can result in unlocking this 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.""" state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
self.indirect_connections.setdefault(region, set()).add(entrance) self.indirect_connections.setdefault(region, set()).add(entrance)
def get_locations(self, player: Optional[int] = None) -> List[Location]: def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
if self._cached_locations is None:
self._cached_locations = [location for region in self.regions for location in region.locations]
if player is not None: if player is not None:
return [location for location in self._cached_locations if location.player == player] return self.regions.location_cache[player].values()
return self._cached_locations return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
for player in self.regions.location_cache))
def clear_location_cache(self):
self._cached_locations = None
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]: 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] 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)] valid_locations = [location.name for location in self.get_unfilled_locations(player)]
else: else:
valid_locations = location_names valid_locations = location_names
relevant_cache = self.regions.location_cache[player]
for location_name in valid_locations: for location_name in valid_locations:
location = self._location_cache.get((location_name, player), None) location = relevant_cache.get(location_name, None)
if location is not None and location.item is None: if location and location.item is None:
yield location yield location
def unlocks_new_location(self, item: Item) -> bool: def unlocks_new_location(self, item: Item) -> bool:
temp_state = self.state.copy() temp_state = self.state.copy()
temp_state.collect(item, True) 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): if temp_state.can_reach(location) and not self.state.can_reach(location):
return True return True
@ -820,15 +813,83 @@ class Region:
locations: List[Location] locations: List[Location]
entrance_type: ClassVar[Type[Entrance]] = Entrance 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): def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name self.name = name
self.entrances = [] self.entrances = []
self.exits = [] self._exits = self.EntranceRegister(multiworld.regions)
self.locations = [] self._locations = self.LocationRegister(multiworld.regions)
self.multiworld = multiworld self.multiworld = multiworld
self._hint_text = hint self._hint_text = hint
self.player = player 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: def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]: if state.stale[self.player]:
state.update_reachable_regions(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.') logger.info('Creating Items.')
AutoWorld.call_all(world, "create_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.') logger.info('Calculating Access Rules.')
for player in world.player_ids: 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") region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region) world.regions.append(region)
locations = region.locations = [] locations = region.locations
for item in world.itempool: for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0) count = common_item_count.get(item.player, {}).get(item.name, 0)
if count: 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)]) world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
if any(world.item_links.values()): if any(world.item_links.values()):
world._recache()
world._all_state = None world._all_state = None
logger.info("Running Item Plando") logger.info("Running Item Plando")

View File

@ -5,6 +5,7 @@ import json
import typing import typing
import builtins import builtins
import os import os
import itertools
import subprocess import subprocess
import sys import sys
import pickle 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: with open(file_name, "wt", encoding="utf-8") as f:
f.write("\n".join(uml)) 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 # basically a shortened reimplementation of this method from core, in order to force the check is done
def fulfills_accessibility() -> bool: def fulfills_accessibility() -> bool:
locations = self.multiworld.get_locations(1).copy() locations = list(self.multiworld.get_locations(1))
state = CollectionState(self.multiworld) state = CollectionState(self.multiworld)
while locations: while locations:
sphere: typing.List[Location] = [] sphere: typing.List[Location] = []

View File

@ -36,7 +36,6 @@ class TestBase(unittest.TestCase):
for game_name, world_type in AutoWorldRegister.world_types.items(): for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name): with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps) multiworld = setup_solo_multiworld(world_type, gen_steps)
multiworld._recache()
region_count = len(multiworld.get_regions()) region_count = len(multiworld.get_regions())
location_count = len(multiworld.get_locations()) location_count = len(multiworld.get_locations())
@ -46,14 +45,12 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()), self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation") f"{game_name} modified locations count during rule creation")
multiworld._recache()
call_all(multiworld, "generate_basic") call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()), self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic") f"{game_name} modified region count during generate_basic")
self.assertGreaterEqual(location_count, len(multiworld.get_locations()), self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during generate_basic") f"{game_name} modified locations count during generate_basic")
multiworld._recache()
call_all(multiworld, "pre_fill") call_all(multiworld, "pre_fill")
self.assertEqual(region_count, len(multiworld.get_regions()), self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during pre_fill") 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) loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc) region.locations.append(loc)
multiworld.clear_location_cache()
multiworld.push_item(loc, ItemFactory('Triforce', player), False) multiworld.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True loc.event = True

View File

@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch items # patch items
for location in world.get_locations(): for location in world.get_locations(player):
if location.player != player or location.address is None or location.shop_slot is not None: if location.address is None or location.shop_slot is not None:
continue continue
itemid = location.item.code if location.item is not None else 0x5A 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!' tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy() hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations) 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) local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles. # 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 # 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: for exit in world.get_region('Menu', player).exits:
exit.hide_path = True exit.hide_path = True
try:
set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) 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('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)) 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 # Helper functions to determine if the moon pearl is required
if inverted: if inverted:
def is_bunny(region): def is_bunny(region):
return region.is_light_world return region and region.is_light_world
def is_link(region): def is_link(region):
return region.is_dark_world return region and region.is_dark_world
else: else:
def is_bunny(region): def is_bunny(region):
return region.is_dark_world return region and region.is_dark_world
def is_link(region): 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): def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or # 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) return options_to_access_rule(possible_options)
# Add requirements for bunny-impassible caves if link is a bunny in them # 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): if not is_bunny(region):
continue continue
rule = get_rule_to_add(region) rule = get_rule_to_add(region)
for exit in region.exits: for region_exit in region.exits:
add_rule(exit, rule) add_rule(region_exit, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player) paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if is_bunny(paradox_shop): if is_bunny(paradox_shop):
add_rule(paradox_shop.entrances[0], get_rule_to_add(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 # 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(): for entrance in world.get_entrances(player):
if entrance.player == player and is_bunny(entrance.connected_region): if is_bunny(entrance.connected_region):
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] : if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon: 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(): 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.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
loc.shop_slot_disabled = True loc.shop_slot_disabled = True
shop.region.locations.append(loc) shop.region.locations.append(loc)
world.clear_location_cache()
class ShopData(NamedTuple): class ShopData(NamedTuple):
@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player):
if shop.type == ShopType.TakeAny: if shop.type == ShopType.TakeAny:
loc.shop_slot_disabled = True loc.shop_slot_disabled = True
shop.region.locations.append(loc) shop.region.locations.append(loc)
world.clear_location_cache()
loc.shop_slot = i loc.shop_slot = i

View File

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

View File

@ -1,5 +1,5 @@
from BaseClasses import CollectionState, ItemClassification 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.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory

View File

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

View File

@ -219,7 +219,7 @@ def create_regions_from_ladxr(player, multiworld, logic):
r = LinksAwakeningRegion( r = LinksAwakeningRegion(
name=name, ladxr_region=l, hint="", player=player, world=multiworld) 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 regions[l] = r
for ladxr_location in logic.location_list: for ladxr_location in logic.location_list:

View File

@ -231,9 +231,7 @@ class LinksAwakeningWorld(World):
# Find instrument, lock # Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please # TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False found = False
for r in self.multiworld.get_regions(): for r in self.multiworld.get_regions(self.player):
if r.player != self.player:
continue
if r.dungeon_index != item.item_data.dungeon_index: if r.dungeon_index != item.item_data.dungeon_index:
continue continue
for loc in r.locations: for loc in r.locations:
@ -269,10 +267,7 @@ class LinksAwakeningWorld(World):
event_location.place_locked_item(self.create_event("Can Play Trendy Game")) event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
for r in self.multiworld.get_regions(): for r in self.multiworld.get_regions(self.player):
if r.player != self.player:
continue
# Set aside dungeon locations # Set aside dungeon locations
if r.dungeon_index: if r.dungeon_index:
self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations 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) world.regions.append(boss_region)
region_final_boss = Region("Final Boss", player, world) 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)] player, "Wervyn Anixil", None, region_final_boss)]
world.regions.append(region_final_boss) world.regions.append(region_final_boss)
region_tfb = Region("True Final Boss", player, world) region_tfb = Region("True Final Boss", player, world)
region_tfb.locations = [MeritousLocation( region_tfb.locations += [MeritousLocation(
player, "Wervyn Anixil?", None, region_tfb)] player, "Wervyn Anixil?", None, region_tfb)]
world.regions.append(region_tfb) world.regions.append(region_tfb)

View File

@ -195,7 +195,8 @@ class OOTWorld(World):
setattr(self, option_name, option_value) setattr(self, option_name, option_value)
self.shop_prices = {} 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.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter() self.starting_items = Counter()
self.songs_as_items = False self.songs_as_items = False
@ -526,6 +527,10 @@ class OOTWorld(World):
# We still need to fill the location even if ALR is off. # We still need to fill the location even if ALR is off.
logger.debug('Unreachable location: %s', new_location.name) logger.debug('Unreachable location: %s', new_location.name)
new_location.player = self.player 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) new_region.locations.append(new_location)
if 'events' in region: if 'events' in region:
for event, rule in region['events'].items(): for event, rule in region['events'].items():
@ -555,7 +560,8 @@ class OOTWorld(World):
self.multiworld.regions.append(new_region) self.multiworld.regions.append(new_region)
self.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): def set_scrub_prices(self):
# Get Deku Scrub Locations # Get Deku Scrub Locations
@ -622,7 +628,7 @@ class OOTWorld(World):
'Twinrova', 'Twinrova',
'Links Pocket' '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] 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] 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() item = prizepool.pop()
loc = prize_locs.pop() loc = prize_locs.pop()
loc.place_locked_item(item) loc.place_locked_item(item)
self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc self.hinted_dungeon_reward_locations[item.name] = loc
def create_item(self, name: str, allow_arbitrary_name: bool = False): def create_item(self, name: str, allow_arbitrary_name: bool = False):
@ -671,7 +676,7 @@ class OOTWorld(World):
self.multiworld.regions.append(menu) self.multiworld.regions.append(menu)
self.load_regions_from_json(overworld_data_path) self.load_regions_from_json(overworld_data_path)
self.load_regions_from_json(bosses_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) create_dungeons(self)
self.parser.create_delayed_rules() self.parser.create_delayed_rules()
@ -682,16 +687,11 @@ class OOTWorld(World):
# Bind entrances to vanilla # Bind entrances to vanilla
for region in self.regions: for region in self.regions:
for exit in region.exits: 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): 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
generate_itempool(self) 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) junk_pool = get_junk_pool(self)
removed_items = [] removed_items = []
@ -769,7 +769,7 @@ class OOTWorld(World):
# Kill unreachable events that can't be gotten even with all items # Kill unreachable events that can't be gotten even with all items
# Make sure to only kill actual internal events, not in-game "events" # 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() all_locations = self.get_locations()
reachable = self.multiworld.get_reachable_locations(all_state, self.player) reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if 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) 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: if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
bigpoe.parent_region.locations.remove(bigpoe) 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 fast scarecrow then we need to kill the Pierre location as it will be unreachable
if self.free_scarecrow: if self.free_scarecrow:
@ -997,6 +996,7 @@ class OOTWorld(World):
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
single_player_placement=False, lock=True, allow_excluded=True) single_player_placement=False, lock=True, allow_excluded=True)
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
if self.hints != 'none': if self.hints != 'none':
self.hint_data_available.wait() self.hint_data_available.wait()
@ -1032,30 +1032,6 @@ class OOTWorld(World):
player_name=self.multiworld.get_player_name(self.player)) player_name=self.multiworld.get_player_name(self.player))
apz5.write() 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. # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod @classmethod
@ -1135,6 +1111,7 @@ class OOTWorld(World):
for autoworld in multiworld.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 fill_slot_data(self): def fill_slot_data(self):
self.collectible_flags_available.wait() self.collectible_flags_available.wait()
return { return {
@ -1142,6 +1119,7 @@ class OOTWorld(World):
'collectible_flag_offsets': self.collectible_flag_offsets 'collectible_flag_offsets': self.collectible_flag_offsets
} }
def modify_multidata(self, multidata: dict): def modify_multidata(self, multidata: dict):
# Replace connect name # Replace connect name
@ -1156,6 +1134,7 @@ class OOTWorld(World):
continue continue
multidata["precollected_items"][self.player].remove(item_id) multidata["precollected_items"][self.player].remove(item_id)
def extend_hint_information(self, er_hint_data: dict): def extend_hint_information(self, er_hint_data: dict):
er_hint_data[self.player] = {} er_hint_data[self.player] = {}
@ -1202,6 +1181,7 @@ class OOTWorld(World):
er_hint_data[self.player][location.address] = main_entrance.name er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}") logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: 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]) 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") 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(): for k, v in self.shop_prices.items():
spoiler_handle.write(f"{k}: {v} Rupees\n") 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 ring handling:
# Key rings are multiple items glued together into one, so we need to give # 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 # the appropriate number of keys in the collection state when they are
@ -1242,9 +1248,8 @@ class OOTWorld(World):
return False return False
def get_shufflable_entrances(self, type=None, only_primary=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 return [entrance for entrance in self.multiworld.get_entrances(self.player) if (
(type == None or entrance.type == type) and (type == None or entrance.type == type) and (not only_primary or entrance.primary))]
(not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False): 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 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): def get_location(self, location):
return self.multiworld.get_location(location, self.player) return self.multiworld.get_location(location, self.player)
def get_region(self, region): def get_region(self, region_name):
return self.multiworld.get_region(region, self.player) 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): def get_entrance(self, entrance):
return self.multiworld.get_entrance(entrance, self.player) 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 # 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. # fail. Re-use test_state from previous final loop.
evolutions_region = self.multiworld.get_region("Evolution", self.player) evolutions_region = self.multiworld.get_region("Evolution", self.player)
clear_cache = False
for location in evolutions_region.locations.copy(): for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player): if not test_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location) 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": if self.multiworld.old_man[self.player] == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
@ -559,7 +555,6 @@ class PokemonRedBlueWorld(World):
else: else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location") raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
self.multiworld._recache()
if self.multiworld.door_shuffle[self.player] == "decoupled": if self.multiworld.door_shuffle[self.player] == "decoupled":
swept_state = self.multiworld.state.copy() swept_state = self.multiworld.state.copy()

View File

@ -546,10 +546,8 @@ def generate_output(self, output_directory: str):
write_quizzes(self, data, random) write_quizzes(self, data, random)
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations(self.player):
if location.player != self.player: if location.party_data:
continue
elif location.party_data:
for party in location.party_data: for party in location.party_data:
if not isinstance(party["party_address"], list): if not isinstance(party["party_address"], list):
addresses = [rom_addresses[party["party_address"]]] 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 # a long enough run to have enough director credits for scavengers and
# help prevent being stuck in the same stages until that point.) # help prevent being stuck in the same stages until that point.)
for location in multiworld.get_locations(): for location in multiworld.get_locations(player):
if location.player != player: continue # ignore all checks that don't belong to this player
if "Scavenger" in location.name: if "Scavenger" in location.name:
add_rule(location, lambda state: state.has("Stage_5", player)) add_rule(location, lambda state: state.has("Stage_5", player))
# Regions # Regions

View File

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

View File

@ -417,7 +417,7 @@ class SoEWorld(World):
flags += option.to_flag() flags += option.to_flag()
with open(placement_file, "wb") as f: # generate placement file 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 item = location.item
assert item is not None, "Can't handle unfilled location" assert item is not None, "Can't handle unfilled location"
if item.code is None or location.address is None: if item.code is None or location.address is None:

View File

@ -193,7 +193,7 @@ class UndertaleWorld(World):
def create_regions(self): def create_regions(self):
def UndertaleRegion(region_name: str, exits=[]): def UndertaleRegion(region_name: str, exits=[]):
ret = Region(region_name, self.player, self.multiworld) 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() for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name and if loc_data.region == region_name and
(loc_name not in exclusion_table["NoStats"] or (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 if item.player == player and item.code and item.advancement
} }
loc_in_this_world = { loc_in_this_world = {
location.name for location in multiworld.get_locations() location.name for location in multiworld.get_locations(player)
if location.player == player and location.address if location.address
} }
always_locations = [ always_locations = [

View File

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