| 
									
										
										
										
											2021-06-06 17:50:48 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import os | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | import string | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | import copy | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | import subprocess | 
					
						
							| 
									
										
										
										
											2021-10-19 01:47:11 +02:00
										 |  |  | import time | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2021-10-19 01:47:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:19:18 +02:00
										 |  |  | import ModuleUpdate | 
					
						
							|  |  |  | ModuleUpdate.update() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | import factorio_rcon | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import colorama | 
					
						
							|  |  |  | import asyncio | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  | from queue import Queue | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | import Utils | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							| 
									
										
										
										
											2021-11-17 22:46:32 +01:00
										 |  |  |     Utils.init_logging("FactorioClient", exception_logger="Client") | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-15 23:52:03 +02:00
										 |  |  | from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | from MultiServer import mark_raw | 
					
						
							| 
									
										
										
										
											2021-08-04 05:40:51 +02:00
										 |  |  | from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-04 05:40:51 +02:00
										 |  |  | from worlds.factorio import Factorio | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-29 15:25:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | class FactorioCommandProcessor(ClientCommandProcessor): | 
					
						
							| 
									
										
										
										
											2021-06-06 17:50:48 +02:00
										 |  |  |     ctx: FactorioContext | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_factorio(self, text: str) -> bool: | 
					
						
							|  |  |  |         """Send the following command to the bound Factorio Server.""" | 
					
						
							|  |  |  |         if self.ctx.rcon_client: | 
					
						
							| 
									
										
										
										
											2021-08-03 09:48:07 -07:00
										 |  |  |             # TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds. | 
					
						
							|  |  |  |             self.ctx.print_to_game(f"/factorio {text}") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             result = self.ctx.rcon_client.send_command(text) | 
					
						
							|  |  |  |             if result: | 
					
						
							|  |  |  |                 self.output(result) | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-27 14:58:39 +02:00
										 |  |  |     def _cmd_resync(self): | 
					
						
							|  |  |  |         """Manually trigger a resync.""" | 
					
						
							|  |  |  |         self.ctx.awaiting_bridge = True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class FactorioContext(CommonContext): | 
					
						
							|  |  |  |     command_processor = FactorioCommandProcessor | 
					
						
							| 
									
										
										
										
											2021-07-12 20:07:02 +02:00
										 |  |  |     game = "Factorio" | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |     items_handling = 0b111  # full remote | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  |     # updated by spinup server | 
					
						
							|  |  |  |     mod_version: Utils.Version = Utils.Version(0, 0, 0) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |     def __init__(self, server_address, password): | 
					
						
							|  |  |  |         super(FactorioContext, self).__init__(server_address, password) | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  |         self.send_index: int = 0 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.rcon_client = None | 
					
						
							| 
									
										
										
										
											2021-05-25 01:03:04 +02:00
										 |  |  |         self.awaiting_bridge = False | 
					
						
							| 
									
										
										
										
											2021-10-16 19:40:27 +02:00
										 |  |  |         self.write_data_path = None | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  |         self.death_link_tick: int = 0  # last send death link on Factorio layer | 
					
						
							| 
									
										
										
										
											2021-06-06 17:41:06 +02:00
										 |  |  |         self.factorio_json_text_parser = FactorioJSONtoTextParser(self) | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |         self.energy_link_increment = 0 | 
					
						
							|  |  |  |         self.last_deplete = 0 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     async def server_auth(self, password_requested: bool = False): | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         if password_requested and not self.password: | 
					
						
							|  |  |  |             await super(FactorioContext, self).server_auth(password_requested) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |         if self.rcon_client: | 
					
						
							|  |  |  |             await get_info(self, self.rcon_client)  # retrieve current auth code | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise Exception("Cannot connect to a server with unknown own identity, " | 
					
						
							|  |  |  |                             "bridge to Factorio first.") | 
					
						
							| 
									
										
										
										
											2021-07-29 15:25:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-21 02:50:24 +01:00
										 |  |  |         await self.send_connect() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |     def on_print(self, args: dict): | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  |         super(FactorioContext, self).on_print(args) | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |         if self.rcon_client: | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  |             self.print_to_game(args['text']) | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def on_print_json(self, args: dict): | 
					
						
							|  |  |  |         if self.rcon_client: | 
					
						
							| 
									
										
										
										
											2021-07-31 01:53:06 +02:00
										 |  |  |             text = self.factorio_json_text_parser(copy.deepcopy(args["data"])) | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  |             self.print_to_game(text) | 
					
						
							| 
									
										
										
										
											2021-07-31 01:53:06 +02:00
										 |  |  |         super(FactorioContext, self).on_print_json(args) | 
					
						
							| 
									
										
										
										
											2021-06-06 17:50:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def savegame_name(self) -> str: | 
					
						
							| 
									
										
										
										
											2021-10-24 23:22:06 +02:00
										 |  |  |         return f"AP_{self.seed_name}_{self.auth}_Save.zip" | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  |     def print_to_game(self, text): | 
					
						
							| 
									
										
										
										
											2021-09-26 08:49:32 +02:00
										 |  |  |         self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] " | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |                                       f"{text}") | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  |     def on_deathlink(self, data: dict): | 
					
						
							|  |  |  |         if self.rcon_client: | 
					
						
							|  |  |  |             self.rcon_client.send_command(f"/ap-deathlink {data['source']}") | 
					
						
							| 
									
										
										
										
											2021-11-04 13:23:13 +01:00
										 |  |  |         super(FactorioContext, self).on_deathlink(data) | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-07 01:01:56 +02:00
										 |  |  |     def on_package(self, cmd: str, args: dict): | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         if cmd in {"Connected", "RoomUpdate"}: | 
					
						
							| 
									
										
										
										
											2021-08-07 01:01:56 +02:00
										 |  |  |             # catch up sync anything that is already cleared. | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |             if "checked_locations" in args and args["checked_locations"]: | 
					
						
							| 
									
										
										
										
											2021-08-27 10:41:29 -07:00
										 |  |  |                 self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  |                                                 item_name in args["checked_locations"]}) | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |             if cmd == "Connected" and self.energy_link_increment: | 
					
						
							|  |  |  |                 asyncio.create_task(self.send_msgs([{ | 
					
						
							| 
									
										
										
										
											2022-03-04 21:41:07 +01:00
										 |  |  |                     "cmd": "SetNotify", "keys": ["EnergyLink"] | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |                 }])) | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |         elif cmd == "SetReply": | 
					
						
							|  |  |  |             if args["key"] == "EnergyLink": | 
					
						
							|  |  |  |                 if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete: | 
					
						
							|  |  |  |                     # it's our deplete request | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |                     gained = int(args["original_value"] - args["value"]) | 
					
						
							|  |  |  |                     gained_text = Utils.format_SI_prefix(gained) + "J" | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |                     if gained: | 
					
						
							| 
									
										
										
										
											2022-04-29 02:58:36 +02:00
										 |  |  |                         logger.debug(f"EnergyLink: Received {gained_text}. " | 
					
						
							|  |  |  |                                      f"{Utils.format_SI_prefix(args['value'])}J remaining.") | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |                         self.rcon_client.send_command(f"/ap-energylink {gained}") | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |     def run_gui(self): | 
					
						
							|  |  |  |         from kvui import GameManager | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         class FactorioManager(GameManager): | 
					
						
							|  |  |  |             logging_pairs = [ | 
					
						
							|  |  |  |                 ("Client", "Archipelago"), | 
					
						
							|  |  |  |                 ("FactorioServer", "Factorio Server Log"), | 
					
						
							|  |  |  |                 ("FactorioWatcher", "Bridge Data Log"), | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             base_title = "Archipelago Factorio Client" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.ui = FactorioManager(self) | 
					
						
							|  |  |  |         self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | async def game_watcher(ctx: FactorioContext): | 
					
						
							| 
									
										
										
										
											2021-04-13 12:35:42 +02:00
										 |  |  |     bridge_logger = logging.getLogger("FactorioWatcher") | 
					
						
							| 
									
										
										
										
											2021-04-04 01:19:54 +02:00
										 |  |  |     from worlds.factorio.Technologies import lookup_id_to_name | 
					
						
							| 
									
										
										
										
											2021-10-19 01:49:51 +02:00
										 |  |  |     next_bridge = time.perf_counter() + 1 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-05-25 01:03:04 +02:00
										 |  |  |         while not ctx.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |             # TODO: restore on-demand refresh | 
					
						
							|  |  |  |             if ctx.rcon_client and time.perf_counter() > next_bridge: | 
					
						
							| 
									
										
										
										
											2021-10-19 01:49:51 +02:00
										 |  |  |                 next_bridge = time.perf_counter() + 1 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |                 ctx.awaiting_bridge = False | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                 data = json.loads(ctx.rcon_client.send_command("/ap-sync")) | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  |                 if not ctx.auth: | 
					
						
							|  |  |  |                     pass  # auth failed, wait for new attempt | 
					
						
							|  |  |  |                 elif data["slot_name"] != ctx.auth: | 
					
						
							| 
									
										
										
										
											2021-07-30 20:18:03 +02:00
										 |  |  |                     bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}") | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                 elif data["seed_name"] != ctx.seed_name: | 
					
						
							| 
									
										
										
										
											2021-07-30 20:18:03 +02:00
										 |  |  |                     bridge_logger.warning( | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |                         f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     data = data["info"] | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |                     research_data = data["research_done"] | 
					
						
							|  |  |  |                     research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} | 
					
						
							|  |  |  |                     victory = data["victory"] | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |                     await ctx.update_death_link(data["death_link"]) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                     if not ctx.finished_game and victory: | 
					
						
							|  |  |  |                         await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | 
					
						
							|  |  |  |                         ctx.finished_game = True | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                     if ctx.locations_checked != research_data: | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |                         bridge_logger.debug( | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                             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)}]) | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  |                     death_link_tick = data.get("death_link_tick", 0) | 
					
						
							|  |  |  |                     if death_link_tick != ctx.death_link_tick: | 
					
						
							|  |  |  |                         ctx.death_link_tick = death_link_tick | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |                         if "DeathLink" in ctx.tags: | 
					
						
							| 
									
										
										
										
											2022-02-23 21:13:17 -08:00
										 |  |  |                             asyncio.create_task(ctx.send_death()) | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |                     if ctx.energy_link_increment: | 
					
						
							|  |  |  |                         in_world_bridges = data["energy_bridges"] | 
					
						
							|  |  |  |                         if in_world_bridges: | 
					
						
							|  |  |  |                             in_world_energy = data["energy"] | 
					
						
							|  |  |  |                             if in_world_energy < (ctx.energy_link_increment * in_world_bridges): | 
					
						
							|  |  |  |                                 # attempt to refill | 
					
						
							|  |  |  |                                 ctx.last_deplete = time.time() | 
					
						
							|  |  |  |                                 asyncio.create_task(ctx.send_msgs([{ | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |                                     "cmd": "Set", "key": "EnergyLink", "operations": | 
					
						
							|  |  |  |                                         [{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges}, | 
					
						
							|  |  |  |                                          {"operation": "max", "value": 0}], | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |                                     "last_deplete": ctx.last_deplete | 
					
						
							|  |  |  |                                 }])) | 
					
						
							|  |  |  |                             # Above Capacity - (len(Bridges) * ENERGY_INCREMENT) | 
					
						
							|  |  |  |                             elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \ | 
					
						
							|  |  |  |                                 ctx.energy_link_increment*in_world_bridges: | 
					
						
							|  |  |  |                                 value = ctx.energy_link_increment * in_world_bridges | 
					
						
							|  |  |  |                                 asyncio.create_task(ctx.send_msgs([{ | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |                                     "cmd": "Set", "key": "EnergyLink", "operations": | 
					
						
							|  |  |  |                                         [{"operation": "add", "value": value}] | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |                                 }])) | 
					
						
							|  |  |  |                                 ctx.rcon_client.send_command( | 
					
						
							|  |  |  |                                     f"/ap-energylink -{value}") | 
					
						
							| 
									
										
										
										
											2022-04-29 02:58:36 +02:00
										 |  |  |                                 logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J") | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-19 01:47:11 +02:00
										 |  |  |             await asyncio.sleep(0.1) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     except Exception as e: | 
					
						
							|  |  |  |         logging.exception(e) | 
					
						
							|  |  |  |         logging.error("Aborted Factorio Server Bridge") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-09 17:26:53 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | def stream_factorio_output(pipe, queue, process): | 
					
						
							| 
									
										
										
										
											2022-10-09 04:10:22 +02:00
										 |  |  |     pipe.reconfigure(errors="replace") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     def queuer(): | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |         while process.poll() is None: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             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() | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     return thread | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def factorio_server_watcher(ctx: FactorioContext): | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     savegame_name = os.path.abspath(ctx.savegame_name) | 
					
						
							|  |  |  |     if not os.path.exists(savegame_name): | 
					
						
							|  |  |  |         logger.info(f"Creating savegame {savegame_name}") | 
					
						
							|  |  |  |         subprocess.run(( | 
					
						
							| 
									
										
										
										
											2021-07-13 03:44:41 +02:00
										 |  |  |             executable, "--create", savegame_name, "--preset", "archipelago" | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |         )) | 
					
						
							|  |  |  |     factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name, | 
					
						
							|  |  |  |                                          *(str(elem) for elem in server_args)), | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |                                         stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                                         stdout=subprocess.PIPE, | 
					
						
							|  |  |  |                                         stdin=subprocess.DEVNULL, | 
					
						
							|  |  |  |                                         encoding="utf-8") | 
					
						
							|  |  |  |     factorio_server_logger.info("Started Factorio Server") | 
					
						
							|  |  |  |     factorio_queue = Queue() | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process) | 
					
						
							|  |  |  |     stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-05-25 01:03:04 +02:00
										 |  |  |         while not ctx.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |             if factorio_process.poll(): | 
					
						
							|  |  |  |                 factorio_server_logger.info("Factorio server has exited.") | 
					
						
							|  |  |  |                 ctx.exit_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             while not factorio_queue.empty(): | 
					
						
							|  |  |  |                 msg = factorio_queue.get() | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |                 factorio_queue.task_done() | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-18 20:45:56 +02:00
										 |  |  |                 if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |                     ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  |                     if not ctx.server: | 
					
						
							|  |  |  |                         logger.info("Established bridge to Factorio Server. " | 
					
						
							|  |  |  |                                     "Ready to connect to Archipelago via /connect") | 
					
						
							| 
									
										
										
										
											2021-08-03 23:24:32 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |                 if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg: | 
					
						
							| 
									
										
										
										
											2021-05-25 01:03:04 +02:00
										 |  |  |                     ctx.awaiting_bridge = True | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |                     factorio_server_logger.debug(msg) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     factorio_server_logger.info(msg) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             if ctx.rcon_client: | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  |                 commands = {} | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |                 while ctx.send_index < len(ctx.items_received): | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |                     transfer_item: NetworkItem = ctx.items_received[ctx.send_index] | 
					
						
							|  |  |  |                     item_id = transfer_item.item | 
					
						
							|  |  |  |                     player_name = ctx.player_names[transfer_item.player] | 
					
						
							| 
									
										
										
										
											2021-08-04 05:40:51 +02:00
										 |  |  |                     if item_id not in Factorio.item_id_to_name: | 
					
						
							|  |  |  |                         factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}") | 
					
						
							| 
									
										
										
										
											2021-04-03 14:47:49 +02:00
										 |  |  |                     else: | 
					
						
							| 
									
										
										
										
											2021-08-04 05:40:51 +02:00
										 |  |  |                         item_name = Factorio.item_id_to_name[item_id] | 
					
						
							| 
									
										
										
										
											2021-04-13 11:14:05 +02:00
										 |  |  |                         factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  |                         commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}' | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |                     ctx.send_index += 1 | 
					
						
							| 
									
										
										
										
											2021-08-24 09:52:12 +02:00
										 |  |  |                 if commands: | 
					
						
							|  |  |  |                     ctx.rcon_client.send_commands(commands) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |             await asyncio.sleep(0.1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     except Exception as e: | 
					
						
							|  |  |  |         logging.exception(e) | 
					
						
							|  |  |  |         logging.error("Aborted Factorio Server Bridge") | 
					
						
							|  |  |  |         ctx.rcon_client = None | 
					
						
							|  |  |  |         ctx.exit_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     finally: | 
					
						
							|  |  |  |         factorio_process.terminate() | 
					
						
							| 
									
										
										
										
											2021-07-21 23:32:28 +02:00
										 |  |  |         factorio_process.wait(5) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  | async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient): | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     info = json.loads(rcon_client.send_command("/ap-rcon-info")) | 
					
						
							|  |  |  |     ctx.auth = info["slot_name"] | 
					
						
							|  |  |  |     ctx.seed_name = info["seed_name"] | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  |     # 0.2.0 addition, not present earlier | 
					
						
							|  |  |  |     death_link = bool(info.get("death_link", False)) | 
					
						
							| 
									
										
										
										
											2022-02-24 00:51:31 +01:00
										 |  |  |     ctx.energy_link_increment = info.get("energy_link", 0) | 
					
						
							|  |  |  |     logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}") | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |     if ctx.energy_link_increment and ctx.ui: | 
					
						
							|  |  |  |         ctx.ui.enable_energy_link() | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |     await ctx.update_death_link(death_link) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  | async def factorio_spinup_server(ctx: FactorioContext) -> bool: | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     savegame_name = os.path.abspath("Archipelago.zip") | 
					
						
							|  |  |  |     if not os.path.exists(savegame_name): | 
					
						
							|  |  |  |         logger.info(f"Creating savegame {savegame_name}") | 
					
						
							|  |  |  |         subprocess.run(( | 
					
						
							| 
									
										
										
										
											2021-07-13 03:44:41 +02:00
										 |  |  |             executable, "--create", savegame_name | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |         )) | 
					
						
							|  |  |  |     factorio_process = subprocess.Popen( | 
					
						
							|  |  |  |         (executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)), | 
					
						
							|  |  |  |         stderr=subprocess.PIPE, | 
					
						
							|  |  |  |         stdout=subprocess.PIPE, | 
					
						
							|  |  |  |         stdin=subprocess.DEVNULL, | 
					
						
							|  |  |  |         encoding="utf-8") | 
					
						
							|  |  |  |     factorio_server_logger.info("Started Information Exchange Factorio Server") | 
					
						
							|  |  |  |     factorio_queue = Queue() | 
					
						
							|  |  |  |     stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process) | 
					
						
							|  |  |  |     stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process) | 
					
						
							|  |  |  |     rcon_client = None | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-07-02 20:52:06 +02:00
										 |  |  |         while not ctx.auth: | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |             while not factorio_queue.empty(): | 
					
						
							|  |  |  |                 msg = factorio_queue.get() | 
					
						
							|  |  |  |                 factorio_server_logger.info(msg) | 
					
						
							| 
									
										
										
										
											2021-08-03 08:51:12 -07:00
										 |  |  |                 if "Loading mod AP-" in msg and msg.endswith("(data.lua)"): | 
					
						
							| 
									
										
										
										
											2021-07-29 15:26:13 +02:00
										 |  |  |                     parts = msg.split() | 
					
						
							|  |  |  |                     ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split("."))) | 
					
						
							| 
									
										
										
										
											2021-10-16 19:40:27 +02:00
										 |  |  |                 elif "Write data path: " in msg: | 
					
						
							|  |  |  |                     ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [") | 
					
						
							|  |  |  |                     if "AppData" in ctx.write_data_path: | 
					
						
							|  |  |  |                         logger.warning("It appears your mods are loaded from Appdata, " | 
					
						
							|  |  |  |                                        "this can lead to problems with multiple Factorio instances. " | 
					
						
							|  |  |  |                                        "If this is the case, you will get a file locked error running Factorio.") | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |                 if not rcon_client and "Starting RCON interface at IP ADDR:" in msg: | 
					
						
							|  |  |  |                     rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |                     if ctx.mod_version == ctx.__class__.mod_version: | 
					
						
							|  |  |  |                         raise Exception("No Archipelago mod was loaded. Aborting.") | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |                     await get_info(ctx, rcon_client) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |             await asyncio.sleep(0.01) | 
					
						
							| 
									
										
										
										
											2021-05-09 16:49:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     except Exception as e: | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  |         logger.exception(e, extra={"compact_gui": True}) | 
					
						
							|  |  |  |         msg = "Aborted Factorio Server Bridge" | 
					
						
							|  |  |  |         logger.error(msg) | 
					
						
							|  |  |  |         ctx.gui_error(msg, e) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |         ctx.exit_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     else: | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |         logger.info( | 
					
						
							|  |  |  |             f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}") | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2021-05-25 01:03:04 +02:00
										 |  |  |     finally: | 
					
						
							|  |  |  |         factorio_process.terminate() | 
					
						
							| 
									
										
										
										
											2021-07-21 23:32:28 +02:00
										 |  |  |         factorio_process.wait(5) | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |     return False | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  | async def main(args): | 
					
						
							|  |  |  |     ctx = FactorioContext(args.connect, args.password) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |     if gui_enabled: | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |         ctx.run_gui() | 
					
						
							|  |  |  |     ctx.run_cli() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  |     factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer") | 
					
						
							| 
									
										
										
										
											2022-01-22 20:35:30 +01:00
										 |  |  |     successful_launch = await factorio_server_task | 
					
						
							|  |  |  |     if successful_launch: | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |         factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer") | 
					
						
							|  |  |  |         progression_watcher = asyncio.create_task( | 
					
						
							|  |  |  |             game_watcher(ctx), name="FactorioProgressionWatcher") | 
					
						
							| 
									
										
										
										
											2021-07-02 01:58:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |         await ctx.exit_event.wait() | 
					
						
							|  |  |  |         ctx.server_address = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-09 16:02:45 +02:00
										 |  |  |         await progression_watcher | 
					
						
							|  |  |  |         await factorio_server_task | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |     await ctx.shutdown() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-06 17:41:06 +02:00
										 |  |  | class FactorioJSONtoTextParser(JSONtoTextParser): | 
					
						
							|  |  |  |     def _handle_color(self, node: JSONMessagePart): | 
					
						
							|  |  |  |         colors = node["color"].split(";") | 
					
						
							|  |  |  |         for color in colors: | 
					
						
							| 
									
										
										
										
											2022-01-18 06:16:16 +01:00
										 |  |  |             if color in self.color_codes: | 
					
						
							|  |  |  |                 node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]" | 
					
						
							| 
									
										
										
										
											2021-06-06 17:41:06 +02:00
										 |  |  |             return self._handle_text(node) | 
					
						
							| 
									
										
										
										
											2021-06-06 17:50:48 +02:00
										 |  |  |         return self._handle_text(node) | 
					
						
							| 
									
										
										
										
											2021-06-06 22:49:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2021-11-09 12:53:05 +01:00
										 |  |  |     parser = get_base_parser(description="Optional arguments to FactorioClient follow. " | 
					
						
							|  |  |  |                                          "Remaining arguments get passed into bound Factorio instance." | 
					
						
							|  |  |  |                                          "Refer to Factorio --help for those.") | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |     parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio') | 
					
						
							| 
									
										
										
										
											2021-07-21 14:42:33 +02:00
										 |  |  |     parser.add_argument('--rcon-password', help='Password to authenticate with RCON.') | 
					
						
							| 
									
										
										
										
											2022-08-01 14:57:30 -07:00
										 |  |  |     parser.add_argument('--server-settings', help='Factorio server settings configuration file.') | 
					
						
							| 
									
										
										
										
											2021-07-21 14:42:33 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     args, rest = parser.parse_known_args() | 
					
						
							| 
									
										
										
										
											2021-06-06 22:49:37 +02:00
										 |  |  |     colorama.init() | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |     rcon_port = args.rcon_port | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     rcon_password = args.rcon_password if args.rcon_password else ''.join( | 
					
						
							|  |  |  |         random.choice(string.ascii_letters) for x in range(32)) | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     factorio_server_logger = logging.getLogger("FactorioServer") | 
					
						
							|  |  |  |     options = Utils.get_options() | 
					
						
							|  |  |  |     executable = options["factorio_options"]["executable"] | 
					
						
							| 
									
										
										
										
											2022-08-01 14:57:30 -07:00
										 |  |  |     server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None) | 
					
						
							|  |  |  |     if server_settings: | 
					
						
							|  |  |  |         server_settings = os.path.abspath(server_settings) | 
					
						
							| 
									
										
										
										
											2021-08-15 13:46:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if not os.path.exists(os.path.dirname(executable)): | 
					
						
							|  |  |  |         raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") | 
					
						
							|  |  |  |     if os.path.isdir(executable):  # user entered a path to a directory, let's find the executable therein | 
					
						
							|  |  |  |         executable = os.path.join(executable, "factorio") | 
					
						
							|  |  |  |     if not os.path.isfile(executable): | 
					
						
							|  |  |  |         if os.path.isfile(executable + ".exe"): | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  |             executable = executable + ".exe" | 
					
						
							|  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-07-20 21:19:53 +02:00
										 |  |  |             raise FileNotFoundError(f"Path {executable} is not an executable file.") | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-01 14:57:30 -07:00
										 |  |  |     if server_settings and os.path.isfile(server_settings): | 
					
						
							|  |  |  |         server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest) | 
					
						
							| 
									
										
										
										
											2021-07-19 21:52:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |     asyncio.run(main(args)) | 
					
						
							| 
									
										
										
										
											2021-06-06 22:49:37 +02:00
										 |  |  |     colorama.deinit() |