Skip to content

Commit 508aa17

Browse files
committed
Merge branch 'main' into zgi-v2
2 parents 6dc1ad2 + eac3e3c commit 508aa17

File tree

671 files changed

+93395
-30271
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

671 files changed

+93395
-30271
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
worlds/blasphemous/region_data.py linguist-generated=true

.github/workflows/codeql-analysis.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747

4848
# Initializes the CodeQL tools for scanning.
4949
- name: Initialize CodeQL
50-
uses: github/codeql-action/init@v2
50+
uses: github/codeql-action/init@v3
5151
with:
5252
languages: ${{ matrix.language }}
5353
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
5858
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5959
# If this step fails, then you should remove it and run the build manually (see below)
6060
- name: Autobuild
61-
uses: github/codeql-action/autobuild@v2
61+
uses: github/codeql-action/autobuild@v3
6262

6363
# ℹ️ Command-line programs to run using the OS shell.
6464
# 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
7272
# make release
7373

7474
- name: Perform CodeQL Analysis
75-
uses: github/codeql-action/analyze@v2
75+
uses: github/codeql-action/analyze@v3

.github/workflows/unittests.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ jobs:
3737
- {version: '3.9'}
3838
- {version: '3.10'}
3939
- {version: '3.11'}
40+
- {version: '3.12'}
4041
include:
4142
- python: {version: '3.8'} # win7 compat
4243
os: windows-latest
43-
- python: {version: '3.11'} # current
44+
- python: {version: '3.12'} # current
4445
os: windows-latest
45-
- python: {version: '3.11'} # current
46+
- python: {version: '3.12'} # current
4647
os: macos-latest
4748

4849
steps:
@@ -70,7 +71,7 @@ jobs:
7071
os:
7172
- ubuntu-latest
7273
python:
73-
- {version: '3.11'} # current
74+
- {version: '3.12'} # current
7475

7576
steps:
7677
- uses: actions/checkout@v4
@@ -88,4 +89,4 @@ jobs:
8889
run: |
8990
source venv/bin/activate
9091
export PYTHONPATH=$(pwd)
91-
python test/hosting/__main__.py
92+
timeout 600 python test/hosting/__main__.py

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ venv/
150150
ENV/
151151
env.bak/
152152
venv.bak/
153-
.code-workspace
153+
*.code-workspace
154154
shell.nix
155155

156156
# Spyder project settings

BaseClasses.py

+217-105
Large diffs are not rendered by default.

BizHawkClient.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

3+
import sys
34
import ModuleUpdate
45
ModuleUpdate.update()
56

67
from worlds._bizhawk.context import launch
78

89
if __name__ == "__main__":
9-
launch()
10+
launch(*sys.argv[1:])

CommonClient.py

+73-32
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,21 @@ def get_ssl_context():
4545

4646

4747
class ClientCommandProcessor(CommandProcessor):
48+
"""
49+
The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
50+
when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
51+
52+
The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
53+
space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
54+
and method("one", "two", "three") without.
55+
56+
In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
57+
"""
4858
def __init__(self, ctx: CommonContext):
4959
self.ctx = ctx
5060

5161
def output(self, text: str):
62+
"""Helper function to abstract logging to the CommonClient UI"""
5263
logger.info(text)
5364

5465
def _cmd_exit(self) -> bool:
@@ -61,6 +72,7 @@ def _cmd_connect(self, address: str = "") -> bool:
6172
if address:
6273
self.ctx.server_address = None
6374
self.ctx.username = None
75+
self.ctx.password = None
6476
elif not self.ctx.server_address:
6577
self.output("Please specify an address.")
6678
return False
@@ -163,13 +175,14 @@ def _cmd_ready(self):
163175
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
164176

165177
def default(self, raw: str):
178+
"""The default message parser to be used when parsing any messages that do not match a command"""
166179
raw = self.ctx.on_user_say(raw)
167180
if raw:
168181
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
169182

170183

171184
class CommonContext:
172-
# Should be adjusted as needed in subclasses
185+
# The following attributes are used to Connect and should be adjusted as needed in subclasses
173186
tags: typing.Set[str] = {"AP"}
174187
game: typing.Optional[str] = None
175188
items_handling: typing.Optional[int] = None
@@ -251,7 +264,7 @@ def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int])
251264
starting_reconnect_delay: int = 5
252265
current_reconnect_delay: int = starting_reconnect_delay
253266
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
254-
ui = None
267+
ui: typing.Optional["kvui.GameManager"] = None
255268
ui_task: typing.Optional["asyncio.Task[None]"] = None
256269
input_task: typing.Optional["asyncio.Task[None]"] = None
257270
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
@@ -342,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing
342355

343356
self.item_names = self.NameLookupDict(self, "item")
344357
self.location_names = self.NameLookupDict(self, "location")
358+
self.versions = {}
359+
self.checksums = {}
345360

346361
self.jsontotextparser = JSONtoTextParser(self)
347362
self.rawjsontotextparser = RawJSONtoTextParser(self)
@@ -428,7 +443,10 @@ async def get_username(self):
428443
self.auth = await self.console_input()
429444

430445
async def send_connect(self, **kwargs: typing.Any) -> None:
431-
""" send `Connect` packet to log in to server """
446+
"""
447+
Send a `Connect` packet to log in to the server,
448+
additional keyword args can override any value in the connection packet
449+
"""
432450
payload = {
433451
'cmd': 'Connect',
434452
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@@ -438,6 +456,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
438456
if kwargs:
439457
payload.update(kwargs)
440458
await self.send_msgs([payload])
459+
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
441460

442461
async def console_input(self) -> str:
443462
if self.ui:
@@ -458,13 +477,15 @@ def cancel_autoreconnect(self) -> bool:
458477
return False
459478

460479
def slot_concerns_self(self, slot) -> bool:
480+
"""Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
461481
if slot == self.slot:
462482
return True
463483
if slot in self.slot_info:
464484
return self.slot in self.slot_info[slot].group_members
465485
return False
466486

467487
def is_echoed_chat(self, print_json_packet: dict) -> bool:
488+
"""Helper function for filtering out messages sent by self."""
468489
return print_json_packet.get("type", "") == "Chat" \
469490
and print_json_packet.get("team", None) == self.team \
470491
and print_json_packet.get("slot", None) == self.slot
@@ -496,13 +517,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]:
496517
"""Gets called before sending a Say to the server from the user.
497518
Returned text is sent, or sending is aborted if None is returned."""
498519
return text
499-
520+
500521
def on_ui_command(self, text: str) -> None:
501522
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
502523
The command processor is still called; this is just intended for command echoing."""
503524
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
504525

505526
def update_permissions(self, permissions: typing.Dict[str, int]):
527+
"""Internal method to parse and save server permissions from RoomInfo"""
506528
for permission_name, permission_flag in permissions.items():
507529
try:
508530
flag = Permission(permission_flag)
@@ -514,6 +536,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]):
514536
async def shutdown(self):
515537
self.server_address = ""
516538
self.username = None
539+
self.password = None
517540
self.cancel_autoreconnect()
518541
if self.server and not self.server.socket.closed:
519542
await self.server.socket.close()
@@ -550,26 +573,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str],
550573
needed_updates.add(game)
551574
continue
552575

553-
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
554-
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
555-
# no action required if local version is new enough
556-
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
557-
or remote_checksum != local_checksum:
558-
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
559-
cache_version: int = cached_game.get("version", 0)
560-
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
561-
# download remote version if cache is not new enough
562-
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
563-
or remote_checksum != cache_checksum:
564-
needed_updates.add(game)
576+
cached_version: int = self.versions.get(game, 0)
577+
cached_checksum: typing.Optional[str] = self.checksums.get(game)
578+
# no action required if cached version is new enough
579+
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
580+
or remote_checksum != cached_checksum:
581+
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
582+
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
583+
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
584+
and remote_checksum == local_checksum):
585+
self.update_game(network_data_package["games"][game], game)
565586
else:
566-
self.update_game(cached_game, game)
587+
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
588+
cache_version: int = cached_game.get("version", 0)
589+
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
590+
# download remote version if cache is not new enough
591+
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
592+
or remote_checksum != cache_checksum:
593+
needed_updates.add(game)
594+
else:
595+
self.update_game(cached_game, game)
567596
if needed_updates:
568597
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
569598

570599
def update_game(self, game_package: dict, game: str):
571600
self.item_names.update_game(game, game_package["item_name_to_id"])
572601
self.location_names.update_game(game, game_package["location_name_to_id"])
602+
self.versions[game] = game_package.get("version", 0)
603+
self.checksums[game] = game_package.get("checksum")
573604

574605
def update_data_package(self, data_package: dict):
575606
for game, game_data in data_package["games"].items():
@@ -611,6 +642,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
611642
logger.info(f"DeathLink: Received from {data['source']}")
612643

613644
async def send_death(self, death_text: str = ""):
645+
"""Helper function to send a deathlink using death_text as the unique death cause string."""
614646
if self.server and self.server.socket:
615647
logger.info("DeathLink: Sending death to your friends...")
616648
self.last_death_link = time.time()
@@ -624,6 +656,7 @@ async def send_death(self, death_text: str = ""):
624656
}])
625657

626658
async def update_death_link(self, death_link: bool):
659+
"""Helper function to set Death Link connection tag on/off and update the connection if already connected."""
627660
old_tags = self.tags.copy()
628661
if death_link:
629662
self.tags.add("DeathLink")
@@ -633,7 +666,7 @@ async def update_death_link(self, death_link: bool):
633666
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
634667

635668
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
636-
"""Displays an error messagebox"""
669+
"""Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
637670
if not self.ui:
638671
return None
639672
title = title or "Error"
@@ -660,17 +693,19 @@ def handle_connection_loss(self, msg: str) -> None:
660693
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
661694
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
662695

663-
def run_gui(self):
664-
"""Import kivy UI system and start running it as self.ui_task."""
696+
def make_gui(self) -> typing.Type["kvui.GameManager"]:
697+
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
665698
from kvui import GameManager
666699

667700
class TextManager(GameManager):
668-
logging_pairs = [
669-
("Client", "Archipelago")
670-
]
671701
base_title = "Archipelago Text Client"
672702

673-
self.ui = TextManager(self)
703+
return TextManager
704+
705+
def run_gui(self):
706+
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
707+
ui_class = self.make_gui()
708+
self.ui = ui_class(self)
674709
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
675710

676711
def run_cli(self):
@@ -983,6 +1018,7 @@ async def console_loop(ctx: CommonContext):
9831018

9841019

9851020
def get_base_parser(description: typing.Optional[str] = None):
1021+
"""Base argument parser to be reused for components subclassing off of CommonClient"""
9861022
import argparse
9871023
parser = argparse.ArgumentParser(description=description)
9881024
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -992,7 +1028,7 @@ def get_base_parser(description: typing.Optional[str] = None):
9921028
return parser
9931029

9941030

995-
def run_as_textclient():
1031+
def run_as_textclient(*args):
9961032
class TextContext(CommonContext):
9971033
# Text Mode to use !hint and such with games that have no text entry
9981034
tags = CommonContext.tags | {"TextOnly"}
@@ -1031,16 +1067,21 @@ async def main(args):
10311067
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
10321068
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
10331069
parser.add_argument("url", nargs="?", help="Archipelago connection url")
1034-
args = parser.parse_args()
1070+
args = parser.parse_args(args)
10351071

1072+
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
10361073
if args.url:
10371074
url = urllib.parse.urlparse(args.url)
1038-
args.connect = url.netloc
1039-
if url.username:
1040-
args.name = urllib.parse.unquote(url.username)
1041-
if url.password:
1042-
args.password = urllib.parse.unquote(url.password)
1075+
if url.scheme == "archipelago":
1076+
args.connect = url.netloc
1077+
if url.username:
1078+
args.name = urllib.parse.unquote(url.username)
1079+
if url.password:
1080+
args.password = urllib.parse.unquote(url.password)
1081+
else:
1082+
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
10431083

1084+
# use colorama to display colored text highlighting on windows
10441085
colorama.init()
10451086

10461087
asyncio.run(main(args))
@@ -1049,4 +1090,4 @@ async def main(args):
10491090

10501091
if __name__ == '__main__':
10511092
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
1052-
run_as_textclient()
1093+
run_as_textclient(*sys.argv[1:]) # default value for parse_args

0 commit comments

Comments
 (0)