diff --git a/CommonClient.py b/CommonClient.py index 6bd1bb4ce6..ae411838d8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -417,7 +417,7 @@ class CommonContext: async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: """ `msgs` JSON serializable """ - if not self.server or self.server.socket.state != websockets.protocol.State.OPEN: + if not self.server or not self.server.socket.open or self.server.socket.closed: return await self.server.socket.send(encode(msgs)) diff --git a/MultiServer.py b/MultiServer.py index a860342f06..f9ed34e2f7 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -324,7 +324,7 @@ class Context: # General networking async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: - if not endpoint.socket or endpoint.socket.state != websockets.protocol.State.OPEN: + if not endpoint.socket or not endpoint.socket.open: return False msg = self.dumper(msgs) try: @@ -339,7 +339,7 @@ class Context: return True async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool: - if not endpoint.socket or endpoint.socket.state != websockets.protocol.State.OPEN: + if not endpoint.socket or not endpoint.socket.open: return False try: await endpoint.socket.send(msg) @@ -355,7 +355,7 @@ class Context: async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool: sockets = [] for endpoint in endpoints: - if endpoint.socket and endpoint.socket.state == websockets.protocol.State.OPEN: + if endpoint.socket and endpoint.socket.open: sockets.append(endpoint.socket) try: websockets.broadcast(sockets, msg) @@ -924,7 +924,7 @@ async def on_client_joined(ctx: Context, client: Client): "If your client supports it, " "you may have additional local commands you can list with /help.", {"type": "Tutorial"}) - if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.protocol.extensions): + if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions): ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! " "It may stop working in the future. If you are a player, please report this to the " "client's developer.") @@ -2107,7 +2107,7 @@ class ServerCommandProcessor(CommonCommandProcessor): def _cmd_exit(self) -> bool: """Shutdown the server""" try: - self.ctx.server.server.close() + self.ctx.server.ws_server.close() finally: self.ctx.exit_event.set() return True @@ -2477,7 +2477,7 @@ async def auto_shutdown(ctx, to_cancel=None): await asyncio.wait_for(ctx.exit_event.wait(), ctx.auto_shutdown) def inactivity_shutdown(): - ctx.server.server.close() + ctx.server.ws_server.close() ctx.exit_event.set() if to_cancel: for task in to_cancel: diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index b7e6128213..07fc014a13 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -310,7 +310,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, await ctx.server port = 0 - for wssocket in ctx.server.server.sockets: + for wssocket in ctx.server.ws_server.sockets: socketname = wssocket.getsockname() if wssocket.family == socket.AF_INET6: # Prefer IPv4, as most users seem to not have working ipv6 support diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f658b930d0..5ed048e881 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -515,10 +515,15 @@ def _populate_sprite_table(): logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.") with concurrent.futures.ThreadPoolExecutor() as pool: - for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]: + sprite_paths = [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')] + for dir in [dir for dir in sprite_paths if os.path.isdir(dir)]: for file in os.listdir(dir): pool.submit(load_sprite_from_file, os.path.join(dir, file)) + if "link" not in _sprite_table: + logging.info("Link sprite was not loaded. Loading link from base rom") + load_sprite_from_file(get_base_rom_path()) + class Sprite(): sprite_size = 28672 @@ -554,6 +559,11 @@ class Sprite(): self.sprite = filedata[0x80000:0x87000] self.palette = filedata[0xDD308:0xDD380] self.glove_palette = filedata[0xDEDF5:0xDEDF9] + h = hashlib.md5() + h.update(filedata) + if h.hexdigest() == LTTPJPN10HASH: + self.name = "Link" + self.author_name = "Nintendo" elif filedata.startswith(b'ZSPR'): self.from_zspr(filedata, filename) else: diff --git a/worlds/civ_6/Container.py b/worlds/civ_6/Container.py index 26bb08c03b..f920781bd1 100644 --- a/worlds/civ_6/Container.py +++ b/worlds/civ_6/Container.py @@ -1,9 +1,10 @@ from dataclasses import dataclass import os +import io from typing import TYPE_CHECKING, Dict, List, Optional, cast import zipfile from BaseClasses import Location -from worlds.Files import APContainer +from worlds.Files import APContainer, AutoPatchRegister from .Enum import CivVICheckType from .Locations import CivVILocation, CivVILocationData @@ -25,18 +26,22 @@ class CivTreeItem: ui_tree_row: int -class CivVIContainer(APContainer): +class CivVIContainer(APContainer, metaclass=AutoPatchRegister): """ Responsible for generating the dynamic mod files for the Civ VI multiworld """ game: Optional[str] = "Civilization VI" + patch_file_ending = ".apcivvi" - def __init__(self, patch_data: Dict[str, str], base_path: str, output_directory: str, + def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "", player: Optional[int] = None, player_name: str = "", server: str = ""): - self.patch_data = patch_data - self.file_path = base_path - container_path = os.path.join(output_directory, base_path + ".apcivvi") - super().__init__(container_path, player, player_name, server) + if isinstance(patch_data, io.BytesIO): + super().__init__(patch_data, player, player_name, server) + else: + self.patch_data = patch_data + self.file_path = base_path + container_path = os.path.join(output_directory, base_path + ".apcivvi") + super().__init__(container_path, player, player_name, server) def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: for filename, yml in self.patch_data.items(): diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index 21cdcb68b3..25f7c78075 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -8,8 +8,14 @@ ## Installing the Archipelago Mod using Lumafly 1. Launch Lumafly and ensure it locates your Hollow Knight installation directory. -2. Click the "Install" button near the "Archipelago" mod entry. - * If desired, also install "Archipelago Map Mod" to use as an in-game tracker. +2. Install the Archipelago mods by doing either of the following: + * Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation. + * [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago) + * [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/) + (includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn, + and AdditionalMaps). + * Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod" + to use as an in-game tracker. 3. Launch the game, you're all set! ### What to do if Lumafly fails to find your installation directory diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 85c746aae7..c7a0f543ba 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -28,6 +28,7 @@ class PortalPlando(PlandoConnections): - entrance: Searing Crags exit: Glacial Peak Portal """ + display_name = "Portal Plando Connections" portals = [f"{portal} Portal" for portal in PORTALS] shop_points = [point for points in SHOP_POINTS.values() for point in points] checkpoints = [point for points in CHECKPOINTS.values() for point in points] @@ -48,6 +49,7 @@ class TransitionPlando(PlandoConnections): exit: Dark Cave - Right direction: both """ + display_name = "Transition Plando Connections" entrances = frozenset(RANDOMIZED_CONNECTIONS.keys()) exits = frozenset(RANDOMIZED_CONNECTIONS.values()) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 2a3434266f..2d5ee1b8a9 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -32,7 +32,7 @@ class MessengerRules: self.connection_rules = { # from ToTHQ "Artificer's Portal": - lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player), + lambda state: state.has("Demon King Crown", self.player), "Shrink Down": lambda state: state.has_all(NOTES, self.player), # the shop @@ -267,6 +267,8 @@ class MessengerRules: # tower of time "Tower of Time Seal - Time Waster": self.has_dart, + # corrupted future + "Corrupted Future - Key of Courage": lambda state: state.has("Magic Firefly", self.player), # cloud ruins "Time Warp Mega Shard": lambda state: self.has_vertical(state) or self.can_dboost(state), @@ -370,7 +372,7 @@ class MessengerRules: add_rule(multiworld.get_entrance("Shrink Down", self.player), self.has_dart) multiworld.completion_condition[self.player] = lambda state: state.has("Do the Thing!", self.player) if self.world.options.accessibility: # not locations accessibility - set_self_locking_items(self.world, self.player) + set_self_locking_items(self.world) class MessengerHardRules(MessengerRules): @@ -530,9 +532,11 @@ class MessengerOOBRules(MessengerRules): self.world.options.accessibility.value = MessengerAccessibility.option_minimal -def set_self_locking_items(world: "MessengerWorld", player: int) -> None: +def set_self_locking_items(world: "MessengerWorld") -> None: # locations where these placements are always valid allow_self_locking_items(world.get_location("Searing Crags - Key of Strength").parent_region, "Power Thistle") allow_self_locking_items(world.get_location("Sunken Shrine - Key of Love"), "Sun Crest", "Moon Crest") - allow_self_locking_items(world.get_location("Corrupted Future - Key of Courage").parent_region, "Demon King Crown") allow_self_locking_items(world.get_location("Elemental Skylands Seal - Water"), "Currents Master") + if not world.options.shuffle_transitions: + allow_self_locking_items(world.get_location("Corrupted Future - Key of Courage").parent_region, + "Demon King Crown") diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 8d33d70900..4721c58d5e 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -5,6 +5,10 @@ - New option `free_fly_blacklist` limits which cities can show up as a free fly location. - Spoiler log and hint text for maps where a species can be found now use human-friendly labels. - Added many item and location groups based on item type, location type, and location geography. +- Dexsanity locations for species which evolve via item use (Fire Stone, Metal Coat, etc.) now contribute those items to +the randomized item pool instead of Great Balls. +- Rock smash encounters are now randomized according to your wild pokemon randomization option. These encounters are +_not_ used for logical access (the seed will never require you to catch something through one of these encounters). ### Fixes