mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-27 05:39:57 -07:00
Merge branch 'main' into main
This commit is contained in:
+1
-1
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user