From ee30914b2ca2753e9f8384911368e951e04046fa Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 13 Apr 2021 14:49:32 +0200 Subject: [PATCH] Send AP text into Factorio worlds --- CommonClient.py | 17 ++++++++++++----- FactorioClient.py | 22 +++++++++++++++++++--- NetUtils.py | 25 ++++++++++++++++++++----- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 2b15c3ff..d5a39d72 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -129,7 +129,7 @@ class CommonContext(): self.input_requests = 0 # game state - self.player_names: typing.Dict[int: str] = {} + self.player_names: typing.Dict[int: str] = {0: "Server"} self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() @@ -195,6 +195,7 @@ class CommonContext(): 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} + self.player_names[0] = "Server" def event_invalid_slot(self): raise Exception('Invalid Slot; please verify that you have connected to the correct world.') @@ -213,6 +214,14 @@ class CommonContext(): await self.disconnect() self.server_task = asyncio.create_task(server_loop(self, address)) + def on_print(self, args: dict): + logger.info(args["text"]) + + def on_print_json(self, args: dict): + if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: + pass # don't want info on other player's local pickups. + logger.info(self.jsontotextparser(args["data"])) + async def server_loop(ctx: CommonContext, address=None): ui_node = getattr(ctx, "ui_node", None) @@ -394,12 +403,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.hint_points = args['hint_points'] elif cmd == 'Print': - logger.info(args["text"]) + ctx.on_print(args) elif cmd == 'PrintJSON': - if not ctx.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: - pass # don't want info on other player's local pickups. - logger.info(ctx.jsontotextparser(args["data"])) + ctx.on_print_json(args) elif cmd == 'InvalidArguments': logger.warning(f"Invalid Arguments: {args['text']}") diff --git a/FactorioClient.py b/FactorioClient.py index 98c409a1..b210cf3c 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -2,16 +2,18 @@ import os import logging import json import string +import copy from concurrent.futures import ThreadPoolExecutor import colorama import asyncio from queue import Queue, Empty -from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor +from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger from MultiServer import mark_raw import Utils import random +from NetUtils import RawJSONtoTextParser, NetworkItem from worlds.factorio.Technologies import lookup_id_to_name @@ -61,6 +63,7 @@ class FactorioContext(CommonContext): super(FactorioContext, self).__init__(*args, **kwargs) self.send_index = 0 self.rcon_client = None + self.raw_json_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested): if password_requested and not self.password: @@ -75,6 +78,18 @@ class FactorioContext(CommonContext): 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" }]) + def on_print(self, args: dict): + logger.info(args["text"]) + if self.rcon_client: + self.rcon_client.send_command(f"Archipelago: {args['text']}") + + def on_print_json(self, args: dict): + if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: + pass # don't want info on other player's local pickups. + copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently + logger.info(self.jsontotextparser(args["data"])) + if self.rcon_client: + self.rcon_client.send_command(f"Archipelago: {self.raw_json_text_parser(copy_data)}") async def game_watcher(ctx: FactorioContext): bridge_logger = logging.getLogger("FactorioWatcher") @@ -146,8 +161,9 @@ async def factorio_server_watcher(ctx: FactorioContext): ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") if ctx.rcon_client: while ctx.send_index < len(ctx.items_received): - item_id = ctx.items_received[ctx.send_index].item - player_name = ctx.player_names[ctx.send_index].player + transfer_item: NetworkItem = ctx.items_received[ctx.send_index] + item_id = transfer_item.item + player_name = ctx.player_names[transfer_item.player] if item_id not in lookup_id_to_name: logging.error(f"Cannot send unknown item ID: {item_id}") else: diff --git a/NetUtils.py b/NetUtils.py index ae89d681..524fb7bf 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -9,6 +9,7 @@ import websockets from Utils import Version + class JSONMessagePart(typing.TypedDict, total=False): text: str # optional @@ -18,7 +19,6 @@ class JSONMessagePart(typing.TypedDict, total=False): found: bool - class ClientStatus(enum.IntEnum): CLIENT_UNKNOWN = 0 CLIENT_CONNECTED = 5 @@ -61,10 +61,12 @@ _encode = JSONEncoder( def encode(obj): return _encode(_scan_for_TypedTuples(obj)) + def get_any_version(data: dict) -> Version: data = {key.lower(): value for key, value in data.items()} # .NET version classes have capitalized keys return Version(int(data["major"]), int(data["minor"]), int(data["build"])) + whitelist = {"NetworkPlayer": NetworkPlayer, "NetworkItem": NetworkItem, } @@ -73,6 +75,7 @@ custom_hooks = { "Version": get_any_version } + def _object_hook(o: typing.Any) -> typing.Any: if isinstance(o, dict): hook = custom_hooks.get(o.get("class", None), None) @@ -82,7 +85,7 @@ def _object_hook(o: typing.Any) -> typing.Any: if cls: for key in tuple(o): if key not in cls._fields: - del(o[key]) + del (o[key]) return cls(**o) return o @@ -151,11 +154,16 @@ class HandlerMeta(type): handlers = attrs["handlers"] = {} trigger: str = "_handle_" for base in bases: - handlers.update(base.commands) + handlers.update(base.handlers) handlers.update({handler_name[len(trigger):]: method for handler_name, method in attrs.items() if handler_name.startswith(trigger)}) orig_init = attrs.get('__init__', None) + if not orig_init: + for base in bases: + orig_init = getattr(base, '__init__', None) + if orig_init: + break def __init__(self, *args, **kwargs): # turn functions into bound methods @@ -167,6 +175,7 @@ class HandlerMeta(type): attrs['__init__'] = __init__ return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs) + class JSONTypes(str, enum.Enum): color = "color" text = "text" @@ -178,6 +187,7 @@ class JSONTypes(str, enum.Enum): location_id = "location_id" entrance_name = "entrance_name" + class JSONtoTextParser(metaclass=HandlerMeta): def __init__(self, ctx): self.ctx = ctx @@ -236,6 +246,11 @@ class JSONtoTextParser(metaclass=HandlerMeta): return self._handle_color(node) +class RawJSONtoTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(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} @@ -281,7 +296,7 @@ class Hint(typing.NamedTuple): add_json_text(parts, " is at ") add_json_text(parts, self.location, type="location_id") add_json_text(parts, " in ") - add_json_text(parts, self.finding_player, type ="player_id") + add_json_text(parts, self.finding_player, type="player_id") if self.entrance: add_json_text(parts, "'s World at ") add_json_text(parts, self.entrance, type="entrance_name") @@ -292,4 +307,4 @@ class Hint(typing.NamedTuple): else: add_json_text(parts, ".") - return {"cmd": "PrintJSON", "data": parts, "type": "hint"} \ No newline at end of file + return {"cmd": "PrintJSON", "data": parts, "type": "hint"}