import collections import logging import typing from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance if typing.TYPE_CHECKING: import BaseClasses CollectionRule = typing.Callable[[BaseClasses.CollectionState], bool] ItemRule = typing.Callable[[BaseClasses.Item], bool] else: CollectionRule = typing.Callable[[object], bool] ItemRule = typing.Callable[[object], bool] def locality_needed(multiworld: MultiWorld) -> bool: for player in multiworld.player_ids: if multiworld.worlds[player].options.local_items.value: return True if multiworld.worlds[player].options.non_local_items.value: return True # Group for group_id, group in multiworld.groups.items(): if set(multiworld.player_ids) == set(group["players"]): continue if group["local_items"]: return True if group["non_local_items"]: return True def locality_rules(multiworld: MultiWorld): if locality_needed(multiworld): forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \ collections.defaultdict(lambda: collections.defaultdict(set)) def forbid(sender: int, receiver: int, items: typing.Set[str]): forbid_data[sender][receiver].update(items) for receiving_player in multiworld.player_ids: local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.local_items.value if local_items: for sending_player in multiworld.player_ids: if receiving_player != sending_player: forbid(sending_player, receiving_player, local_items) non_local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.non_local_items.value if non_local_items: forbid(receiving_player, receiving_player, non_local_items) # Group for receiving_group_id, receiving_group in multiworld.groups.items(): if set(multiworld.player_ids) == set(receiving_group["players"]): continue if receiving_group["local_items"]: for sending_player in multiworld.player_ids: if sending_player not in receiving_group["players"]: forbid(sending_player, receiving_group_id, receiving_group["local_items"]) if receiving_group["non_local_items"]: for sending_player in multiworld.player_ids: if sending_player in receiving_group["players"]: forbid(sending_player, receiving_group_id, receiving_group["non_local_items"]) # create fewer lambda's to save memory and cache misses func_cache = {} for location in multiworld.get_locations(): if (location.player, location.item_rule) in func_cache: location.item_rule = func_cache[location.player, location.item_rule] # empty rule that just returns True, overwrite elif location.item_rule is Location.item_rule: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ i.name not in sending_blockers[i.player] # special rule, needs to also be fulfilled. else: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ i.name not in sending_blockers[i.player] and old_rule(i) def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: location = multiworld.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: if not location.advancement: location.progress_type = LocationProgressType.EXCLUDED else: logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): spot.access_rule = rule def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule # empty rule, replace instead of add if old_rule is Location.access_rule or old_rule is Entrance.access_rule: spot.access_rule = rule if combine == "and" else old_rule else: if combine == "and": spot.access_rule = lambda state: rule(state) and old_rule(state) else: spot.access_rule = lambda state: rule(state) or old_rule(state) def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule # empty rule if old_rule is Location.item_rule: location.item_rule = lambda i: i.name != item or i.player != player else: location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): """unused, but kept as a debugging tool.""" old_rule = location.item_rule location.item_rule = lambda i: i.name not in items and old_rule(i) def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule # empty rule, replace instead of add if old_rule is Location.item_rule: location.item_rule = rule if combine == "and" else old_rule else: if combine == "and": location.item_rule = lambda item: rule(item) and old_rule(item) else: location.item_rule = lambda item: rule(item) or old_rule(item) def item_name_in_location_names(state: "BaseClasses.CollectionState", item: str, player: int, location_name_player_pairs: typing.Sequence[typing.Tuple[str, int]]) -> bool: for location in location_name_player_pairs: if location_item_name(state, location[0], location[1]) == (item, player): return True return False def item_name_in_locations(item: str, player: int, locations: typing.Sequence["BaseClasses.Location"]) -> bool: for location in locations: if location.item and location.item.name == item and location.item.player == player: return True return False def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ typing.Optional[typing.Tuple[str, int]]: location = state.multiworld.get_location(location, player) if location.item is None: return None return location.item.name, location.item.player def allow_self_locking_items(spot: typing.Union[Location, Region], *item_names: str) -> None: """ This function sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there. spot: Location or Region that the item(s) are allowed to be placed in item_names: item name or names that are allowed to be placed in the Location or Region """ player = spot.player def add_allowed_rules(area: typing.Union[Location, Entrance], location: Location) -> None: def set_always_allow(location: Location, rule: typing.Callable) -> None: location.always_allow = rule for item_name in item_names: add_rule(area, lambda state, item_name=item_name: location_item_name(state, location.name, player) == (item_name, player), "or") set_always_allow(location, lambda state, item: item.player == player and item.name in [item_name for item_name in item_names]) if isinstance(spot, Region): for entrance in spot.entrances: for location in spot.locations: add_allowed_rules(entrance, location) else: add_allowed_rules(spot, spot)