From 252bb69808346a2634c1d4a8a018f418aa63e055 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 25 May 2021 01:03:04 +0200 Subject: [PATCH] FactorioClient: Read Bridge file after a server log indicates that the file was written --- FactorioClient.py | 57 ++++--- FactorioClientGUI.py | 200 +------------------------ Main.py | 5 +- Utils.py | 2 +- data/factorio/mod_template/control.lua | 3 +- 5 files changed, 44 insertions(+), 223 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index 1b24fb02..20b5e8e9 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -65,6 +65,7 @@ class FactorioContext(CommonContext): super(FactorioContext, self).__init__(*args, **kwargs) self.send_index = 0 self.rcon_client = None + self.awaiting_bridge = False self.raw_json_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested): @@ -86,10 +87,10 @@ class FactorioContext(CommonContext): 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"])) + text = self.raw_json_text_parser(args["data"]) + logger.info(text) if self.rcon_client: - cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '') + cleaned_text = text.replace('"', '') self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") async def game_watcher(ctx: FactorioContext, bridge_file: str): @@ -97,27 +98,29 @@ async def game_watcher(ctx: FactorioContext, bridge_file: str): from worlds.factorio.Technologies import lookup_id_to_name bridge_counter = 0 try: - while 1: + while not ctx.exit_event.is_set(): if os.path.exists(bridge_file): bridge_logger.info("Found Factorio Bridge file.") - while 1: - with open(bridge_file) as f: - data = json.load(f) - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} - victory = data["victory"] - ctx.auth = data["slot_name"] - ctx.seed_name = data["seed_name"] + while not ctx.exit_event.is_set(): + if ctx.awaiting_bridge: + ctx.awaiting_bridge = False + with open(bridge_file) as f: + data = json.load(f) + research_data = data["research_done"] + research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} + victory = data["victory"] + ctx.auth = data["slot_name"] + ctx.seed_name = data["seed_name"] - if not ctx.finished_game and victory: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True - if ctx.locations_checked != research_data: - bridge_logger.info(f"New researches done: " - f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") - ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + if ctx.locations_checked != research_data: + bridge_logger.info(f"New researches done: " + f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") + ctx.locations_checked = research_data + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) await asyncio.sleep(1) else: bridge_counter += 1 @@ -160,8 +163,9 @@ async def factorio_server_watcher(ctx: FactorioContext): stream_factorio_output(factorio_process.stdout, factorio_queue) stream_factorio_output(factorio_process.stderr, factorio_queue) script_folder = None + progression_watcher = None try: - while 1: + while not ctx.exit_event.is_set(): while not factorio_queue.empty(): msg = factorio_queue.get() factorio_server_logger.info(msg) @@ -177,7 +181,10 @@ async def factorio_server_watcher(ctx: FactorioContext): if os.path.exists(bridge_file): os.remove(bridge_file) logging.info(f"Bridge File Path: {bridge_file}") - asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") + progression_watcher= asyncio.create_task( + game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") + if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg: + ctx.awaiting_bridge = True if ctx.rcon_client: while ctx.send_index < len(ctx.items_received): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] @@ -192,10 +199,16 @@ async def factorio_server_watcher(ctx: FactorioContext): ctx.send_index += 1 await asyncio.sleep(1) + except Exception as e: logging.exception(e) logging.error("Aborted Factorio Server Bridge") + finally: + factorio_process.terminate() + if progression_watcher: + await progression_watcher + async def main(): ctx = FactorioContext(None, None, True) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index 68357289..a2bc5806 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -3,211 +3,15 @@ import logging os.makedirs("logs", exist_ok=True) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) -import json -import string os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_ARGS"] = "1" -from concurrent.futures import ThreadPoolExecutor import asyncio -from queue import Queue -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger -from MultiServer import mark_raw +from CommonClient import server_loop, logger +from FactorioClient import FactorioContext, factorio_server_watcher -import Utils -import random -from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus - -from worlds.factorio.Technologies import lookup_id_to_name - -rcon_port = 24242 -rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) -save_name = "Archipelago" - - - -options = Utils.get_options() -executable = options["factorio_options"]["executable"] -bin_dir = os.path.dirname(executable) -if not os.path.isdir(bin_dir): - raise FileNotFoundError(bin_dir) -if not os.path.exists(executable): - if os.path.exists(executable + ".exe"): - executable = executable + ".exe" - else: - raise FileNotFoundError(executable) - -import sys -server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) - -threadpool = ThreadPoolExecutor(10) - - -class FactorioCommandProcessor(ClientCommandProcessor): - @mark_raw - def _cmd_factorio(self, text: str) -> bool: - """Send the following command to the bound Factorio Server.""" - if self.ctx.rcon_client: - result = self.ctx.rcon_client.send_command(text) - if result: - self.output(result) - return True - return False - - def _cmd_connect(self, address: str = "") -> bool: - """Connect to a MultiWorld Server""" - if not self.ctx.auth: - self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.") - return super(FactorioCommandProcessor, self)._cmd_connect(address) - - -class FactorioContext(CommonContext): - command_processor = FactorioCommandProcessor - - def __init__(self, *args, **kwargs): - 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: - await super(FactorioContext, self).server_auth(password_requested) - - await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': self.auth, 'version': Utils._version_tuple, - 'tags': ['AP'], - 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" - }]) - - def on_print(self, args: dict): - logger.info(args["text"]) - if self.rcon_client: - cleaned_text = args['text'].replace('"', '') - self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_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. - text = self.raw_json_text_parser(args["data"]) - logger.info(text) - if self.rcon_client: - cleaned_text = text.replace('"', '') - self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") - -async def game_watcher(ctx: FactorioContext, bridge_file: str): - bridge_logger = logging.getLogger("FactorioWatcher") - from worlds.factorio.Technologies import lookup_id_to_name - bridge_counter = 0 - try: - while not ctx.exit_event.is_set(): - if os.path.exists(bridge_file): - bridge_logger.info("Found Factorio Bridge file.") - while not ctx.exit_event.is_set(): - with open(bridge_file) as f: - data = json.load(f) - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} - victory = data["victory"] - ctx.auth = data["slot_name"] - ctx.seed_name = data["seed_name"] - - if not ctx.finished_game and victory: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - - if ctx.locations_checked != research_data: - bridge_logger.info(f"New researches done: " - f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") - ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) - await asyncio.sleep(1) - else: - bridge_counter += 1 - if bridge_counter >= 60: - bridge_logger.info( - "Did not find Factorio Bridge file, " - "waiting for mod to run, which requires the server to run, " - "which requires a player to be connected.") - bridge_counter = 0 - await asyncio.sleep(1) - except Exception as e: - logging.exception(e) - logging.error("Aborted Factorio Server Bridge") - - -def stream_factorio_output(pipe, queue): - def queuer(): - while 1: - text = pipe.readline().strip() - if text: - queue.put_nowait(text) - - from threading import Thread - - thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True) - thread.start() - - -async def factorio_server_watcher(ctx: FactorioContext): - import subprocess - import factorio_rcon - factorio_server_logger = logging.getLogger("FactorioServer") - factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)), - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.DEVNULL, - encoding="utf-8") - factorio_server_logger.info("Started Factorio Server") - factorio_queue = Queue() - stream_factorio_output(factorio_process.stdout, factorio_queue) - stream_factorio_output(factorio_process.stderr, factorio_queue) - script_folder = None - progression_watcher = None - try: - while not ctx.exit_event.is_set(): - while not factorio_queue.empty(): - msg = factorio_queue.get() - factorio_server_logger.info(msg) - if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: - ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) - # trigger lua interface confirmation - ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") - ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") - ctx.rcon_client.send_command("/ap-sync") - if not script_folder and "Write data path:" in msg: - script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip() - bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json") - if os.path.exists(bridge_file): - os.remove(bridge_file) - logging.info(f"Bridge File Path: {bridge_file}") - progression_watcher= asyncio.create_task( - game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") - if ctx.rcon_client: - while ctx.send_index < len(ctx.items_received): - 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: - item_name = lookup_id_to_name[item_id] - factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") - ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}') - ctx.send_index += 1 - await asyncio.sleep(1) - - - except Exception as e: - logging.exception(e) - logging.error("Aborted Factorio Server Bridge") - - finally: - factorio_process.terminate() - if progression_watcher: - await progression_watcher async def main(): ctx = FactorioContext(None, None, True) diff --git a/Main.py b/Main.py index ed0ac0e1..f3d8e429 100644 --- a/Main.py +++ b/Main.py @@ -502,7 +502,10 @@ def main(args, seed=None): minimum_versions = {"server": (0, 1, 1), "clients": client_versions} games = {} for slot in world.player_ids: - client_versions[slot] = (0, 0, 3) + if world.game[slot] == "Factorio": + client_versions[slot] = (1, 1, 2) + else: + client_versions[slot] = (0, 0, 3) games[slot] = world.game[slot] connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names} diff --git a/Utils.py b/Utils.py index b3f43d97..3c38e94f 100644 --- a/Utils.py +++ b/Utils.py @@ -12,7 +12,7 @@ class Version(typing.NamedTuple): minor: int build: int -__version__ = "0.1.1" +__version__ = "0.1.2" _version_tuple = tuplize_version(__version__) import builtins diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index 17d80e1c..dcb0b2e9 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -134,7 +134,7 @@ end) -- for testing script.on_event(defines.events.on_tick, function(event) - if event.tick%600 == 300 then + if event.tick%3600 == 300 then dumpInfo(game.forces["player"]) end end) @@ -186,6 +186,7 @@ function dumpInfo(force) end end game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0) + log("Archipelago Bridge File written for game tick ".. game.tick .. ".") -- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0) -- game.print("Sent progress to Archipelago.") end