2021-06-06 15:50:48 +00:00
|
|
|
from __future__ import annotations
|
2021-04-01 09:40:58 +00:00
|
|
|
import os
|
|
|
|
import logging
|
|
|
|
import json
|
|
|
|
import string
|
2021-04-13 12:49:32 +00:00
|
|
|
import copy
|
2021-06-06 15:50:48 +00:00
|
|
|
import sys
|
2021-07-01 23:58:03 +00:00
|
|
|
import subprocess
|
|
|
|
import factorio_rcon
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
import colorama
|
|
|
|
import asyncio
|
2021-05-13 19:57:11 +00:00
|
|
|
from queue import Queue
|
2021-04-13 12:49:32 +00:00
|
|
|
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
2021-04-01 09:40:58 +00:00
|
|
|
from MultiServer import mark_raw
|
|
|
|
|
|
|
|
import Utils
|
|
|
|
import random
|
2021-06-06 15:41:06 +00:00
|
|
|
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
from worlds.factorio.Technologies import lookup_id_to_name
|
|
|
|
|
2021-07-19 19:52:08 +00:00
|
|
|
os.makedirs("logs", exist_ok=True)
|
|
|
|
|
|
|
|
# Log to file in gui case
|
|
|
|
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
|
|
|
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
|
|
|
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
|
|
|
|
else:
|
|
|
|
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
|
|
|
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
|
|
|
|
|
|
|
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
|
|
|
|
2021-07-21 12:42:33 +00:00
|
|
|
def get_kivy_app():
|
2021-07-19 19:52:08 +00:00
|
|
|
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"))
|
2021-07-21 12:42:33 +00:00
|
|
|
return FactorioManager
|
2021-05-09 15:26:53 +00:00
|
|
|
|
2021-07-29 13:25:45 +00:00
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
class FactorioCommandProcessor(ClientCommandProcessor):
|
2021-06-06 15:50:48 +00:00
|
|
|
ctx: FactorioContext
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
@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
|
|
|
|
|
2021-07-27 12:58:39 +00:00
|
|
|
def _cmd_resync(self):
|
|
|
|
"""Manually trigger a resync."""
|
|
|
|
self.ctx.awaiting_bridge = True
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
class FactorioContext(CommonContext):
|
|
|
|
command_processor = FactorioCommandProcessor
|
2021-07-12 18:07:02 +00:00
|
|
|
game = "Factorio"
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-07-29 13:26:13 +00:00
|
|
|
# updated by spinup server
|
|
|
|
mod_version: Utils.Version = Utils.Version(0, 0, 0)
|
|
|
|
|
2021-07-19 19:52:08 +00:00
|
|
|
def __init__(self, server_address, password):
|
|
|
|
super(FactorioContext, self).__init__(server_address, password)
|
2021-04-01 09:40:58 +00:00
|
|
|
self.send_index = 0
|
|
|
|
self.rcon_client = None
|
2021-05-24 23:03:04 +00:00
|
|
|
self.awaiting_bridge = False
|
2021-04-13 12:49:32 +00:00
|
|
|
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
2021-06-06 15:41:06 +00:00
|
|
|
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
async def server_auth(self, password_requested):
|
|
|
|
if password_requested and not self.password:
|
|
|
|
await super(FactorioContext, self).server_auth(password_requested)
|
|
|
|
|
2021-07-29 13:25:45 +00:00
|
|
|
if not self.auth:
|
|
|
|
if self.rcon_client:
|
|
|
|
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-04-01 09:40:58 +00:00
|
|
|
await self.send_msgs([{"cmd": 'Connect',
|
2021-06-18 20:15:54 +00:00
|
|
|
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
2021-04-01 09:40:58 +00:00
|
|
|
'tags': ['AP'],
|
|
|
|
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
|
|
|
|
}])
|
|
|
|
|
2021-04-13 12:49:32 +00:00
|
|
|
def on_print(self, args: dict):
|
|
|
|
logger.info(args["text"])
|
|
|
|
if self.rcon_client:
|
2021-07-29 13:26:13 +00:00
|
|
|
self.print_to_game(args['text'])
|
2021-04-13 12:49:32 +00:00
|
|
|
|
|
|
|
def on_print_json(self, args: dict):
|
2021-06-06 15:41:06 +00:00
|
|
|
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
|
2021-05-24 23:03:04 +00:00
|
|
|
logger.info(text)
|
2021-04-13 12:49:32 +00:00
|
|
|
if self.rcon_client:
|
2021-06-06 15:41:06 +00:00
|
|
|
text = self.factorio_json_text_parser(args["data"])
|
2021-07-29 13:26:13 +00:00
|
|
|
self.print_to_game(text)
|
2021-06-06 15:50:48 +00:00
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
@property
|
|
|
|
def savegame_name(self) -> str:
|
|
|
|
return f"AP_{self.seed_name}_{self.auth}.zip"
|
|
|
|
|
2021-07-29 13:26:13 +00:00
|
|
|
def print_to_game(self, text):
|
|
|
|
# TODO: remove around version 0.2
|
|
|
|
if self.mod_version < Utils.Version(0, 1, 6):
|
|
|
|
text = text.replace('"', '')
|
|
|
|
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
|
|
|
f"{text}\")")
|
|
|
|
else:
|
|
|
|
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
|
|
|
f"{text}")
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
async def game_watcher(ctx: FactorioContext):
|
2021-04-13 10:35:42 +00:00
|
|
|
bridge_logger = logging.getLogger("FactorioWatcher")
|
2021-04-03 23:19:54 +00:00
|
|
|
from worlds.factorio.Technologies import lookup_id_to_name
|
2021-04-01 09:40:58 +00:00
|
|
|
try:
|
2021-05-24 23:03:04 +00:00
|
|
|
while not ctx.exit_event.is_set():
|
2021-07-02 18:52:06 +00:00
|
|
|
if ctx.awaiting_bridge and ctx.rcon_client:
|
2021-07-01 23:58:03 +00:00
|
|
|
ctx.awaiting_bridge = False
|
2021-07-02 18:52:06 +00:00
|
|
|
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}")
|
|
|
|
elif data["seed_name"] != ctx.seed_name:
|
2021-07-19 19:52:08 +00:00
|
|
|
logger.warning(
|
|
|
|
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
2021-07-02 18:52:06 +00:00
|
|
|
else:
|
|
|
|
data = data["info"]
|
2021-07-01 23:58:03 +00:00
|
|
|
research_data = data["research_done"]
|
|
|
|
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
|
|
|
victory = data["victory"]
|
|
|
|
|
2021-07-02 18:52:06 +00:00
|
|
|
if not ctx.finished_game and victory:
|
|
|
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
|
|
ctx.finished_game = True
|
2021-07-01 23:58:03 +00:00
|
|
|
|
2021-07-02 18:52:06 +00:00
|
|
|
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)}])
|
2021-07-01 23:58:03 +00:00
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
logging.error("Aborted Factorio Server Bridge")
|
|
|
|
|
2021-05-09 15:26:53 +00:00
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
def stream_factorio_output(pipe, queue, process):
|
2021-04-01 09:40:58 +00:00
|
|
|
def queuer():
|
2021-07-01 23:58:03 +00:00
|
|
|
while process.poll() is None:
|
2021-04-01 09:40:58 +00: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-01 23:58:03 +00:00
|
|
|
return thread
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def factorio_server_watcher(ctx: FactorioContext):
|
2021-07-01 23:58:03 +00: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 01:44:41 +00:00
|
|
|
executable, "--create", savegame_name, "--preset", "archipelago"
|
2021-07-01 23:58:03 +00:00
|
|
|
))
|
|
|
|
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
|
|
|
|
*(str(elem) for elem in server_args)),
|
2021-04-01 09:40:58 +00: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-01 23:58:03 +00:00
|
|
|
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
|
|
|
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
2021-04-01 09:40:58 +00:00
|
|
|
try:
|
2021-05-24 23:03:04 +00:00
|
|
|
while not ctx.exit_event.is_set():
|
2021-07-01 23:58:03 +00:00
|
|
|
if factorio_process.poll():
|
|
|
|
factorio_server_logger.info("Factorio server has exited.")
|
|
|
|
ctx.exit_event.set()
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
while not factorio_queue.empty():
|
|
|
|
msg = factorio_queue.get()
|
|
|
|
factorio_server_logger.info(msg)
|
2021-05-18 18:45:56 +00:00
|
|
|
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
2021-04-01 09:40:58 +00:00
|
|
|
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
2021-07-02 18:52:06 +00:00
|
|
|
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
2021-05-24 23:03:04 +00:00
|
|
|
ctx.awaiting_bridge = True
|
2021-07-27 12:59:16 +00:00
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
if ctx.rcon_client:
|
|
|
|
while ctx.send_index < len(ctx.items_received):
|
2021-04-13 12:49:32 +00:00
|
|
|
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
|
|
|
item_id = transfer_item.item
|
|
|
|
player_name = ctx.player_names[transfer_item.player]
|
2021-04-03 12:47:49 +00:00
|
|
|
if item_id not in lookup_id_to_name:
|
2021-04-05 13:37:15 +00:00
|
|
|
logging.error(f"Cannot send unknown item ID: {item_id}")
|
2021-04-03 12:47:49 +00:00
|
|
|
else:
|
|
|
|
item_name = lookup_id_to_name[item_id]
|
2021-04-13 09:14:05 +00:00
|
|
|
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
2021-07-04 13:25:56 +00:00
|
|
|
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
2021-04-01 09:40:58 +00:00
|
|
|
ctx.send_index += 1
|
2021-07-01 23:58:03 +00: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 21:32:28 +00:00
|
|
|
factorio_process.wait(5)
|
2021-07-01 23:58:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_info(ctx, rcon_client):
|
|
|
|
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
|
|
|
ctx.auth = info["slot_name"]
|
|
|
|
ctx.seed_name = info["seed_name"]
|
|
|
|
|
|
|
|
|
|
|
|
async def factorio_spinup_server(ctx: FactorioContext):
|
|
|
|
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 01:44:41 +00:00
|
|
|
executable, "--create", savegame_name
|
2021-07-01 23:58:03 +00: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 18:52:06 +00:00
|
|
|
while not ctx.auth:
|
2021-07-01 23:58:03 +00:00
|
|
|
while not factorio_queue.empty():
|
|
|
|
msg = factorio_queue.get()
|
|
|
|
factorio_server_logger.info(msg)
|
2021-07-29 13:26:13 +00:00
|
|
|
if "Loading mod AP-" and msg.endswith("(data.lua)"):
|
|
|
|
parts = msg.split()
|
|
|
|
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
2021-07-01 23:58:03 +00:00
|
|
|
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
|
|
|
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
|
|
|
get_info(ctx, rcon_client)
|
|
|
|
await asyncio.sleep(0.01)
|
2021-05-09 14:49:47 +00:00
|
|
|
|
2021-07-29 13:26:13 +00:00
|
|
|
if ctx.mod_version == ctx.__class__.mod_version:
|
|
|
|
raise Exception("No Archipelago mod was loaded. Aborting.")
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
logging.error("Aborted Factorio Server Bridge")
|
2021-07-01 23:58:03 +00:00
|
|
|
ctx.exit_event.set()
|
|
|
|
|
|
|
|
else:
|
2021-07-29 13:26:13 +00:00
|
|
|
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-05-24 23:03:04 +00:00
|
|
|
finally:
|
|
|
|
factorio_process.terminate()
|
2021-07-21 21:32:28 +00:00
|
|
|
factorio_process.wait(5)
|
2021-05-24 23:03:04 +00:00
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-07-19 19:52:08 +00:00
|
|
|
async def main(args):
|
|
|
|
ctx = FactorioContext(args.connect, args.password)
|
2021-07-01 23:58:03 +00:00
|
|
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
2021-07-19 19:52:08 +00:00
|
|
|
if gui_enabled:
|
2021-07-01 23:58:03 +00:00
|
|
|
input_task = None
|
2021-07-21 12:42:33 +00:00
|
|
|
ui_app = get_kivy_app()(ctx)
|
2021-07-01 23:58:03 +00:00
|
|
|
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
|
|
|
else:
|
|
|
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
|
|
|
ui_task = None
|
|
|
|
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
|
|
|
await factorio_server_task
|
2021-04-01 09:40:58 +00:00
|
|
|
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
2021-07-01 23:58:03 +00:00
|
|
|
progression_watcher = asyncio.create_task(
|
|
|
|
game_watcher(ctx), name="FactorioProgressionWatcher")
|
|
|
|
|
2021-04-01 09:40:58 +00:00
|
|
|
await ctx.exit_event.wait()
|
|
|
|
ctx.server_address = None
|
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
await progression_watcher
|
|
|
|
await factorio_server_task
|
2021-04-01 09:40:58 +00:00
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
if ctx.server and not ctx.server.socket.closed:
|
2021-04-01 09:40:58 +00:00
|
|
|
await ctx.server.socket.close()
|
|
|
|
if ctx.server_task is not None:
|
|
|
|
await ctx.server_task
|
|
|
|
|
|
|
|
while ctx.input_requests > 0:
|
|
|
|
ctx.input_queue.put_nowait(None)
|
|
|
|
ctx.input_requests -= 1
|
|
|
|
|
2021-07-01 23:58:03 +00:00
|
|
|
if ui_task:
|
|
|
|
await ui_task
|
|
|
|
|
|
|
|
if input_task:
|
|
|
|
input_task.cancel()
|
2021-04-01 09:40:58 +00:00
|
|
|
|
|
|
|
|
2021-06-06 15:41:06 +00:00
|
|
|
class FactorioJSONtoTextParser(JSONtoTextParser):
|
|
|
|
def _handle_color(self, node: JSONMessagePart):
|
|
|
|
colors = node["color"].split(";")
|
|
|
|
for color in colors:
|
|
|
|
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
|
|
|
|
"brown", "cyan", "acid"}:
|
2021-06-06 21:44:04 +00:00
|
|
|
node["text"] = f"[color={color}]{node['text']}[/color]"
|
2021-06-06 15:41:06 +00:00
|
|
|
return self._handle_text(node)
|
|
|
|
elif color == "magenta":
|
|
|
|
node["text"] = f"[color=pink]{node['text']}[/color]"
|
|
|
|
return self._handle_text(node)
|
2021-06-06 15:50:48 +00:00
|
|
|
return self._handle_text(node)
|
2021-06-06 20:49:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2021-07-19 19:52:08 +00:00
|
|
|
import argparse
|
|
|
|
|
2021-07-21 12:42:33 +00:00
|
|
|
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
|
|
|
|
"Remaining arguments get passed into bound Factorio instance."
|
|
|
|
"Refer to factorio --help for those.")
|
2021-07-19 19:52:08 +00:00
|
|
|
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
2021-07-21 12:42:33 +00:00
|
|
|
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
2021-07-19 19:52:08 +00:00
|
|
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
|
|
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
|
|
|
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
|
|
|
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
2021-07-21 12:42:33 +00:00
|
|
|
|
|
|
|
args, rest = parser.parse_known_args()
|
2021-06-06 20:49:37 +00:00
|
|
|
colorama.init()
|
2021-07-19 19:52:08 +00:00
|
|
|
rcon_port = args.rcon_port
|
2021-07-21 12:42:33 +00: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 19:52:08 +00:00
|
|
|
|
|
|
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
|
|
|
options = Utils.get_options()
|
|
|
|
executable = options["factorio_options"]["executable"]
|
|
|
|
bin_dir = os.path.dirname(executable)
|
2021-07-20 19:19:53 +00:00
|
|
|
if not os.path.exists(bin_dir):
|
|
|
|
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.")
|
2021-07-19 19:52:08 +00:00
|
|
|
if not os.path.isdir(bin_dir):
|
2021-07-20 19:19:53 +00:00
|
|
|
raise FileNotFoundError(f"Path {bin_dir} is not a directory.")
|
2021-07-19 19:52:08 +00:00
|
|
|
if not os.path.exists(executable):
|
|
|
|
if os.path.exists(executable + ".exe"):
|
|
|
|
executable = executable + ".exe"
|
|
|
|
else:
|
2021-07-20 19:19:53 +00:00
|
|
|
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
2021-07-19 19:52:08 +00:00
|
|
|
|
2021-07-21 12:42:33 +00:00
|
|
|
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
|
2021-07-19 19:52:08 +00:00
|
|
|
|
2021-06-06 20:49:37 +00:00
|
|
|
loop = asyncio.get_event_loop()
|
2021-07-19 19:52:08 +00:00
|
|
|
loop.run_until_complete(main(args))
|
2021-06-06 20:49:37 +00:00
|
|
|
loop.close()
|
|
|
|
colorama.deinit()
|