Merge branch 'main' into main

This commit is contained in:
Lexipherous
2025-03-21 09:57:36 +00:00
committed by GitHub
9 changed files with 53 additions and 22 deletions
+1 -1
View File
@@ -417,7 +417,7 @@ class CommonContext:
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """ """ `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 return
await self.server.socket.send(encode(msgs)) await self.server.socket.send(encode(msgs))
+6 -6
View File
@@ -324,7 +324,7 @@ class Context:
# General networking # General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: 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 return False
msg = self.dumper(msgs) msg = self.dumper(msgs)
try: try:
@@ -339,7 +339,7 @@ class Context:
return True return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool: 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 return False
try: try:
await endpoint.socket.send(msg) 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: async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
sockets = [] sockets = []
for endpoint in endpoints: 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) sockets.append(endpoint.socket)
try: try:
websockets.broadcast(sockets, msg) websockets.broadcast(sockets, msg)
@@ -924,7 +924,7 @@ async def on_client_joined(ctx: Context, client: Client):
"If your client supports it, " "If your client supports it, "
"you may have additional local commands you can list with /help.", "you may have additional local commands you can list with /help.",
{"type": "Tutorial"}) {"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! " 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 " "It may stop working in the future. If you are a player, please report this to the "
"client's developer.") "client's developer.")
@@ -2107,7 +2107,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
def _cmd_exit(self) -> bool: def _cmd_exit(self) -> bool:
"""Shutdown the server""" """Shutdown the server"""
try: try:
self.ctx.server.server.close() self.ctx.server.ws_server.close()
finally: finally:
self.ctx.exit_event.set() self.ctx.exit_event.set()
return True 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) await asyncio.wait_for(ctx.exit_event.wait(), ctx.auto_shutdown)
def inactivity_shutdown(): def inactivity_shutdown():
ctx.server.server.close() ctx.server.ws_server.close()
ctx.exit_event.set() ctx.exit_event.set()
if to_cancel: if to_cancel:
for task in to_cancel: for task in to_cancel:
+1 -1
View File
@@ -310,7 +310,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
await ctx.server await ctx.server
port = 0 port = 0
for wssocket in ctx.server.server.sockets: for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname() socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6: if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support # Prefer IPv4, as most users seem to not have working ipv6 support
+11 -1
View File
@@ -515,10 +515,15 @@ def _populate_sprite_table():
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.") logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool: 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): for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file)) 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(): class Sprite():
sprite_size = 28672 sprite_size = 28672
@@ -554,6 +559,11 @@ class Sprite():
self.sprite = filedata[0x80000:0x87000] self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380] self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9] 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'): elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename) self.from_zspr(filedata, filename)
else: else:
+12 -7
View File
@@ -1,9 +1,10 @@
from dataclasses import dataclass from dataclasses import dataclass
import os import os
import io
from typing import TYPE_CHECKING, Dict, List, Optional, cast from typing import TYPE_CHECKING, Dict, List, Optional, cast
import zipfile import zipfile
from BaseClasses import Location from BaseClasses import Location
from worlds.Files import APContainer from worlds.Files import APContainer, AutoPatchRegister
from .Enum import CivVICheckType from .Enum import CivVICheckType
from .Locations import CivVILocation, CivVILocationData from .Locations import CivVILocation, CivVILocationData
@@ -25,18 +26,22 @@ class CivTreeItem:
ui_tree_row: int ui_tree_row: int
class CivVIContainer(APContainer): class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
""" """
Responsible for generating the dynamic mod files for the Civ VI multiworld Responsible for generating the dynamic mod files for the Civ VI multiworld
""" """
game: Optional[str] = "Civilization VI" 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 = ""): player: Optional[int] = None, player_name: str = "", server: str = ""):
self.patch_data = patch_data if isinstance(patch_data, io.BytesIO):
self.file_path = base_path super().__init__(patch_data, player, player_name, server)
container_path = os.path.join(output_directory, base_path + ".apcivvi") else:
super().__init__(container_path, player, player_name, server) 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: def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
for filename, yml in self.patch_data.items(): for filename, yml in self.patch_data.items():
+8 -2
View File
@@ -8,8 +8,14 @@
## Installing the Archipelago Mod using Lumafly ## Installing the Archipelago Mod using Lumafly
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory. 1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" button near the "Archipelago" mod entry. 2. Install the Archipelago mods by doing either of the following:
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker. * 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! 3. Launch the game, you're all set!
### What to do if Lumafly fails to find your installation directory ### What to do if Lumafly fails to find your installation directory
+2
View File
@@ -28,6 +28,7 @@ class PortalPlando(PlandoConnections):
- entrance: Searing Crags - entrance: Searing Crags
exit: Glacial Peak Portal exit: Glacial Peak Portal
""" """
display_name = "Portal Plando Connections"
portals = [f"{portal} Portal" for portal in PORTALS] portals = [f"{portal} Portal" for portal in PORTALS]
shop_points = [point for points in SHOP_POINTS.values() for point in points] shop_points = [point for points in SHOP_POINTS.values() for point in points]
checkpoints = [point for points in CHECKPOINTS.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 exit: Dark Cave - Right
direction: both direction: both
""" """
display_name = "Transition Plando Connections"
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys()) entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
exits = frozenset(RANDOMIZED_CONNECTIONS.values()) exits = frozenset(RANDOMIZED_CONNECTIONS.values())
+8 -4
View File
@@ -32,7 +32,7 @@ class MessengerRules:
self.connection_rules = { self.connection_rules = {
# from ToTHQ # from ToTHQ
"Artificer's Portal": "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": "Shrink Down":
lambda state: state.has_all(NOTES, self.player), lambda state: state.has_all(NOTES, self.player),
# the shop # the shop
@@ -267,6 +267,8 @@ class MessengerRules:
# tower of time # tower of time
"Tower of Time Seal - Time Waster": "Tower of Time Seal - Time Waster":
self.has_dart, self.has_dart,
# corrupted future
"Corrupted Future - Key of Courage": lambda state: state.has("Magic Firefly", self.player),
# cloud ruins # cloud ruins
"Time Warp Mega Shard": "Time Warp Mega Shard":
lambda state: self.has_vertical(state) or self.can_dboost(state), 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) 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) multiworld.completion_condition[self.player] = lambda state: state.has("Do the Thing!", self.player)
if self.world.options.accessibility: # not locations accessibility 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): class MessengerHardRules(MessengerRules):
@@ -530,9 +532,11 @@ class MessengerOOBRules(MessengerRules):
self.world.options.accessibility.value = MessengerAccessibility.option_minimal 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 # 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("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("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") 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")
+4
View File
@@ -5,6 +5,10 @@
- New option `free_fly_blacklist` limits which cities can show up as a free fly location. - 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. - 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. - 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 ### Fixes