Skip to content

Commit

Permalink
Merge branch 'main' into StardewValley/stardew-text-client
Browse files Browse the repository at this point in the history
  • Loading branch information
Jouramie authored Oct 15, 2024
2 parents 2666698 + 618564c commit 1c4efc8
Show file tree
Hide file tree
Showing 520 changed files with 91,901 additions and 29,476 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
worlds/blasphemous/region_data.py linguist-generated=true
7 changes: 4 additions & 3 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ jobs:
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: macos-latest

steps:
Expand Down Expand Up @@ -70,7 +71,7 @@ jobs:
os:
- ubuntu-latest
python:
- {version: '3.11'} # current
- {version: '3.12'} # current

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ venv/
ENV/
env.bak/
venv.bak/
.code-workspace
*.code-workspace
shell.nix

# Spyder project settings
Expand Down
318 changes: 213 additions & 105 deletions BaseClasses.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion BizHawkClient.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

import sys
import ModuleUpdate
ModuleUpdate.update()

from worlds._bizhawk.context import launch

if __name__ == "__main__":
launch()
launch(*sys.argv[1:])
69 changes: 50 additions & 19 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,21 @@ def get_ssl_context():


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

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

def _cmd_exit(self) -> bool:
Expand All @@ -61,6 +72,7 @@ def _cmd_connect(self, address: str = "") -> bool:
if address:
self.ctx.server_address = None
self.ctx.username = None
self.ctx.password = None
elif not self.ctx.server_address:
self.output("Please specify an address.")
return False
Expand Down Expand Up @@ -163,13 +175,14 @@ def _cmd_ready(self):
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")

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


class CommonContext:
# Should be adjusted as needed in subclasses
# The following attributes are used to Connect and should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None
Expand Down Expand Up @@ -251,7 +264,7 @@ def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int])
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui: typing.Optional["kvui.GameManager"] = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
Expand Down Expand Up @@ -428,7 +441,10 @@ async def get_username(self):
self.auth = await self.console_input()

async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """
"""
Send a `Connect` packet to log in to the server,
additional keyword args can override any value in the connection packet
"""
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
Expand All @@ -438,6 +454,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])

async def console_input(self) -> str:
if self.ui:
Expand All @@ -458,13 +475,15 @@ def cancel_autoreconnect(self) -> bool:
return False

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

def is_echoed_chat(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out messages sent by self."""
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
Expand Down Expand Up @@ -496,13 +515,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text

def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])

def update_permissions(self, permissions: typing.Dict[str, int]):
"""Internal method to parse and save server permissions from RoomInfo"""
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
Expand All @@ -514,6 +534,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]):
async def shutdown(self):
self.server_address = ""
self.username = None
self.password = None
self.cancel_autoreconnect()
if self.server and not self.server.socket.closed:
await self.server.socket.close()
Expand Down Expand Up @@ -611,6 +632,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
logger.info(f"DeathLink: Received from {data['source']}")

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

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

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

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

class TextManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Text Client"

self.ui = TextManager(self)
return TextManager

def run_gui(self):
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
ui_class = self.make_gui()
self.ui = ui_class(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")

def run_cli(self):
Expand Down Expand Up @@ -983,6 +1008,7 @@ async def console_loop(ctx: CommonContext):


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


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

# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
if url.scheme == "archipelago":
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")

# use colorama to display colored text highlighting on windows
colorama.init()

asyncio.run(main(args))
Expand All @@ -1049,4 +1080,4 @@ async def main(args):

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

0 comments on commit 1c4efc8

Please sign in to comment.