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
|
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)
|
||||||
|
|
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.')
|
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")
|
||||||
|
|
15
Utils.py
15
Utils.py
|
@ -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)
|
||||||
|
|
|
@ -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] = []
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"]]]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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_)
|
||||||
|
|
Loading…
Reference in New Issue