Core: change Region caching to on_change from on-miss-strategy (#2366)
This commit is contained in:
parent
d9b076a687
commit
3e0d1d4e1c
185
BaseClasses.py
185
BaseClasses.py
|
@ -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)
|
||||
|
|
7
Main.py
7
Main.py
|
@ -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")
|
||||
|
|
15
Utils.py
15
Utils.py
|
@ -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)
|
||||
|
|
|
@ -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] = []
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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_)
|
||||
|
|
Loading…
Reference in New Issue