| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  | import collections | 
					
						
							| 
									
										
										
										
											2024-01-02 08:03:39 -06:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2021-10-03 17:22:47 +02:00
										 |  |  | import typing | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-13 18:55:34 -05:00
										 |  |  | from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance | 
					
						
							| 
									
										
										
										
											2022-01-19 20:19:07 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-03 17:22:47 +02:00
										 |  |  | 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] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  | def locality_needed(multiworld: MultiWorld) -> bool: | 
					
						
							|  |  |  |     for player in multiworld.player_ids: | 
					
						
							|  |  |  |         if multiworld.worlds[player].options.local_items.value: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |         if multiworld.worlds[player].options.non_local_items.value: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Group | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |     for group_id, group in multiworld.groups.items(): | 
					
						
							|  |  |  |         if set(multiworld.player_ids) == set(group["players"]): | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  |             continue | 
					
						
							|  |  |  |         if group["local_items"]: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  |         if group["non_local_items"]: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  | def locality_rules(multiworld: MultiWorld): | 
					
						
							|  |  |  |     if locality_needed(multiworld): | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |         for receiving_player in multiworld.player_ids: | 
					
						
							|  |  |  |             local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.local_items.value | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             if local_items: | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |                 for sending_player in multiworld.player_ids: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |                     if receiving_player != sending_player: | 
					
						
							|  |  |  |                         forbid(sending_player, receiving_player, local_items) | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |             non_local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.non_local_items.value | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             if non_local_items: | 
					
						
							|  |  |  |                 forbid(receiving_player, receiving_player, non_local_items) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Group | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |         for receiving_group_id, receiving_group in multiworld.groups.items(): | 
					
						
							|  |  |  |             if set(multiworld.player_ids) == set(receiving_group["players"]): | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |                 continue | 
					
						
							|  |  |  |             if receiving_group["local_items"]: | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |                 for sending_player in multiworld.player_ids: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |                     if sending_player not in receiving_group["players"]: | 
					
						
							|  |  |  |                         forbid(sending_player, receiving_group_id, receiving_group["local_items"]) | 
					
						
							|  |  |  |             if receiving_group["non_local_items"]: | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |                 for sending_player in multiworld.player_ids: | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |                     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 = {} | 
					
						
							| 
									
										
										
										
											2024-05-12 12:51:20 -04:00
										 |  |  |         for location in multiworld.get_locations(): | 
					
						
							| 
									
										
										
										
											2022-10-17 03:22:02 +02:00
										 |  |  |             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.__class__.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) | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-02 08:03:39 -06:00
										 |  |  | def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     for loc_name in exclude_locations: | 
					
						
							| 
									
										
										
										
											2021-10-05 17:52:03 -05:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2024-01-02 08:03:39 -06:00
										 |  |  |             location = multiworld.get_location(loc_name, player) | 
					
						
							| 
									
										
										
										
											2021-10-05 17:52:03 -05:00
										 |  |  |         except KeyError as e:  # failed to find the given location. Check if it's a legitimate location | 
					
						
							| 
									
										
										
										
											2024-01-02 08:03:39 -06:00
										 |  |  |             if loc_name not in multiworld.worlds[player].location_name_to_id: | 
					
						
							| 
									
										
										
										
											2021-10-05 17:55:15 -05:00
										 |  |  |                 raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2024-04-14 13:37:48 -05:00
										 |  |  |             if not location.advancement: | 
					
						
							| 
									
										
										
										
											2024-01-02 08:03:39 -06:00
										 |  |  |                 location.progress_type = LocationProgressType.EXCLUDED | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") | 
					
						
							| 
									
										
										
										
											2021-07-14 08:24:34 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-01 16:36:14 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-22 07:24:36 +02:00
										 |  |  | def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     spot.access_rule = rule | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  | def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     old_rule = spot.access_rule | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  |     # empty rule, replace instead of add | 
					
						
							|  |  |  |     if old_rule is spot.__class__.access_rule: | 
					
						
							|  |  |  |         spot.access_rule = rule if combine == "and" else old_rule | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-22 07:24:36 +02:00
										 |  |  | def forbid_item(location: "BaseClasses.Location", item: str, player: int): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     old_rule = location.item_rule | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  |     # empty rule | 
					
						
							|  |  |  |     if old_rule is location.__class__.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) | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-22 07:24:36 +02:00
										 |  |  | def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     old_rule = location.item_rule | 
					
						
							|  |  |  |     location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-22 07:24:36 +02:00
										 |  |  | def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     """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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  | def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     old_rule = location.item_rule | 
					
						
							| 
									
										
										
										
											2022-10-14 22:52:45 +02:00
										 |  |  |     # empty rule, replace instead of add | 
					
						
							|  |  |  |     if old_rule is location.__class__.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) | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-23 22:12:55 +02:00
										 |  |  | 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: | 
					
						
							| 
									
										
										
										
											2023-03-13 18:55:34 -05:00
										 |  |  |         if location_item_name(state, location[0], location[1]) == (item, player): | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |             return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-23 22:12:55 +02:00
										 |  |  | 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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-13 18:55:34 -05:00
										 |  |  | def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ | 
					
						
							| 
									
										
										
										
											2022-08-22 07:24:36 +02:00
										 |  |  |         typing.Optional[typing.Tuple[str, int]]: | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |     location = state.multiworld.get_location(location, player) | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     if location.item is None: | 
					
						
							|  |  |  |         return None | 
					
						
							| 
									
										
										
										
											2021-10-03 17:22:47 +02:00
										 |  |  |     return location.item.name, location.item.player | 
					
						
							| 
									
										
										
										
											2023-03-13 18:55:34 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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) |