forked from mirror/Archipelago
Merge branch 'main' into main
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user