From 43cb9611fb28a02f594ed2170281043285ae8101 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 23 Aug 2024 17:05:30 -0700 Subject: [PATCH 1/8] Core: some typing and cleaning in `BaseClasses.py` (#3391) * Core: some typing and cleaning in `BaseClasses.py` * more backwards `__repr__` * double-quote string * remove some end-of-line whitespace --- BaseClasses.py | 91 +++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 61f3f8f67c..29264f34ab 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -11,8 +11,10 @@ from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag -from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \ - TypedDict, Union, Type, ClassVar +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, + Optional, Protocol, Set, Tuple, Union, Type) + +from typing_extensions import NotRequired, TypedDict import NetUtils import Options @@ -22,16 +24,16 @@ if typing.TYPE_CHECKING: from worlds import AutoWorld -class Group(TypedDict, total=False): +class Group(TypedDict): name: str game: str world: "AutoWorld.World" - players: Set[int] - item_pool: Set[str] - replacement_items: Dict[int, Optional[str]] - local_items: Set[str] - non_local_items: Set[str] - link_replacement: bool + players: AbstractSet[int] + item_pool: NotRequired[Set[str]] + replacement_items: NotRequired[Dict[int, Optional[str]]] + local_items: NotRequired[Set[str]] + non_local_items: NotRequired[Set[str]] + link_replacement: NotRequired[bool] class ThreadBarrierProxy: @@ -48,6 +50,11 @@ class ThreadBarrierProxy: "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") +class HasNameAndPlayer(Protocol): + name: str + player: int + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -156,7 +163,7 @@ class MultiWorld(): self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} for player in range(1, players + 1): - def set_player_attr(attr, val): + def set_player_attr(attr: str, val) -> None: self.__dict__.setdefault(attr, {})[player] = val set_player_attr('plando_items', []) set_player_attr('plando_texts', {}) @@ -165,13 +172,13 @@ class MultiWorld(): set_player_attr('completion_condition', lambda state: True) self.worlds = {} self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " - "world's random object instead (usually self.random)") + "world's random object instead (usually self.random)") self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: return self.player_ids + tuple(self.groups) - def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: + def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]: """Create a group with name and return the assigned player ID and group. If a group of this name already exists, the set of players is extended instead of creating a new one.""" from worlds import AutoWorld @@ -195,7 +202,7 @@ class MultiWorld(): return new_id, new_group - def get_player_groups(self, player) -> Set[int]: + def get_player_groups(self, player: int) -> Set[int]: return {group_id for group_id, group in self.groups.items() if player in group["players"]} def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None): @@ -258,7 +265,7 @@ class MultiWorld(): "link_replacement": replacement_prio.index(item_link["link_replacement"]), } - for name, item_link in item_links.items(): + for _name, item_link in item_links.items(): current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups pool = set() local_items = set() @@ -388,7 +395,7 @@ class MultiWorld(): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) - def get_name_string_for_object(self, obj) -> str: + def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' def get_player_name(self, player: int) -> str: @@ -439,7 +446,7 @@ class MultiWorld(): def get_items(self) -> List[Item]: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]: + def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]: if resolve_group_locations: player_groups = self.get_player_groups(player) return [location for location in self.get_locations() if @@ -448,7 +455,7 @@ class MultiWorld(): return [location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player] - def find_item(self, item, player: int) -> Location: + def find_item(self, item: str, player: int) -> Location: return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) @@ -806,7 +813,7 @@ class CollectionState(): if found >= count: return True return False - + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" @@ -821,7 +828,7 @@ class CollectionState(): def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -900,7 +907,7 @@ class Entrance: addresses = None target = None - def __init__(self, player: int, name: str = '', parent: Region = None): + def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None: self.name = name self.parent_region = parent self.player = player @@ -920,9 +927,6 @@ class Entrance: region.entrances.append(self) def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1048,7 +1052,7 @@ class Region: self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -1088,9 +1092,6 @@ class Region: rules[connecting_region] if rules and connecting_region in rules else None) def __repr__(self): - return self.__str__() - - def __str__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' @@ -1109,9 +1110,9 @@ class Location: locked: bool = False show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT - always_allow = staticmethod(lambda state, item: False) + always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) - item_rule = staticmethod(lambda item: True) + item_rule: Callable[[Item], bool] = staticmethod(lambda item: True) item: Optional[Item] = None def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): @@ -1120,11 +1121,15 @@ class Location: self.address = address self.parent_region = parent - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items) - or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) - and self.item_rule(item) - and (not check_access or self.can_reach(state)))) + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + return (( + self.always_allow(state, item) + and item.name not in state.multiworld.worlds[item.player].options.non_local_items + ) or ( + (self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) + and self.item_rule(item) + and (not check_access or self.can_reach(state)) + )) def can_reach(self, state: CollectionState) -> bool: # Region.can_reach is just a cache lookup, so placing it first for faster abort on average @@ -1139,9 +1144,6 @@ class Location: self.locked = True def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1163,7 +1165,7 @@ class Location: @property def native_item(self) -> bool: """Returns True if the item in this location matches game.""" - return self.item and self.item.game == self.game + return self.item is not None and self.item.game == self.game @property def hint_text(self) -> str: @@ -1246,9 +1248,6 @@ class Item: return hash((self.name, self.player)) def __repr__(self) -> str: - return self.__str__() - - def __str__(self) -> str: if self.location and self.location.parent_region and self.location.parent_region.multiworld: return self.location.parent_region.multiworld.get_name_string_for_object(self) return f"{self.name} (Player {self.player})" @@ -1326,9 +1325,9 @@ class Spoiler: # in the second phase, we cull each sphere such that the game is still beatable, # reducing each range of influence to the bare minimum required inside it - restore_later = {} + restore_later: Dict[Location, Item] = {} for num, sphere in reversed(tuple(enumerate(collection_spheres))): - to_delete = set() + to_delete: Set[Location] = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, @@ -1346,7 +1345,7 @@ class Spoiler: sphere -= to_delete # second phase, sphere 0 - removed_precollected = [] + removed_precollected: List[Item] = [] for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) multiworld.precollected_items[item.player].remove(item) @@ -1499,9 +1498,9 @@ class Spoiler: if self.paths: outfile.write('\n\nPaths:\n\n') - path_listings = [] + path_listings: List[str] = [] for location, path in sorted(self.paths.items()): - path_lines = [] + path_lines: List[str] = [] for region, exit in path: if exit is not None: path_lines.append("{} -> {}".format(region, exit)) From 56dbba6a31a8be06d698155c3ef2d419c5b8388c Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:05:42 -0400 Subject: [PATCH 2/8] Celeste 64: Typo #3840 oops --- worlds/celeste64/docs/guide_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/celeste64/docs/guide_en.md b/worlds/celeste64/docs/guide_en.md index 24fea92e35..74ab94b913 100644 --- a/worlds/celeste64/docs/guide_en.md +++ b/worlds/celeste64/docs/guide_en.md @@ -28,7 +28,7 @@ An Example `AP.json` file: ``` { - "Url": "archipelago:12345", + "Url": "archipelago.gg:12345", "SlotName": "Maddy", "Password": "" } From 6efa065867332fc7fa91d3cc4add38c9fff96231 Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:06:08 -0500 Subject: [PATCH 3/8] Kingdom Hearts: Make Ceiling Division Human-Readable #3839 --- worlds/kh1/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py index c8cb71ffd6..e1f72f5b3e 100644 --- a/worlds/kh1/Rules.py +++ b/worlds/kh1/Rules.py @@ -1,5 +1,6 @@ from BaseClasses import CollectionState from worlds.generic.Rules import add_rule +from math import ceil SINGLE_PUPPIES = ["Puppy " + str(i).rjust(2,"0") for i in range(1,100)] TRIPLE_PUPPIES = ["Puppies " + str(3*(i-1)+1).rjust(2, "0") + "-" + str(3*(i-1)+3).rjust(2, "0") for i in range(1,34)] @@ -28,7 +29,7 @@ def has_puppies_all(state: CollectionState, player: int, puppies_required: int) return state.has("All Puppies", player) def has_puppies_triplets(state: CollectionState, player: int, puppies_required: int) -> bool: - return state.has_from_list_unique(TRIPLE_PUPPIES, player, -(puppies_required / -3)) + return state.has_from_list_unique(TRIPLE_PUPPIES, player, ceil(puppies_required / 3)) def has_puppies_individual(state: CollectionState, player: int, puppies_required: int) -> bool: return state.has_from_list_unique(SINGLE_PUPPIES, player, puppies_required) From e61d521ba8df5a1dfd1dda44a4e7c7e9989865a9 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:08:04 +0200 Subject: [PATCH 4/8] The Witness: Shuffle Dog (#3425) * Town Pet the Dog * Add shuffle dog to options presets * I cri evritim * I guess it's as good a time as any * :( * fix the soft conflict * add all the shuffle dog options to some of the unit tests bc why not * Laser Panels are just 'General' now, I'm pretty sure * Could I really call it allsanity? --- worlds/witness/__init__.py | 20 +++++++++++-------- worlds/witness/data/static_locations.py | 2 ++ worlds/witness/data/static_logic.py | 3 +++ worlds/witness/locations.py | 6 +----- worlds/witness/options.py | 14 +++++++++++-- worlds/witness/player_items.py | 2 +- worlds/witness/player_logic.py | 5 ++++- worlds/witness/presets.py | 6 ++++++ .../witness/test/test_roll_other_options.py | 3 +++ 9 files changed, 44 insertions(+), 17 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 02d11373b2..33c63eddbe 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -189,12 +189,13 @@ class WitnessWorld(World): event_locations.append(location_obj) # Place other locked items - dog_puzzle_skip = self.create_item("Puzzle Skip") - self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) - self.own_itempool.append(dog_puzzle_skip) + if self.options.shuffle_dog == "puzzle_skip": + dog_puzzle_skip = self.create_item("Puzzle Skip") + self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) - self.items_placed_early.append("Puzzle Skip") + self.own_itempool.append(dog_puzzle_skip) + self.items_placed_early.append("Puzzle Skip") if self.options.early_symbol_item: # Pick an early item to place on the tutorial gate. @@ -213,7 +214,7 @@ class WitnessWorld(World): self.own_itempool.append(gate_item) self.items_placed_early.append(random_early_item) - # There are some really restrictive settings in The Witness. + # There are some really restrictive options in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. # This is done both to prevent generation failures, but also to make the early game less linear. # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. @@ -221,11 +222,14 @@ class WitnessWorld(World): state = CollectionState(self.multiworld) state.sweep_for_advancements(locations=event_locations) - num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) + num_early_locs = sum( + 1 for loc in self.multiworld.get_reachable_locations(state, self.player) + if loc.address and not loc.item + ) - # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items + # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items - needed_size = 3 + needed_size = 2 needed_size += self.options.puzzle_randomization == "sigma_expert" needed_size += self.options.shuffle_symbols needed_size += self.options.shuffle_doors > 0 diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index d9566080a0..5c5ad554dd 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -104,6 +104,8 @@ GENERAL_LOCATIONS = { "Town RGB House Upstairs Right", "Town RGB House Sound Room Right", + "Town Pet the Dog", + "Windmill Theater Entry Panel", "Theater Exit Left Panel", "Theater Exit Right Panel", diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index b61b0f9d2f..87e1015257 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -147,6 +147,9 @@ class StaticWitnessLogicObj: elif "EP" in entity_name: entity_type = "EP" location_type = "EP" + elif "Pet the Dog" in entity_name: + entity_type = "Event" + location_type = "Good Boi" elif entity_hex.startswith("0xFF"): entity_type = "Event" location_type = None diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index d095b8bed4..49a4437c5a 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -19,7 +19,7 @@ class WitnessPlayerLocations: def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """Defines locations AFTER logic changes due to options""" - self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi"} self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy() if world.options.shuffle_discarded_panels: @@ -53,10 +53,6 @@ class WitnessPlayerLocations: if static_witness_logic.ENTITIES_BY_NAME[ch]["locationType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] - dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] - self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id - self.CHECK_PANELHEX_TO_ID = dict( sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1]) ) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 6f7222d5f9..f91e5218c3 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -129,12 +129,18 @@ class ShuffleEnvironmentalPuzzles(Choice): option_obelisk_sides = 2 -class ShuffleDog(Toggle): +class ShuffleDog(Choice): """ - Adds petting the Town dog into the location pool. + Adds petting the dog statue in Town into the location pool. + Alternatively, you can force it to be a Puzzle Skip. """ display_name = "Pet the Dog" + option_off = 0 + option_puzzle_skip = 1 + option_random_item = 2 + default = 1 + class EnvironmentalPuzzlesDifficulty(Choice): """ @@ -424,6 +430,7 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + shuffle_dog: ShuffleDog witness_option_groups = [ @@ -471,5 +478,8 @@ witness_option_groups = [ ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + ]), + OptionGroup("Silly Options", [ + ShuffleDog, ]) ] diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 44a959f2b4..3e09fe2ddb 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -215,7 +215,7 @@ class WitnessPlayerItems: item = self.item_data[item_name] if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child - # items were removed from the pool when we pruned out all progression items not in the settings. + # items were removed from the pool when we pruned out all progression items not in the options. output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code) for child_item in item.definition.child_item_names] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 3321983dd8..7313d8238c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -609,6 +609,9 @@ class WitnessPlayerLogic: adjustment_linesets_in_order.append(get_complex_doors()) adjustment_linesets_in_order.append(get_complex_additional_panels()) + if not world.options.shuffle_dog: + adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF80 (Town Pet the Dog)"]) + if world.options.shuffle_boat: adjustment_linesets_in_order.append(get_boat()) @@ -890,7 +893,7 @@ class WitnessPlayerLogic: ) def determine_unrequired_entities(self, world: "WitnessWorld") -> None: - """Figure out which major items are actually useless in this world's settings""" + """Figure out which major items are actually useless in this world's options""" # Gather quick references to relevant options eps_shuffled = world.options.shuffle_EPs diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 2a53484a4c..105514c91e 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -37,6 +37,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # For relative beginners who want to move to the next step. @@ -73,6 +75,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # Allsanity but without the BS (no expert, no tedious EPs). @@ -109,5 +113,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.option_random_item, }, } diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py index 3912ce252e..bea278a042 100644 --- a/worlds/witness/test/test_roll_other_options.py +++ b/worlds/witness/test/test_roll_other_options.py @@ -12,6 +12,7 @@ class TestExpertNonRandomizedEPs(WitnessTestBase): "victory_condition": "challenge", "shuffle_discarded_panels": False, "shuffle_boat": False, + "shuffle_dog": "off", } @@ -24,6 +25,7 @@ class TestVanillaAutoElevatorsPanels(WitnessTestBase): "early_caves": True, "shuffle_vault_boxes": True, "mountain_lasers": 11, + "shuffle_dog": "puzzle_skip", } @@ -46,6 +48,7 @@ class TestMaxEntityShuffle(WitnessTestBase): "obelisk_keys": True, "shuffle_lasers": "anywhere", "victory_condition": "mountain_box_long", + "shuffle_dog": "random_item", } From 35c9061c9cb3fbb12687e7afba73824c27e4ec1e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:08:46 +0200 Subject: [PATCH 5/8] The Witness: Switch to world.player_name (#3693) * lint * player_name * oops lmao * shorten --- worlds/witness/__init__.py | 15 ++++++++------- worlds/witness/hints.py | 13 +++++-------- worlds/witness/player_logic.py | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 33c63eddbe..ee5eba9150 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -128,10 +128,10 @@ class WitnessWorld(World): ) if not has_locally_relevant_progression and self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + warning(f"{self.player_name}'s Witness world doesn't have any progression" f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.") elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough" + raise OptionError(f"{self.player_name}'s Witness world doesn't have enough" f" progression items that can be placed in other players' worlds. Please turn on Symbol" f" Shuffle, Door Shuffle, or Obelisk Keys.") @@ -251,9 +251,10 @@ class WitnessWorld(World): self.player_locations.add_location_late(loc) self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) - player = self.multiworld.get_player_name(self.player) - - warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") + warning( + f"""Location "{loc}" had to be added to {self.player_name}'s world + due to insufficient sphere 1 size.""" + ) def create_items(self) -> None: # Determine pool size. @@ -290,7 +291,7 @@ class WitnessWorld(World): self.multiworld.push_precollected(self.create_item(inventory_item_name)) if len(item_pool) > pool_size: - error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})" + error(f"{self.player_name}'s Witness world has too few locations ({pool_size})" f" to place its necessary items ({len(item_pool)}).") return @@ -300,7 +301,7 @@ class WitnessWorld(World): num_puzzle_skips = self.options.puzzle_skip_amount.value if num_puzzle_skips > remaining_item_slots: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" + warning(f"{self.player_name}'s Witness world has insufficient locations" f" to place all requested puzzle skips.") num_puzzle_skips = remaining_item_slots item_pool["Puzzle Skip"] = num_puzzle_skips diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index c8ddf260d4..cd1d38f6e7 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" + location_name += " (" + world.player_name + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item.player) + ")" + item_name += " (" + world.player_name + ")" hint_text = "" area: Optional[str] = None @@ -388,8 +388,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp while len(hints) < hint_amount: if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Ran out of items/locations to hint for player {player_name}.") + logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.") break location_hint: Optional[WitnessLocationHint] @@ -590,8 +589,7 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount, hunt_panels)) if len(hinted_areas) < amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Was not able to make {amount} area hints for player {player_name}. " + logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. " f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.") return hints, unhinted_locations_per_area @@ -680,8 +678,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, # If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount if len(generated_hints) != hint_amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. " + logging.warning(f"Couldn't generate {hint_amount} hints for player {world.player_name}. " f"Generated {len(generated_hints)} instead.") return generated_hints diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 7313d8238c..b0e330c90c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -774,8 +774,7 @@ class WitnessPlayerLogic: # If we are disabling a laser, something has gone wrong. if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser": laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"] - player_name = world.multiworld.get_player_name(world.player) - raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}." + raise RuntimeError(f"Somehow, {laser_name} was disabled for player {world.player_name}." f" This is not allowed to happen, please report to Violet.") newly_discovered_disabled_entities.add(entity) From 6f617e302d65058fc4602991f465b3db2fbebe44 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 23 Aug 2024 20:09:50 -0400 Subject: [PATCH 6/8] Launcher: Update message that displays when installing a custom apworld for a game in main (#3607) --- worlds/LauncherComponents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 18c1a1661e..d127bbea36 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path break if found_already_loaded: raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n" - "so a Launcher restart is required to use the new installation.") + "so a Launcher restart is required to use the new installation.\n" + "If the Launcher is not open, no action needs to be taken.") world_source = worlds.WorldSource(str(target), is_zip=True) bisect.insort(worlds.world_sources, world_source) world_source.load() From 5c5f2ffc947a6fc2befd09f528694964e44edc9f Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 23 Aug 2024 19:12:01 -0500 Subject: [PATCH 7/8] kvui: assert kivy is not imported before kvui (#3823) --- kvui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kvui.py b/kvui.py index f83590a819..65cf52c7a4 100644 --- a/kvui.py +++ b/kvui.py @@ -5,6 +5,8 @@ import typing import re from collections import deque +assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility" + if sys.platform == "win32": import ctypes From d1a7fd7da118d9470c037f7374445f0f086ede6a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Fri, 23 Aug 2024 17:51:52 -0700 Subject: [PATCH 8/8] Pokemon Emerald: Send current map to trackers (#3726) --- worlds/pokemon_emerald/client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index a830957e9c..eeae3a5248 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -137,6 +137,8 @@ class PokemonEmeraldClient(BizHawkClient): previous_death_link: float ignore_next_death_link: bool + current_map: Optional[int] + def __init__(self) -> None: super().__init__() self.local_checked_locations = set() @@ -150,6 +152,7 @@ class PokemonEmeraldClient(BizHawkClient): self.death_counter = None self.previous_death_link = 0 self.ignore_next_death_link = False + self.current_map = None async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: from CommonClient import logger @@ -243,6 +246,7 @@ class PokemonEmeraldClient(BizHawkClient): sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little") + await self.handle_tracker_info(ctx, guards) await self.handle_death_link(ctx, guards) await self.handle_received_items(ctx, guards) await self.handle_wonder_trade(ctx, guards) @@ -403,6 +407,31 @@ class PokemonEmeraldClient(BizHawkClient): # Exit handler and return to main loop to reconnect pass + async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: + # Current map + sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") + + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(sb1_address + 0x4, 2, "System Bus")], + [guards["SAVE BLOCK 1"]] + ) + if read_result is None: # Save block moved + return + + current_map = int.from_bytes(read_result[0], "big") + if current_map != self.current_map: + self.current_map = current_map + await ctx.send_msgs([{ + "cmd": "Bounce", + "slots": [ctx.slot], + "tags": ["Tracker"], + "data": { + "type": "MapUpdate", + "mapId": current_map, + }, + }]) + async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: """ Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game