From 3cb64d5f2e7fed3c3826f5c973164c50b7f8dc37 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 3 Oct 2024 21:09:14 -0500 Subject: [PATCH 01/12] Core: move region and location management to worlds --- BaseClasses.py | 88 +++++++++++++++++++--------------- worlds/AutoWorld.py | 10 ++-- worlds/messenger/subclasses.py | 2 +- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0d4f34e51445..9dd8afa6b684 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -98,34 +98,42 @@ 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)} + region_cache: Union[Dict[int, Dict[str, Region]], Dict[str, Region]] + entrance_cache: Union[Dict[int, Dict[str, Entrance]], Dict[str, Entrance]] + location_cache: Union[Dict[int, Dict[str, Location]], Dict[str, Location]] + multiworld: "MultiWorld" + + def __init__(self, multiworld: "Multiworld" = None): + # players is no longer needed. The multiworld is passed in here so we can reference the worlds' caches + # while they continue to use multiworld.regions + # TODO remove later + self.multiworld = multiworld + self.region_cache = {} + self.entrance_cache = {} + self.location_cache = {} def __iadd__(self, other: Iterable[Region]): self.extend(other) return self def append(self, region: Region): - assert region.name not in self.region_cache[region.player], \ - f"{region.name} already exists in region cache." - self.region_cache[region.player][region.name] = region + # TODO + if self.multiworld is not None: + region_cache = self.multiworld.worlds[region.player].regions.region_cache + else: + region_cache = self.region_cache + assert region.name not in region_cache, f"{region.name} already exists in region cache." + region_cache[region.name] = region def extend(self, regions: Iterable[Region]): + # TODO + if self.multiworld is not None: + region_cache = self.multiworld.worlds[regions[0].player].regions.region_cache + else: + region_cache = self.region_cache for region in regions: - assert region.name not in self.region_cache[region.player], \ - f"{region.name} already exists in region cache." - self.region_cache[region.player][region.name] = region - - def add_group(self, new_id: int): - self.region_cache[new_id] = {} - self.entrance_cache[new_id] = {} - self.location_cache[new_id] = {} + assert region.name not in region_cache, f"{region.name} already exists in region cache." + region_cache[region.name] = region def __iter__(self) -> Iterator[Region]: for regions in self.region_cache.values(): @@ -141,7 +149,7 @@ def __init__(self, players: int): self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.algorithm = 'balanced' self.groups = {} - self.regions = self.RegionManager(players) + self.regions = self.RegionManager(self) self.shops = [] self.itempool = [] self.seed = None @@ -189,7 +197,6 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset( return group_id, group new_id: int = self.players + len(self.groups) + 1 - self.regions.add_group(new_id) self.game[new_id] = game self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] @@ -417,16 +424,19 @@ def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} def get_regions(self, player: Optional[int] = None) -> Collection[Region]: - return self.regions if player is None else self.regions.region_cache[player].values() + if player is not None: + return self.worlds[player].regions.region_cache.values() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.region_cache.values() + for player in self.get_all_ids())) def get_region(self, region_name: str, player: int) -> Region: - return self.regions.region_cache[player][region_name] + return self.worlds[player].get_region(region_name) def get_entrance(self, entrance_name: str, player: int) -> Entrance: - return self.regions.entrance_cache[player][entrance_name] + return self.worlds[player].get_entrance(entrance_name) def get_location(self, location_name: str, player: int) -> Location: - return self.regions.location_cache[player][location_name] + return self.worlds[player].get_location(location_name) def get_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) @@ -489,9 +499,9 @@ def push_item(self, location: Location, item: Item, collect: bool = True): 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)) + return self.worlds[player].regions.entrance_cache.values() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.entrance_cache.values() + for player in self.get_all_ids())) def register_indirect_condition(self, region: Region, entrance: Entrance): """Report that access to this Region can result in unlocking this Entrance, @@ -500,9 +510,9 @@ def register_indirect_condition(self, region: Region, entrance: Entrance): def get_locations(self, player: Optional[int] = None) -> Iterable[Location]: if player is not 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)) + return self.worlds[player].regions.location_cache.values() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.location_cache.values() + for player in self.get_all_ids())) 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] @@ -524,7 +534,7 @@ def get_unfilled_locations_for_players(self, location_names: List[str], players: valid_locations = [location.name for location in self.get_unfilled_locations(player)] else: valid_locations = location_names - relevant_cache = self.regions.location_cache[player] + relevant_cache = self.worlds[player].regions.location_cache for location_name in valid_locations: location = relevant_cache.get(location_name, None) if location and location.item is None: @@ -1007,22 +1017,22 @@ def __delitem__(self, index: int) -> None: del(self.region_manager.location_cache[location.player][location.name]) def insert(self, index: int, value: Location) -> None: - assert value.name not in self.region_manager.location_cache[value.player], \ + assert value.name not in self.region_manager.location_cache, \ f"{value.name} already exists in the location cache." self._list.insert(index, value) - self.region_manager.location_cache[value.player][value.name] = value + self.region_manager.location_cache[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]) + del (self.region_manager.entrance_cache[entrance.name]) def insert(self, index: int, value: Entrance) -> None: - assert value.name not in self.region_manager.entrance_cache[value.player], \ + assert value.name not in self.region_manager.entrance_cache, \ f"{value.name} already exists in the entrance cache." self._list.insert(index, value) - self.region_manager.entrance_cache[value.player][value.name] = value + self.region_manager.entrance_cache[value.name] = value _locations: LocationRegister[Location] _exits: EntranceRegister[Entrance] @@ -1030,8 +1040,8 @@ def insert(self, index: int, value: Entrance) -> None: def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name self.entrances = [] - self._exits = self.EntranceRegister(multiworld.regions) - self._locations = self.LocationRegister(multiworld.regions) + self._exits = self.EntranceRegister(multiworld.worlds[player].regions) + self._locations = self.LocationRegister(multiworld.worlds[player].regions) self.multiworld = multiworld self._hint_text = hint self.player = player diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f7dae2b92750..197d8ca44354 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -294,6 +294,8 @@ class World(metaclass=AutoWorldRegister): origin_region_name: str = "Menu" """Name of the Region from which accessibility is tested.""" + regions: "MultiWorld.RegionManager" + """Regions for this world instance. Regions should be added to this, and not override it.""" explicit_indirect_conditions: bool = True """If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly. @@ -334,6 +336,8 @@ def __init__(self, multiworld: "MultiWorld", player: int): self.player = player self.random = Random(multiworld.random.getrandbits(64)) multiworld.per_slot_randoms[player] = self.random + from BaseClasses import MultiWorld + self.regions = MultiWorld.RegionManager() def __getattr__(self, item: str) -> Any: if item == "settings": @@ -528,13 +532,13 @@ def create_filler(self) -> "Item": # convenience methods def get_location(self, location_name: str) -> "Location": - return self.multiworld.get_location(location_name, self.player) + return self.regions.location_cache[location_name] def get_entrance(self, entrance_name: str) -> "Entrance": - return self.multiworld.get_entrance(entrance_name, self.player) + return self.regions.entrance_cache[entrance_name] def get_region(self, region_name: str) -> "Region": - return self.multiworld.get_region(region_name, self.player) + return self.regions.region_cache[region_name] @property def player_name(self) -> str: diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index b60aeb179feb..916d01b03ee8 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -42,7 +42,7 @@ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = N loc_dict = {loc: world.location_name_to_id.get(loc, None) for loc in locations} self.add_locations(loc_dict, MessengerLocation) - self.multiworld.regions.append(self) + world.regions.append(self) class MessengerLocation(Location): From 9379c78b205b448332d1c53635362f5dda1b375c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 16:37:21 -0500 Subject: [PATCH 02/12] move RegionManager class to root level --- BaseClasses.py | 97 +++++++++++++++++++++++---------------------- worlds/AutoWorld.py | 8 ++-- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9dd8afa6b684..a345ccb3c1a7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -55,6 +55,52 @@ class HasNameAndPlayer(Protocol): player: int +class RegionManager(typing.Generic[_T_Reg, _T_Ent, _T_Loc]): + region_cache: Union[Dict[int, Dict[str, Region]], Dict[str, Region]] + entrance_cache: Union[Dict[int, Dict[str, Entrance]], Dict[str, Entrance]] + location_cache: Union[Dict[int, Dict[str, Location]], Dict[str, Location]] + multiworld: "MultiWorld" + + def __init__(self, multiworld: "Multiworld" = None): + # players is no longer needed. The multiworld is passed in here so we can reference the worlds' caches + # while they continue to use multiworld.regions + # TODO remove later + self.multiworld = multiworld + self.region_cache = {} + self.entrance_cache = {} + self.location_cache = {} + + def __iadd__(self, other: Iterable[Region]): + self.extend(other) + return self + + def append(self, region: Region): + # TODO + if self.multiworld is not None: + region_cache = self.multiworld.worlds[region.player].regions.region_cache + else: + region_cache = self.region_cache + assert region.name not in region_cache, f"{region.name} already exists in region cache." + region_cache[region.name] = region + + def extend(self, regions: Iterable[Region]): + # TODO + if self.multiworld is not None: + region_cache = self.multiworld.worlds[regions[0].player].regions.region_cache + else: + region_cache = self.region_cache + for region in regions: + assert region.name not in region_cache, f"{region.name} already exists in region cache." + region_cache[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()) + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -97,51 +143,6 @@ def __init__(self, rule): def __getitem__(self, player) -> bool: return self.rule(player) - class RegionManager: - region_cache: Union[Dict[int, Dict[str, Region]], Dict[str, Region]] - entrance_cache: Union[Dict[int, Dict[str, Entrance]], Dict[str, Entrance]] - location_cache: Union[Dict[int, Dict[str, Location]], Dict[str, Location]] - multiworld: "MultiWorld" - - def __init__(self, multiworld: "Multiworld" = None): - # players is no longer needed. The multiworld is passed in here so we can reference the worlds' caches - # while they continue to use multiworld.regions - # TODO remove later - self.multiworld = multiworld - self.region_cache = {} - self.entrance_cache = {} - self.location_cache = {} - - def __iadd__(self, other: Iterable[Region]): - self.extend(other) - return self - - def append(self, region: Region): - # TODO - if self.multiworld is not None: - region_cache = self.multiworld.worlds[region.player].regions.region_cache - else: - region_cache = self.region_cache - assert region.name not in region_cache, f"{region.name} already exists in region cache." - region_cache[region.name] = region - - def extend(self, regions: Iterable[Region]): - # TODO - if self.multiworld is not None: - region_cache = self.multiworld.worlds[regions[0].player].regions.region_cache - else: - region_cache = self.region_cache - for region in regions: - assert region.name not in region_cache, f"{region.name} already exists in region cache." - region_cache[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()) @@ -149,7 +150,7 @@ def __init__(self, players: int): self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.algorithm = 'balanced' self.groups = {} - self.regions = self.RegionManager(self) + self.regions = RegionManager(self) self.shops = [] self.itempool = [] self.seed = None @@ -988,9 +989,9 @@ class Region: entrance_type: ClassVar[Type[Entrance]] = Entrance class Register(MutableSequence): - region_manager: MultiWorld.RegionManager + region_manager: RegionManager - def __init__(self, region_manager: MultiWorld.RegionManager): + def __init__(self, region_manager: RegionManager): self._list = [] self.region_manager = region_manager diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 197d8ca44354..88550c407ca5 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -14,7 +14,7 @@ from BaseClasses import CollectionState if TYPE_CHECKING: - from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance + from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance, RegionManager from . import GamesPackage from settings import Group @@ -294,7 +294,7 @@ class World(metaclass=AutoWorldRegister): origin_region_name: str = "Menu" """Name of the Region from which accessibility is tested.""" - regions: "MultiWorld.RegionManager" + regions: "RegionManager" """Regions for this world instance. Regions should be added to this, and not override it.""" explicit_indirect_conditions: bool = True @@ -336,8 +336,8 @@ def __init__(self, multiworld: "MultiWorld", player: int): self.player = player self.random = Random(multiworld.random.getrandbits(64)) multiworld.per_slot_randoms[player] = self.random - from BaseClasses import MultiWorld - self.regions = MultiWorld.RegionManager() + from BaseClasses import RegionManager + self.regions = RegionManager() def __getattr__(self, item: str) -> Any: if item == "settings": From 13e51dfe60eb0a25e4c2b421aaaf43d1ee063472 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 17:45:40 -0500 Subject: [PATCH 03/12] move getters to access the RegionManager and add variable typing --- BaseClasses.py | 54 +++++++++++++++++++++++++++++++-------------- worlds/AutoWorld.py | 6 ++--- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a345ccb3c1a7..f24c3ebcd3e8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -55,10 +55,15 @@ class HasNameAndPlayer(Protocol): player: int +_T_Reg = typing.TypeVar("_T_Reg", bound="Region") +_T_Ent = typing.TypeVar("_T_Ent", bound="Entrance") +_T_Loc = typing.TypeVar("_T_Loc", bound="Location") + + class RegionManager(typing.Generic[_T_Reg, _T_Ent, _T_Loc]): - region_cache: Union[Dict[int, Dict[str, Region]], Dict[str, Region]] - entrance_cache: Union[Dict[int, Dict[str, Entrance]], Dict[str, Entrance]] - location_cache: Union[Dict[int, Dict[str, Location]], Dict[str, Location]] + region_cache: Dict[str, Region] + entrance_cache: Dict[str, Entrance] + location_cache: Dict[str, Location] multiworld: "MultiWorld" def __init__(self, multiworld: "Multiworld" = None): @@ -94,11 +99,28 @@ def extend(self, regions: Iterable[Region]): region_cache[region.name] = region def __iter__(self) -> Iterator[Region]: - for regions in self.region_cache.values(): - yield from regions.values() + yield from self.region_cache.values() def __len__(self): - return sum(len(regions) for regions in self.region_cache.values()) + return len(self.region_cache.values()) + + def get_regions(self) -> typing.Iterable[_T_Reg]: + return self.region_cache.values() + + def get_region(self, name: str) -> _T_Reg: + return self.region_cache[name] + + def get_locations(self) -> typing.Iterable[_T_Loc]: + return self.location_cache.values() + + def get_location(self, name: str) -> _T_Loc: + return self.location_cache[name] + + def get_entrances(self) -> typing.Iterable[_T_Ent]: + return self.entrance_cache.values() + + def get_entrance(self, name: str) -> _T_Ent: + return self.entrance_cache[name] class MultiWorld(): @@ -426,18 +448,18 @@ def world_name_lookup(self): def get_regions(self, player: Optional[int] = None) -> Collection[Region]: if player is not None: - return self.worlds[player].regions.region_cache.values() - return Utils.RepeatableChain(tuple(self.worlds[player].regions.region_cache.values() + return self.worlds[player].regions.get_regions() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_regions() for player in self.get_all_ids())) def get_region(self, region_name: str, player: int) -> Region: - return self.worlds[player].get_region(region_name) + return self.worlds[player].regions.get_region(region_name) def get_entrance(self, entrance_name: str, player: int) -> Entrance: - return self.worlds[player].get_entrance(entrance_name) + return self.worlds[player].regions.get_entrance(entrance_name) def get_location(self, location_name: str, player: int) -> Location: - return self.worlds[player].get_location(location_name) + return self.worlds[player].regions.get_location(location_name) def get_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) @@ -500,8 +522,8 @@ def push_item(self, location: Location, item: Item, collect: bool = True): def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]: if player is not None: - return self.worlds[player].regions.entrance_cache.values() - return Utils.RepeatableChain(tuple(self.worlds[player].regions.entrance_cache.values() + return self.worlds[player].regions.get_entrances() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_entrances() for player in self.get_all_ids())) def register_indirect_condition(self, region: Region, entrance: Entrance): @@ -511,8 +533,8 @@ def register_indirect_condition(self, region: Region, entrance: Entrance): def get_locations(self, player: Optional[int] = None) -> Iterable[Location]: if player is not None: - return self.worlds[player].regions.location_cache.values() - return Utils.RepeatableChain(tuple(self.worlds[player].regions.location_cache.values() + return self.worlds[player].regions.get_locations() + return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_locations() for player in self.get_all_ids())) def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]: @@ -1015,7 +1037,7 @@ 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]) + del(self.region_manager.location_cache[location.name]) def insert(self, index: int, value: Location) -> None: assert value.name not in self.region_manager.location_cache, \ diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 88550c407ca5..709aba566e12 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -532,13 +532,13 @@ def create_filler(self) -> "Item": # convenience methods def get_location(self, location_name: str) -> "Location": - return self.regions.location_cache[location_name] + return self.regions.get_location(location_name) def get_entrance(self, entrance_name: str) -> "Entrance": - return self.regions.entrance_cache[entrance_name] + return self.regions.get_entrance(entrance_name) def get_region(self, region_name: str) -> "Region": - return self.regions.region_cache[region_name] + return self.regions.get_region(region_name) @property def player_name(self) -> str: From a0fa02fe33a0b32a6a3a71b0753b01c3d2e2d2d7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 19:19:28 -0500 Subject: [PATCH 04/12] missed some typing --- BaseClasses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f24c3ebcd3e8..e53fe97ffc19 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -61,12 +61,12 @@ class HasNameAndPlayer(Protocol): class RegionManager(typing.Generic[_T_Reg, _T_Ent, _T_Loc]): - region_cache: Dict[str, Region] - entrance_cache: Dict[str, Entrance] - location_cache: Dict[str, Location] + region_cache: Dict[str, _T_Reg] + entrance_cache: Dict[str, _T_Ent] + location_cache: Dict[str, _T_Loc] multiworld: "MultiWorld" - def __init__(self, multiworld: "Multiworld" = None): + def __init__(self, multiworld: "MultiWorld" = None): # players is no longer needed. The multiworld is passed in here so we can reference the worlds' caches # while they continue to use multiworld.regions # TODO remove later From 0d44bbabeea723f54c0b66f11edc1db7079b0f5d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 19:20:41 -0500 Subject: [PATCH 05/12] multiworld.get_regions returns an `Iterable`, not a `Collection` --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index e53fe97ffc19..3e52adced486 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -446,7 +446,7 @@ def get_out_file_name_base(self, player: int) -> str: def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} - def get_regions(self, player: Optional[int] = None) -> Collection[Region]: + def get_regions(self, player: Optional[int] = None) -> Iterable[Region]: if player is not None: return self.worlds[player].regions.get_regions() return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_regions() From 4add621d81eb6859aaa7d8b1d06b56a4e69244a7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 20:01:03 -0500 Subject: [PATCH 06/12] fix tests and some worlds directly accessing the cache --- BaseClasses.py | 10 +++++----- test/general/__init__.py | 4 +++- test/general/test_helpers.py | 6 ++---- worlds/aquaria/__init__.py | 12 ++++++------ worlds/dark_souls_3/__init__.py | 2 +- worlds/oot/Dungeon.py | 6 +++--- worlds/oot/LocationList.py | 2 +- worlds/oot/__init__.py | 14 +++++++------- worlds/pokemon_rb/regions.py | 2 +- worlds/sc2/test/test_Regions.py | 4 ++-- worlds/sm/__init__.py | 2 +- worlds/stardew_valley/stardew_rule/state.py | 2 +- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3e52adced486..78c4e8f6b76d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -89,12 +89,12 @@ def append(self, region: Region): region_cache[region.name] = region def extend(self, regions: Iterable[Region]): - # TODO - if self.multiworld is not None: - region_cache = self.multiworld.worlds[regions[0].player].regions.region_cache - else: - region_cache = self.region_cache for region in regions: + # TODO + if self.multiworld is not None: + region_cache = self.multiworld.worlds[region.player].regions.region_cache + else: + region_cache = self.region_cache assert region.name not in region_cache, f"{region.name} already exists in region cache." region_cache[region.name] = region diff --git a/test/general/__init__.py b/test/general/__init__.py index 8afd84976540..53be7afacf7b 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,6 +1,8 @@ from argparse import Namespace from typing import List, Optional, Tuple, Type, Union +from tornado.gen import multi + from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region from worlds import network_data_package from worlds.AutoWorld import World, call_all @@ -73,7 +75,7 @@ def generate_test_multiworld(players: int = 1) -> MultiWorld: :return: The generated test multiworld """ multiworld = setup_multiworld([TestWorld] * players, seed=0) - multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)] + multiworld.regions += [Region("Menu", player_id, multiworld) for player_id in multiworld.player_ids] return multiworld diff --git a/test/general/test_helpers.py b/test/general/test_helpers.py index be8473975638..f619d41dfd54 100644 --- a/test/general/test_helpers.py +++ b/test/general/test_helpers.py @@ -2,6 +2,7 @@ from typing import Callable, Dict, Optional from BaseClasses import CollectionState, MultiWorld, Region +from test.general import TestWorld, setup_solo_multiworld class TestHelpers(unittest.TestCase): @@ -9,10 +10,7 @@ class TestHelpers(unittest.TestCase): player: int = 1 def setUp(self) -> None: - self.multiworld = MultiWorld(self.player) - self.multiworld.game[self.player] = "helper_test_game" - self.multiworld.player_name = {1: "Tester"} - self.multiworld.set_seed() + self.multiworld = setup_solo_multiworld(TestWorld, ()) def test_region_helpers(self) -> None: """Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior""" diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 1fb04036d81b..8bb5ce3f1a8b 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -92,7 +92,7 @@ class AquariaWorld(World): options: AquariaOptions "Every options of the world" - regions: AquariaRegions + _regions: AquariaRegions "Used to manage Regions" exclude: List[str] @@ -100,7 +100,7 @@ class AquariaWorld(World): def __init__(self, multiworld: MultiWorld, player: int): """Initialisation of the Aquaria World""" super(AquariaWorld, self).__init__(multiworld, player) - self.regions = AquariaRegions(multiworld, player) + self._regions = AquariaRegions(multiworld, player) self.ingredients_substitution = [] self.exclude = [] @@ -108,9 +108,9 @@ def create_regions(self) -> None: """ Create every Region in `regions` """ - self.regions.add_regions_to_world() - self.regions.connect_regions() - self.regions.add_event_locations() + self._regions.add_regions_to_world() + self._regions.connect_regions() + self._regions.add_event_locations() def create_item(self, name: str) -> AquariaItem: """ @@ -177,7 +177,7 @@ def set_rules(self) -> None: Launched when the Multiworld generator is ready to generate rules """ - self.regions.adjusting_rules(self.options) + self._regions.adjusting_rules(self.options) self.multiworld.completion_condition[self.player] = lambda \ state: state.has("Victory", self.player) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b51668539be2..6dae3ae7cfda 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1305,7 +1305,7 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: """Sets a rule for the entrance to the given region.""" assert region in location_tables - if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if not any(region == reg for reg in self.regions.region_cache): return if isinstance(rule, str): if " -> " not in rule: assert item_dictionary[rule].classification == ItemClassification.progression diff --git a/worlds/oot/Dungeon.py b/worlds/oot/Dungeon.py index d0074d0d9895..c190bde433b7 100644 --- a/worlds/oot/Dungeon.py +++ b/worlds/oot/Dungeon.py @@ -11,10 +11,10 @@ def __init__(self, world, name, hint, font_color): self.small_keys = [] self.dungeon_items = [] - for region in world.multiworld.regions: - if region.player == world.player and region.dungeon == self.name: + for region in world.regions.get_regions(): + if region.dungeon == self.name: region.dungeon = self - self.regions.append(region) + self.regions.append(region) def copy(self, new_world): diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py index 27ad575699f5..dccdfaea37e9 100644 --- a/worlds/oot/LocationList.py +++ b/worlds/oot/LocationList.py @@ -2096,7 +2096,7 @@ def location_is_viewable(loc_name, correct_chest_appearances, fast_chests): # Sets all Drop locations to a unique name in order to avoid name issues and to identify locations in the spoiler # Also cause them to not be shown in the list of locations, only in playthrough def set_drop_location_names(ootworld): - for region in ootworld.regions: + for region in ootworld._regions: for location in region.locations: if location.type == 'Drop': location.name = location.parent_region.name + " " + location.name diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b93f60b2a08e..bb0062cdba2f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -218,7 +218,7 @@ def generate_early(self): option_value = result.current_key setattr(self, option_name, option_value) - self.regions = [] # internal caches of regions for this world, used later + self._regions = [] # internal caches of regions for this world, used later self._regions_cache = {} self.shop_prices = {} @@ -586,7 +586,7 @@ def load_regions_from_json(self, file_path): new_region.exits.append(new_exit) self.multiworld.regions.append(new_region) - self.regions.append(new_region) + self._regions.append(new_region) self._regions_cache[new_region.name] = new_region @@ -624,7 +624,7 @@ def set_scrub_prices(self): def random_shop_prices(self): shop_item_indexes = ['7', '5', '8', '6'] self.shop_prices = {} - for region in self.regions: + for region in self._regions: if self.shopsanity == 'random': shop_item_count = self.random.randint(0, 4) else: @@ -756,7 +756,7 @@ def create_regions(self): self.set_scrub_prices() # Bind entrances to vanilla - for region in self.regions: + for region in self._regions: for exit in region.exits: exit.connect(self.get_region(exit.vanilla_connected_region)) @@ -1121,7 +1121,7 @@ def hint_type_players(hint_type: str) -> set: items_by_region = {} for player in barren_hint_players: items_by_region[player] = {} - for r in multiworld.worlds[player].regions: + for r in multiworld.worlds[player]._regions: items_by_region[player][r._hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True} for d in multiworld.worlds[player].dungeons: items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True} @@ -1265,7 +1265,7 @@ def get_entrance_to_region(region): if entrance.name in hint_entrances: return entrance if region.dungeon is not None: - for r in region.dungeon.regions: + for r in region.dungeon._regions: for e in r.entrances: if e.name in hint_entrances: return e @@ -1275,7 +1275,7 @@ def get_entrance_to_region(region): if (self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances or self.shuffle_bosses != 'off'): - for region in self.regions: + for region in self._regions: if not any(bool(loc.address) for loc in region.locations): # check if region has any non-event locations continue main_entrance = get_entrance_to_region(region) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 575f4a61ca6f..4d18fc5dc8cb 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1928,7 +1928,7 @@ def create_regions(world): lambda state: logic.can_fly(state, world, player) and state.has("Town Map", player), one_way=True, name="Town Map Fly Location") - cache = multiworld.regions.entrance_cache[world.player].copy() + cache = world.regions.entrance_cache.copy() if world.options.badgesanity or world.options.door_shuffle in ("off", "simple"): badges = None badge_locs = None diff --git a/worlds/sc2/test/test_Regions.py b/worlds/sc2/test/test_Regions.py index c268b65da9a8..286aba396fd8 100644 --- a/worlds/sc2/test/test_Regions.py +++ b/worlds/sc2/test/test_Regions.py @@ -36,6 +36,6 @@ class TestGridGeneration(Sc2TestBase): } def test_size_matches_exclusions(self): - self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions) + self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.world.regions) # WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location - self.assertEqual(len(self.multiworld.regions), 29) + self.assertEqual(len(self.world.regions), 29) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index bf9d6d087edd..0e2397e9e720 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -291,7 +291,7 @@ def create_regions(self): 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 not in self.multiworld.regions.entrance_cache[self.player]: + if src.Name + "->" + dest.Name not in self.regions.entrance_cache: 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) diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index 5f5e61b3d4e5..668f38a57f84 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -73,7 +73,7 @@ class Reach(BaseStardewRule): player: int def __call__(self, state: CollectionState) -> bool: - if self.resolution_hint == 'Region' and self.spot not in state.multiworld.regions.region_cache[self.player]: + if self.resolution_hint == 'Region' and self.spot not in state.multiworld.worlds[self.player].regions.region_cache: return False return state.can_reach(self.spot, self.resolution_hint, self.player) From aaae352d44524671832610cd0e2c84bd78565465 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 8 Oct 2024 20:05:30 -0500 Subject: [PATCH 07/12] revert incorrect change in oot --- worlds/oot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index bb0062cdba2f..480be87a1643 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1265,7 +1265,7 @@ def get_entrance_to_region(region): if entrance.name in hint_entrances: return entrance if region.dungeon is not None: - for r in region.dungeon._regions: + for r in region.dungeon.regions: for e in r.entrances: if e.name in hint_entrances: return e From ae1ae2b4e9d9f02f35ace70eb21e65783af81ac9 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 18 Nov 2024 17:13:00 -0600 Subject: [PATCH 08/12] update __iter__() and __len__() implementations --- BaseClasses.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 78c4e8f6b76d..ea117a46666c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -99,9 +99,16 @@ def extend(self, regions: Iterable[Region]): region_cache[region.name] = region def __iter__(self) -> Iterator[Region]: - yield from self.region_cache.values() + # TODO + if self.multiworld is not None: + yield from self.multiworld.get_regions() + else: + yield from self.region_cache.values() def __len__(self): + # TODO + if self.multiworld is not None: + return len(self.multiworld.get_regions()) return len(self.region_cache.values()) def get_regions(self) -> typing.Iterable[_T_Reg]: From f07236f482ba48fdf1e7d3fb740db24d96e08302 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 18 Nov 2024 17:19:37 -0600 Subject: [PATCH 09/12] DS3 uses strings --- worlds/dark_souls_3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index ce236d36767d..765ffb1fc544 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -89,6 +89,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.all_excluded_locations = set() def generate_early(self) -> None: + self.created_regions = set() self.all_excluded_locations.update(self.options.exclude_locations.value) # Inform Universal Tracker where Yhorm is being randomized to. @@ -294,6 +295,7 @@ def create_region(self, region_name, location_table) -> Region: new_region.locations.append(new_location) self.multiworld.regions.append(new_region) + self.created_regions.add(region_name) return new_region def create_items(self) -> None: @@ -1305,7 +1307,7 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: """Sets a rule for the entrance to the given region.""" assert region in location_tables - if region not in self.regions: return + if region not in self.created_regions: return if isinstance(rule, str): if " -> " not in rule: assert item_dictionary[rule].classification == ItemClassification.progression From d06ea988fb0209b55a7653ab99cb363b49085639 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 18 Nov 2024 17:29:14 -0600 Subject: [PATCH 10/12] Aquaria: don't create regions during initialization --- worlds/aquaria/Regions.py | 14 ++++++++------ worlds/aquaria/__init__.py | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 7a41e0d0c864..5204d9381c1a 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -1396,12 +1396,7 @@ def add_regions_to_world(self) -> None: self.__add_abyss_regions_to_world() self.__add_body_regions_to_world() - def __init__(self, multiworld: MultiWorld, player: int): - """ - Initialisation of the regions - """ - self.multiworld = multiworld - self.player = player + def create_regions(self) -> None: self.__create_home_water_area() self.__create_energy_temple() self.__create_openwater() @@ -1412,3 +1407,10 @@ def __init__(self, multiworld: MultiWorld, player: int): self.__create_abyss() self.__create_sunken_city() self.__create_body() + + def __init__(self, multiworld: MultiWorld, player: int): + """ + Initialisation of the regions + """ + self.multiworld = multiworld + self.player = player diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 1e58fc5fc6ef..806b6ac3167b 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -108,6 +108,7 @@ def create_regions(self) -> None: """ Create every Region in `regions` """ + self._regions.create_regions() self._regions.add_regions_to_world() self._regions.connect_regions() self._regions.add_event_locations() From 6b66e902f167106e3e5e864d7ea595e229ead055 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 18 Nov 2024 17:47:25 -0600 Subject: [PATCH 11/12] undo world changes that aren't strictly required --- worlds/messenger/subclasses.py | 2 +- worlds/oot/Dungeon.py | 2 +- worlds/sc2/test/test_Regions.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 916d01b03ee8..b60aeb179feb 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -42,7 +42,7 @@ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = N loc_dict = {loc: world.location_name_to_id.get(loc, None) for loc in locations} self.add_locations(loc_dict, MessengerLocation) - world.regions.append(self) + self.multiworld.regions.append(self) class MessengerLocation(Location): diff --git a/worlds/oot/Dungeon.py b/worlds/oot/Dungeon.py index c190bde433b7..149b210fee19 100644 --- a/worlds/oot/Dungeon.py +++ b/worlds/oot/Dungeon.py @@ -11,7 +11,7 @@ def __init__(self, world, name, hint, font_color): self.small_keys = [] self.dungeon_items = [] - for region in world.regions.get_regions(): + for region in world.regions: if region.dungeon == self.name: region.dungeon = self self.regions.append(region) diff --git a/worlds/sc2/test/test_Regions.py b/worlds/sc2/test/test_Regions.py index 286aba396fd8..c268b65da9a8 100644 --- a/worlds/sc2/test/test_Regions.py +++ b/worlds/sc2/test/test_Regions.py @@ -36,6 +36,6 @@ class TestGridGeneration(Sc2TestBase): } def test_size_matches_exclusions(self): - self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.world.regions) + self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions) # WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location - self.assertEqual(len(self.world.regions), 29) + self.assertEqual(len(self.multiworld.regions), 29) From e4e9cd9f127b6ac73c369130a6416ff4a71ab727 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 18 Nov 2024 18:15:13 -0600 Subject: [PATCH 12/12] ok one of them was --- test/general/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/general/__init__.py b/test/general/__init__.py index 53be7afacf7b..8afd84976540 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,8 +1,6 @@ from argparse import Namespace from typing import List, Optional, Tuple, Type, Union -from tornado.gen import multi - from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region from worlds import network_data_package from worlds.AutoWorld import World, call_all @@ -75,7 +73,7 @@ def generate_test_multiworld(players: int = 1) -> MultiWorld: :return: The generated test multiworld """ multiworld = setup_multiworld([TestWorld] * players, seed=0) - multiworld.regions += [Region("Menu", player_id, multiworld) for player_id in multiworld.player_ids] + multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)] return multiworld