Compare commits

...

9 Commits

12 changed files with 62 additions and 31 deletions

View File

@@ -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))

View File

@@ -506,7 +506,7 @@ class LinksAwakeningContext(CommonContext):
la_task = None
client = None
# TODO: does this need to re-read on reset?
found_checks = []
found_checks = set()
last_resend = time.time()
magpie_enabled = False
@@ -558,10 +558,6 @@ class LinksAwakeningContext(CommonContext):
self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_checks(self):
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
await self.send_msgs(message)
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions
@@ -613,8 +609,8 @@ class LinksAwakeningContext(CommonContext):
self.client.pending_deathlink = True
def new_checks(self, item_ids, ladxr_ids):
self.found_checks += item_ids
create_task_log_exception(self.send_checks())
self.found_checks.update(item_ids)
create_task_log_exception(self.check_locations(self.found_checks))
if self.magpie_enabled:
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
@@ -721,7 +717,7 @@ class LinksAwakeningContext(CommonContext):
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
await self.check_locations(self.found_checks)
if self.magpie_enabled:
try:
self.magpie.set_checks(self.client.tracker.all_checks)

View File

@@ -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:

View File

@@ -272,7 +272,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

View File

@@ -1,5 +1,5 @@
colorama>=0.4.6
websockets>=15.0.1
websockets>=13.0.1,<14
PyYAML>=6.0.2
jellyfish>=1.1.0
jinja2>=3.1.4

View File

@@ -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:

View File

@@ -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():

View File

@@ -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

View File

@@ -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())

View File

@@ -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")

View File

@@ -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

View File

@@ -262,6 +262,10 @@ def is_easter_time() -> bool:
# Thus, we just take a range from the earliest to latest possible easter dates.
today = date.today()
if today < date(2025, 3, 31): # Don't go live early if 0.6.0 RC3 happens, with a little leeway
return False
earliest_easter_day = date(today.year, 3, 20) # Earliest possible is 3/22 + 2 day buffer for Good Friday
last_easter_day = date(today.year, 4, 26) # Latest possible is 4/25 + 1 day buffer for Easter Monday