From cbbdb2948df6fa5f9211c4db4f0e90eeac19931d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Feb 2021 23:46:05 +0100 Subject: [PATCH] attach command to args dict --- MultiClient.py | 82 +++++++++++++++++---------------------- MultiServer.py | 101 +++++++++++++++++++++++-------------------------- NetUtils.py | 58 +++++++++++++++++++++++----- Utils.py | 2 +- 4 files changed, 133 insertions(+), 110 deletions(-) diff --git a/MultiClient.py b/MultiClient.py index 98f5dec5..7bee2fa5 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -79,7 +79,7 @@ class Context(): self.server_task = None self.server: typing.Optional[Endpoint] = None self.password = password - self.server_version = (0, 0, 0) + self.server_version = Version(0, 0, 0) self.team = None self.slot = None @@ -118,9 +118,9 @@ class Context(): async def send_msgs(self, msgs): if not self.server or not self.server.socket.open or self.server.socket.closed: return - await self.server.socket.send(dumps(msgs)) + await self.server.socket.send(encode(msgs)) - def consume_players_package(self, package:typing.List[tuple]): + def consume_players_package(self, package: typing.List[tuple]): self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team} @@ -131,6 +131,7 @@ def color_item(item_id: int, green: bool = False) -> str: item_colors.append("white_bg") return color(item_name, *item_colors) + START_RECONNECT_DELAY = 5 SNES_RECONNECT_DELAY = 5 SERVER_RECONNECT_DELAY = 5 @@ -160,8 +161,8 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes - -location_shop_order = [name for name, info in Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order +location_shop_order = [name for name, info in + Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()]) location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), @@ -711,12 +712,6 @@ async def snes_flush_writes(ctx: Context): await snes_write(ctx, writes) -async def send_msgs(websocket, msgs): - if not websocket or not websocket.open or websocket.closed: - return - await websocket.send(dumps(msgs)) - - async def server_loop(ctx: Context, address=None): global SERVER_RECONNECT_DELAY ctx.ui_node.send_connection_status(ctx) @@ -754,8 +749,8 @@ async def server_loop(ctx: Context, address=None): ctx.ui_node.send_connection_status(ctx) SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY async for data in ctx.server.socket: - for msg in loads(data): - await process_server_cmd(ctx, msg[0], msg[1]) + for msg in decode(data): + await process_server_cmd(ctx, msg) logger.warning('Disconnected from multiworld server, type /connect to reconnect') except WebUI.WaitingForUiException: pass @@ -776,7 +771,7 @@ async def server_loop(ctx: Context, address=None): ctx.auth = None ctx.items_received = [] ctx.locations_info = {} - ctx.server_version = (0, 0, 0) + ctx.server_version = Version(0, 0, 0) if ctx.server and ctx.server.socket is not None: await ctx.server.socket.close() ctx.server = None @@ -799,6 +794,8 @@ async def server_autoreconnect(ctx: Context): missing_unknown = re.compile("Unknown Location ID: (?P\d+)") + + def convert_unknown_missing(missing_items: list) -> list: missing = [] for location in missing_items: @@ -810,7 +807,8 @@ def convert_unknown_missing(missing_items: list) -> list: return missing -async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]): +async def process_server_cmd(ctx: Context, args: dict): + cmd = args["cmd"] if cmd == 'RoomInfo': logger.info('--------------------------------') logger.info('Room Information:') @@ -873,10 +871,11 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict] ctx.consume_players_package(args["players"]) msgs = [] if ctx.locations_checked: - msgs.append(['LocationChecks', - {"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}]) + msgs.append({"cmd": "LocationChecks", + "locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}) if ctx.locations_scouted: - msgs.append(['LocationScouts', {"locations": list(ctx.locations_scouted)}]) + msgs.append({"cmd": "LocationScouts", + "locations": list(ctx.locations_scouted)}) if msgs: await ctx.send_msgs(msgs) if ctx.finished_game: @@ -895,13 +894,12 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict] if start_index == 0: ctx.items_received = [] elif start_index != len(ctx.items_received): - sync_msg = [['Sync', None]] + sync_msg = [{'cmd': 'Sync'}] if ctx.locations_checked: - sync_msg.append(['LocationChecks', - {"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}]) + sync_msg.append({"cmd": "LocationChecks", + "locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}) await ctx.send_msgs(sync_msg) if start_index == len(ctx.items_received): - for item in args['items']: ctx.items_received.append(NetworkItem(*item)) ctx.watcher_event.set() @@ -916,7 +914,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict] ctx.locations_info[location] = (item, player) ctx.watcher_event.set() - elif cmd == 'ItemSent': # going away + elif cmd == 'ItemSent': # going away found = NetworkItem(*args["item"]) receiving_player = args["receiver"] ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player], @@ -925,12 +923,13 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict] get_item_name_from_id(found.item) in Items.progression_items) item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green') found_player = color(ctx.player_names[found.player], 'yellow' if found.player != ctx.slot else 'magenta') - receiving_player = color(ctx.player_names[receiving_player], 'yellow' if receiving_player != ctx.slot else 'magenta') + receiving_player = color(ctx.player_names[receiving_player], + 'yellow' if receiving_player != ctx.slot else 'magenta') logging.info( '%s sent %s to %s (%s)' % (found_player, item, receiving_player, color(get_location_name_from_address(found.location), 'blue_bg', 'white'))) - elif cmd == 'ItemFound': # going away + elif cmd == 'ItemFound': # going away found = NetworkItem(*args["item"]) ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item), get_location_name_from_address(found.location), found.player == ctx.slot, @@ -940,7 +939,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict] logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location), 'blue_bg', 'white'))) - elif cmd == 'Hint': # going away + elif cmd == 'Hint': # going away hints = [Utils.Hint(*hint) for hint in args["hints"]] for hint in hints: ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player], @@ -998,10 +997,11 @@ async def server_auth(ctx: Context, password_requested): ctx.awaiting_rom = False ctx.auth = ctx.rom auth = base64.b64encode(ctx.rom).decode() - await ctx.send_msgs([['Connect', { - 'password': ctx.password, 'name': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx), - 'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past" - }]]) + await ctx.send_msgs([{"cmd": 'Connect', + 'password': ctx.password, 'name': auth, 'version': Utils._version_tuple, + 'tags': get_tags(ctx), + 'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past" + }]) async def console_input(ctx: Context): @@ -1094,16 +1094,6 @@ class ClientCommandProcessor(CommandProcessor): self.output("No missing location checks found.") return True - def _cmd_show_items(self, toggle: str = "") -> bool: - """Toggle showing of items received across the team""" - if toggle: - self.ctx.found_items = toggle.lower() in {"1", "true", "on"} - else: - self.ctx.found_items = not self.ctx.found_items - logger.info(f"Set showing team items to {self.ctx.found_items}") - asyncio.create_task(self.ctx.send_msgs([['UpdateTags', get_tags(self.ctx)]])) - return True - def _cmd_slow_mode(self, toggle: str = ""): """Toggle slow mode, which limits how fast you send / receive items.""" if toggle: @@ -1120,7 +1110,7 @@ class ClientCommandProcessor(CommandProcessor): self.output("Web UI was never started.") def default(self, raw: str): - asyncio.create_task(self.ctx.send_msgs([['Say', {"text": raw}]])) + asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}])) async def console_loop(ctx: Context): @@ -1165,7 +1155,7 @@ async def track_locations(ctx: Context, roomid, roomdata): try: if roomid in location_shop_ids: - misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order)*3)+5) + misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5) for cnt, b in enumerate(misc_data): my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt] if int(b) > 0 and my_check not in ctx.locations_checked: @@ -1174,7 +1164,6 @@ async def track_locations(ctx: Context, roomid, roomdata): print(e) logger.info(f"Exception: {e}") - for location, (loc_roomid, loc_mask) in location_table_uw.items(): try: if location not in ctx.locations_checked and loc_roomid == roomid and ( @@ -1238,14 +1227,13 @@ async def track_locations(ctx: Context, roomid, roomdata): print(e) logger.info(f"Exception: {e}") - if new_locations: - await ctx.send_msgs([['LocationChecks', {"locations": new_locations}]]) + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}]) async def send_finished_game(ctx: Context): try: - await ctx.send_msgs([['StatusUpdate', {"status": CLientStatus.CLIENT_GOAL}]]) + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": CLientStatus.CLIENT_GOAL}]) ctx.finished_game = True except Exception as ex: logger.exception(ex) @@ -1343,7 +1331,7 @@ async def game_watcher(ctx: Context): if scout_location > 0 and scout_location not in ctx.locations_scouted: ctx.locations_scouted.add(scout_location) logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}') - await ctx.send_msgs([['LocationScouts', {"locations": [scout_location]}]]) + await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) await track_locations(ctx, roomid, roomdata) diff --git a/MultiServer.py b/MultiServer.py index 703d4e2d..c057d68c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -28,15 +28,15 @@ from fuzzywuzzy import process as fuzzy_process from worlds.alttp import Items, Regions import Utils from Utils import get_item_name_from_id, get_location_name_from_address, \ - _version_tuple, restricted_loads -from NetUtils import Node, Endpoint, CLientStatus, NetworkItem + _version_tuple, restricted_loads, Version +from NetUtils import Node, Endpoint, CLientStatus, NetworkItem, decode colorama.init() console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id)) class Client(Endpoint): - version: typing.List[int] = [0, 0, 0] + version = Version(0, 0, 0) tags: typing.List[str] = [] def __init__(self, socket: websockets.server.WebSocketServerProtocol, ctx: Context): @@ -47,7 +47,6 @@ class Client(Endpoint): self.slot = None self.send_index = 0 self.tags = [] - self.version = [0, 0, 0] self.messageprocessor = client_message_processor(ctx, self) self.ctx = weakref.ref(ctx) @@ -302,18 +301,18 @@ class Context(Node): def notify_all(self, text): logging.info("Notice (all): %s" % text) - self.broadcast_all([['Print', {"text": text}]]) + self.broadcast_all([{"cmd": "Print", "text": text}]) def notify_client(self, client: Client, text: str): if not client.auth: return logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) - asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}]])) + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}])) def notify_client_multiple(self, client: Client, texts: typing.List[str]): if not client.auth: return - asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}] for text in texts])) + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) def broadcast_team(self, team, msgs): for client in self.endpoints: @@ -321,7 +320,7 @@ class Context(Node): asyncio.create_task(self.send_msgs(client, msgs)) def broadcast_all(self, msgs): - msgs = dumps(msgs) + msgs = self.dumper(msgs) for endpoint in self.endpoints: if endpoint.auth: asyncio.create_task(self.send_encoded_msgs(endpoint, msgs)) @@ -333,9 +332,9 @@ class Context(Node): # separated out, due to compatibilty between clients def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): - cmd = dumps([["Hint", {"hints" : hints}]]) - texts = [['PrintHTML', format_hint(ctx, team, hint)] for hint in hints] - for _, text in texts: + cmd = ctx.dumper([{"cmd": "Hint", "hints" : hints}]) + texts = ([format_hint(ctx, team, hint)] for hint in hints) + for text in texts: logging.info("Notice (Team #%d): %s" % (team + 1, text)) for client in ctx.endpoints: if client.auth and client.team == team: @@ -343,8 +342,8 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None): - cmd = dumps([["RoomUpdate", - {"players": ctx.get_players_package()}]]) + cmd = ctx.dumper([{"cmd": "RoomUpdate", + "players": ctx.get_players_package()}]) if client is None: for client in ctx.endpoints: if client.team == team and client.auth: @@ -360,8 +359,8 @@ async def server(websocket, path, ctx: Context): try: await on_client_connected(ctx, client) async for data in websocket: - for msg in loads(data): - await process_client_cmd(ctx, client, msg[0], msg[1]) + for msg in decode(data): + await process_client_cmd(ctx, client, msg) except Exception as e: if not isinstance(e, websockets.WebSocketException): logging.exception(e) @@ -372,7 +371,8 @@ async def server(websocket, path, ctx: Context): async def on_client_connected(ctx: Context, client: Client): - await ctx.send_msgs(client, [['RoomInfo', { + await ctx.send_msgs(client, [{ + 'cmd': 'RoomInfo', 'password': ctx.password is not None, 'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client in ctx.endpoints if client.auth], @@ -384,7 +384,7 @@ async def on_client_connected(ctx: Context, client: Client): 'remaining_mode': ctx.remaining_mode, 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points - }]]) + }]) async def on_client_disconnected(ctx: Context, client: Client): @@ -420,12 +420,6 @@ async def countdown(ctx: Context, timer): ctx.notify_all(f'[Server]: GO') ctx.countdown_timer = 0 -async def missing(ctx: Context, client: Client, locations: list, checked_locations: list): - await ctx.send_msgs(client, [['Missing', { - 'locations': dumps(locations), - 'checked_locations': dumps(checked_locations) - }]]) - def get_players_string(ctx: Context): auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} @@ -459,9 +453,10 @@ def send_new_items(ctx: Context): continue items = get_received_items(ctx, client.team, client.slot) if len(items) > client.send_index: - asyncio.create_task(ctx.send_msgs(client, [ - ['ReceivedItems', {"index": client.send_index, - "items": tuplize_received_items(items)[client.send_index:]}]])) + asyncio.create_task(ctx.send_msgs(client, [{ + "cmd": "ReceivedItems", + "index": client.send_index, + "items": tuplize_received_items(items)[client.send_index:]}])) client.send_index = len(items) @@ -514,15 +509,16 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi for client in ctx.endpoints: if client.team == team and client.wants_item_notification: asyncio.create_task( - ctx.send_msgs(client, [['ItemFound', - {"item": NetworkItem(target_item, location, slot)}]])) + ctx.send_msgs(client, [{"cmd": "ItemFound", + "item": NetworkItem(target_item, location, slot)}])) ctx.location_checks[team, slot] |= known_locations send_new_items(ctx) if found_items: for client in ctx.endpoints: if client.team == team and client.slot == slot: - asyncio.create_task(ctx.send_msgs(client, [["RoomUpdate", {"hint_points": get_client_points(ctx, client)}]])) + asyncio.create_task(ctx.send_msgs(client, [{"cmd": "RoomUpdate", + "hint_points": get_client_points(ctx, client)}])) ctx.save() @@ -792,9 +788,6 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output( "Sorry, client forfeiting requires you to have beaten the game on this server." " You can ask the server admin for a /forfeit") - if self.client.version < [2, 1, 0]: - self.output( - "Your client is too old to send game beaten information. Please update, load you savegame and reconnect.") return False def _cmd_remaining(self) -> bool: @@ -823,9 +816,6 @@ class ClientMessageProcessor(CommonCommandProcessor): else: self.output( "Sorry, !remaining requires you to have beaten the game on this server") - if self.client.version < [2, 1, 0]: - self.output( - "Your client is too old to send game beaten information. Please update, load you savegame and reconnect.") return False @@ -978,20 +968,22 @@ def get_client_points(ctx: Context, client: Client) -> int: ctx.hint_cost * ctx.hints_used[client.team, client.slot]) -async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typing.Optional[dict]): +async def process_client_cmd(ctx: Context, client: Client, args: dict): + cmd: str = args["cmd"] + if type(cmd) is not str: - await ctx.send_msgs(client, [['InvalidCmd', {"text": f"Command should be str, got {type(cmd)}"}]]) + await ctx.send_msgs(client, [{"cmd": "InvalidCmd", "text": f"Command should be str, got {type(cmd)}"}]) return if args is not None and type(args) != dict: - await ctx.send_msgs(client, [['InvalidArguments', - {'text': f'Expected Optional[dict], got {type(args)} for {cmd}'}]]) + await ctx.send_msgs(client, [{"cmd": "InvalidArguments", + 'text': f'Expected Optional[dict], got {type(args)} for {cmd}'}]) return if cmd == 'Connect': if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \ 'game' not in args: - await ctx.send_msgs(client, [['InvalidArguments', {'text': 'Connect'}]]) + await ctx.send_msgs(client, [{'cmd': 'InvalidArguments', 'text': 'Connect'}]) return errors = set() @@ -1019,29 +1011,31 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin client.team = team client.slot = slot minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0)))) - if minver > tuple(args['version']): + if minver > args['version']: errors.add('IncompatibleVersion') if ctx.compatibility == 1 and "AP" not in args['tags']: errors.add('IncompatibleVersion') #only exact version match allowed - elif ctx.compatibility == 0 and tuple(args['version']) != _version_tuple: + elif ctx.compatibility == 0 and args['version'] != _version_tuple: errors.add('IncompatibleVersion') if errors: logging.info(f"A client connection was refused due to: {errors}") - await ctx.send_msgs(client, [['ConnectionRefused', {"errors": list(errors)}]]) + await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}]) else: ctx.client_ids[client.team, client.slot] = args["uuid"] client.auth = True client.version = args['version'] client.tags = args['tags'] - reply = [['Connected', {"team": client.team, "slot": client.slot, - "players": ctx.get_players_package(), - "missing_checks": get_missing_checks(ctx, client), - "items_checked": get_checked_checks(ctx, client)}]] + reply = [{ + "cmd": "Connected", + "team": client.team, "slot": client.slot, + "players": ctx.get_players_package(), + "missing_checks": get_missing_checks(ctx, client), + "items_checked": get_checked_checks(ctx, client)}] items = get_received_items(ctx, client.team, client.slot) if items: - reply.append(['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}]) + reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)}) client.send_index = len(items) await ctx.send_msgs(client, reply) @@ -1052,7 +1046,8 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin items = get_received_items(ctx, client.team, client.slot) if items: client.send_index = len(items) - await ctx.send_msgs(client, [['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}]]) + await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0, + "items": tuplize_received_items(items)}]) elif cmd == 'LocationChecks': register_location_checks(ctx, client.team, client.slot, args["locations"]) @@ -1061,7 +1056,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin locs = [] for location in args["locations"]: if type(location) is not int or 0 >= location > len(Regions.location_table): - await ctx.send_msgs(client, [['InvalidArguments', {"text": 'LocationScouts'}]]) + await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}]) return loc_name = list(Regions.location_table.keys())[location - 1] target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)] @@ -1074,11 +1069,11 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin locs.append([target_item, location, target_player]) # logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}") - await ctx.send_msgs(client, [['LocationInfo', {'locations': locs}]]) + await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) elif cmd == 'UpdateTags': if not args or type(args) is not list: - await ctx.send_msgs(client, [['InvalidArguments', {"text": 'UpdateTags'}]]) + await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'UpdateTags'}]) return client.tags = args @@ -1095,7 +1090,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin if cmd == 'Say': if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): - await ctx.send_msgs(client, [['InvalidArguments', {"text" : 'Say'}]]) + await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text" : 'Say'}]) return client.messageprocessor(args["text"]) diff --git a/NetUtils.py b/NetUtils.py index 843f2cfa..8498a45d 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -3,19 +3,58 @@ import asyncio import logging import typing import enum -from json import loads, dumps +from json import JSONEncoder, JSONDecoder import websockets + class JSONMessagePart(typing.TypedDict): type: typing.Optional[str] color: typing.Optional[str] text: typing.Optional[str] + +def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any: + if isinstance(obj, tuple) and hasattr(obj, "_fields"): # NamedTuple is not actually a parent class + data = obj._asdict() + data["class"] = obj.__class__.__name__ + return data + if isinstance(obj, (tuple, list)): + return tuple(_scan_for_TypedTuples(o) for o in obj) + if isinstance(obj, dict): + return {key: _scan_for_TypedTuples(value) for key, value in obj.items()} + return obj + + +_encode = JSONEncoder( + ensure_ascii=False, + check_circular=False, +).encode + + +def encode(obj): + return _encode(_scan_for_TypedTuples(obj)) + +from Utils import Version # for object hook +whitelist = {"NetworkPlayer", "NetworkItem", "Version"} + +def _object_hook(o: typing.Any) -> typing.Any: + if isinstance(o, dict): + cls = o.get("class", None) + if cls in whitelist: + del (o["class"]) + return globals()[cls](**o) + + return o + + +decode = JSONDecoder(object_hook=_object_hook).decode + + class Node: endpoints: typing.List - dumper = staticmethod(dumps) - loader = staticmethod(loads) + dumper = staticmethod(encode) + loader = staticmethod(decode) def __init__(self): self.endpoints = [] @@ -26,13 +65,14 @@ class Node: for endpoint in self.endpoints: asyncio.create_task(self.send_encoded_msgs(endpoint, msgs)) - async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[typing.Sequence[str, typing.Optional[dict]]]): + async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]): if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed: return + msg = self.dumper(msgs) try: - await endpoint.socket.send(self.dumper(msgs)) + await endpoint.socket.send(msg) except websockets.ConnectionClosed: - logging.exception("Exception during send_msgs") + logging.exception(f"Exception during send_msgs, could not send {msg}") await self.disconnect(endpoint) async def send_encoded_msgs(self, endpoint: Endpoint, msg: str): @@ -104,7 +144,7 @@ class JSONtoTextParser(metaclass=HandlerMeta): return node.get("text", "") def _handle_player_id(self, node: JSONMessagePart): - player = node["player"] + player = node["text"] node["color"] = 'yellow' if player != self.ctx.slot else 'magenta' node["text"] = self.ctx.player_names[player] return self._handle_color(node) @@ -115,7 +155,6 @@ class JSONtoTextParser(metaclass=HandlerMeta): return self._handle_color(node) - color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, 'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} @@ -136,6 +175,7 @@ class CLientStatus(enum.IntEnum): CLIENT_PLAYING = 20 CLIENT_GOAL = 30 + class NetworkPlayer(typing.NamedTuple): team: int slot: int @@ -146,4 +186,4 @@ class NetworkPlayer(typing.NamedTuple): class NetworkItem(typing.NamedTuple): item: int location: int - player: int \ No newline at end of file + player: int diff --git a/Utils.py b/Utils.py index b7587b5d..9b977d40 100644 --- a/Utils.py +++ b/Utils.py @@ -10,7 +10,7 @@ def tuplize_version(version: str) -> typing.Tuple[int, ...]: class Version(typing.NamedTuple): major: int minor: int - micro: int + build: int __version__ = "0.0.1" _version_tuple = tuplize_version(__version__)