From 096e682b18ef9171d765e945bc7971bc97d9a795 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 30 Jul 2021 20:18:03 +0200 Subject: [PATCH] FactorioClient: implement JSONPrint in the client --- FactorioClient.py | 112 ++++------------------------------ data/client.kv | 1 + kvui.py | 150 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 101 deletions(-) create mode 100644 kvui.py diff --git a/FactorioClient.py b/FactorioClient.py index e9adbc5d..174a8d50 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -32,102 +32,6 @@ else: gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv -def get_kivy_app(): - os.environ["KIVY_NO_CONSOLELOG"] = "1" - os.environ["KIVY_NO_FILELOG"] = "1" - os.environ["KIVY_NO_ARGS"] = "1" - from kivy.app import App - from kivy.base import ExceptionHandler, ExceptionManager, Config - from kivy.uix.gridlayout import GridLayout - from kivy.uix.textinput import TextInput - from kivy.uix.recycleview import RecycleView - from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem - from kivy.lang import Builder - - class FactorioManager(App): - def __init__(self, ctx): - super(FactorioManager, self).__init__() - self.ctx = ctx - self.commandprocessor = ctx.command_processor(ctx) - self.icon = r"data/icon.png" - - def build(self): - self.grid = GridLayout() - self.grid.cols = 1 - - self.tabs = TabbedPanel() - self.tabs.default_tab_text = "All" - self.title = "Archipelago Factorio Client" - pairs = [ - ("Client", "Archipelago"), - ("FactorioServer", "Factorio Server Log"), - ("FactorioWatcher", "Bridge Data Log"), - ] - self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs)) - for logger_name, display_name in pairs: - bridge_logger = logging.getLogger(logger_name) - panel = TabbedPanelItem(text=display_name) - panel.content = UILog(bridge_logger) - self.tabs.add_widget(panel) - - self.grid.add_widget(self.tabs) - textinput = TextInput(size_hint_y=None, height=30, multiline=False) - textinput.bind(on_text_validate=self.on_message) - self.grid.add_widget(textinput) - self.commandprocessor("/help") - return self.grid - - def on_stop(self): - self.ctx.exit_event.set() - - def on_message(self, textinput: TextInput): - try: - input_text = textinput.text.strip() - textinput.text = "" - - if self.ctx.input_requests > 0: - self.ctx.input_requests -= 1 - self.ctx.input_queue.put_nowait(input_text) - elif input_text: - self.commandprocessor(input_text) - except Exception as e: - logger.exception(e) - - - class LogtoUI(logging.Handler): - def __init__(self, on_log): - super(LogtoUI, self).__init__(logging.DEBUG) - self.on_log = on_log - - def handle(self, record: logging.LogRecord) -> None: - self.on_log(record) - - - class UILog(RecycleView): - cols = 1 - - def __init__(self, *loggers_to_handle, **kwargs): - super(UILog, self).__init__(**kwargs) - self.data = [] - for logger in loggers_to_handle: - logger.addHandler(LogtoUI(self.on_log)) - - def on_log(self, record: logging.LogRecord) -> None: - self.data.append({"text": record.getMessage()}) - - - class E(ExceptionHandler): - def handle_exception(self, inst): - logger.exception(inst) - return ExceptionManager.RAISE - - - ExceptionManager.add_handler(E()) - - Config.set("input", "mouse", "mouse,disable_multitouch") - Builder.load_file(Utils.local_path("data", "client.kv")) - return FactorioManager - class FactorioCommandProcessor(ClientCommandProcessor): ctx: FactorioContext @@ -150,6 +54,7 @@ class FactorioCommandProcessor(ClientCommandProcessor): class FactorioContext(CommonContext): command_processor = FactorioCommandProcessor game = "Factorio" + ui = None # updated by spinup server mod_version: Utils.Version = Utils.Version(0, 0, 0) @@ -185,8 +90,11 @@ class FactorioContext(CommonContext): self.print_to_game(args['text']) def on_print_json(self, args: dict): - text = self.raw_json_text_parser(copy.deepcopy(args["data"])) - logger.info(text) + if self.ui: + self.ui.print_json(copy.deepcopy(args["data"])) + else: + text = self.raw_json_text_parser(copy.deepcopy(args["data"])) + logger.info(text) if self.rcon_client: text = self.factorio_json_text_parser(args["data"]) self.print_to_game(text) @@ -215,9 +123,9 @@ async def game_watcher(ctx: FactorioContext): ctx.awaiting_bridge = False data = json.loads(ctx.rcon_client.send_command("/ap-sync")) if data["slot_name"] != ctx.auth: - logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}") + bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}") elif data["seed_name"] != ctx.seed_name: - logger.warning( + bridge_logger.warning( f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") else: data = data["info"] @@ -370,7 +278,9 @@ async def main(args): ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") if gui_enabled: input_task = None - ui_app = get_kivy_app()(ctx) + from kvui import FactorioManager + ui_app = FactorioManager(ctx) + ctx.ui = ui_app ui_task = asyncio.create_task(ui_app.async_run(), name="UI") else: input_task = asyncio.create_task(console_loop(ctx), name="Input") diff --git a/data/client.kv b/data/client.kv index 052ec723..593af7f8 100644 --- a/data/client.kv +++ b/data/client.kv @@ -11,6 +11,7 @@ size_hint_y: None height: self.texture_size[1] font_size: dp(20) + markup: True : viewclass: 'Row' scroll_y: 0 diff --git a/kvui.py b/kvui.py new file mode 100644 index 00000000..db69fd60 --- /dev/null +++ b/kvui.py @@ -0,0 +1,150 @@ +import os +import logging + +os.environ["KIVY_NO_CONSOLELOG"] = "1" +os.environ["KIVY_NO_FILELOG"] = "1" +os.environ["KIVY_NO_ARGS"] = "1" +from kivy.app import App +from kivy.base import ExceptionHandler, ExceptionManager, Config +from kivy.uix.gridlayout import GridLayout +from kivy.uix.textinput import TextInput +from kivy.uix.recycleview import RecycleView +from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem +from kivy.utils import escape_markup +from kivy.lang import Builder + +import Utils +from NetUtils import JSONtoTextParser, JSONMessagePart + +class GameManager(App): + logging_pairs = [ + ("Client", "Archipelago"), + ] + + def __init__(self, ctx): + self.ctx = ctx + self.commandprocessor = ctx.command_processor(ctx) + self.icon = r"data/icon.png" + self.json_to_kivy_parser = KivyJSONtoTextParser(ctx) + self.log_panels = {} + super(GameManager, self).__init__() + + def build(self): + self.grid = GridLayout() + self.grid.cols = 1 + + self.tabs = TabbedPanel() + self.tabs.default_tab_text = "All" + + self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) + for logger_name, name in + self.logging_pairs)) + + for logger_name, display_name in self.logging_pairs: + bridge_logger = logging.getLogger(logger_name) + panel = TabbedPanelItem(text=display_name) + self.log_panels[display_name] = panel.content = UILog(bridge_logger) + self.tabs.add_widget(panel) + + self.grid.add_widget(self.tabs) + textinput = TextInput(size_hint_y=None, height=30, multiline=False) + textinput.bind(on_text_validate=self.on_message) + self.grid.add_widget(textinput) + self.commandprocessor("/help") + return self.grid + + def on_stop(self): + self.ctx.exit_event.set() + + def on_message(self, textinput: TextInput): + try: + input_text = textinput.text.strip() + textinput.text = "" + + if self.ctx.input_requests > 0: + self.ctx.input_requests -= 1 + self.ctx.input_queue.put_nowait(input_text) + elif input_text: + self.commandprocessor(input_text) + except Exception as e: + logging.getLogger("Client").exception(e) + + def print_json(self, data): + text = self.json_to_kivy_parser(data) + self.log_panels["Archipelago"].on_message_markup(text) + self.log_panels["All"].on_message_markup(text) + + +class FactorioManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("FactorioServer", "Factorio Server Log"), + ("FactorioWatcher", "Bridge Data Log"), + ] + title = "Archipelago Factorio Client" + + def __init__(self, ctx): + super(FactorioManager, self).__init__(ctx) + + +class LogtoUI(logging.Handler): + def __init__(self, on_log): + super(LogtoUI, self).__init__(logging.DEBUG) + self.on_log = on_log + + def handle(self, record: logging.LogRecord) -> None: + self.on_log(record) + + +class UILog(RecycleView): + cols = 1 + + def __init__(self, *loggers_to_handle, **kwargs): + super(UILog, self).__init__(**kwargs) + self.data = [] + for logger in loggers_to_handle: + logger.addHandler(LogtoUI(self.on_log)) + + def on_log(self, record: logging.LogRecord) -> None: + self.data.append({"text": escape_markup(record.getMessage())}) + + def on_message_markup(self, text): + self.data.append({"text": text}) + + +class E(ExceptionHandler): + logger = logging.getLogger("Client") + + def handle_exception(self, inst): + self.logger.exception(inst) + return ExceptionManager.RAISE + + +class KivyJSONtoTextParser(JSONtoTextParser): + color_codes = { + # not exact color names, close enough but decent looking + "black": "000000", + "red": "EE0000", + "green": "00FF7F", + "yellow": "FAFAD2", + "blue": "6495ED", + "magenta": "EE00EE", + "cyan": "00EEEE", + "white": "FFFFFF" + } + + def _handle_color(self, node: JSONMessagePart): + colors = node["color"].split(";") + node["text"] = escape_markup(node["text"]) + for color in colors: + color_code = self.color_codes.get(color, None) + if color_code: + node["text"] = f"[color={color_code}]{node['text']}[/color]" + return self._handle_text(node) + return self._handle_text(node) + + +ExceptionManager.add_handler(E()) + +Config.set("input", "mouse", "mouse,disable_multitouch") +Builder.load_file(Utils.local_path("data", "client.kv"))