FactorioClient:

fix reconnect
add auto-world-gen

todo:
move remaining script output bridge to rcon
This commit is contained in:
Fabian Dill 2021-07-02 01:58:03 +02:00
parent 0a64caf4c5
commit 97f45f5d96
4 changed files with 139 additions and 108 deletions

View File

@ -5,7 +5,8 @@ import json
import string import string
import copy import copy
import sys import sys
from concurrent.futures import ThreadPoolExecutor import subprocess
import factorio_rcon
import colorama import colorama
import asyncio import asyncio
@ -21,9 +22,9 @@ from worlds.factorio.Technologies import lookup_id_to_name
rcon_port = 24242 rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago"
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options() options = Utils.get_options()
executable = options["factorio_options"]["executable"] executable = options["factorio_options"]["executable"]
bin_dir = os.path.dirname(executable) bin_dir = os.path.dirname(executable)
@ -35,9 +36,7 @@ if not os.path.exists(executable):
else: else:
raise FileNotFoundError(executable) raise FileNotFoundError(executable)
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
thread_pool = ThreadPoolExecutor(10)
class FactorioCommandProcessor(ClientCommandProcessor): class FactorioCommandProcessor(ClientCommandProcessor):
@ -56,7 +55,10 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_connect(self, address: str = "") -> bool: def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server""" """Connect to a MultiWorld Server"""
if not self.ctx.auth: if not self.ctx.auth:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.") if self.ctx.rcon_client:
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
else:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
return super(FactorioCommandProcessor, self)._cmd_connect(address) return super(FactorioCommandProcessor, self)._cmd_connect(address)
@ -70,6 +72,7 @@ class FactorioContext(CommonContext):
self.awaiting_bridge = False self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self) self.raw_json_text_parser = RawJSONtoTextParser(self)
self.factorio_json_text_parser = FactorioJSONtoTextParser(self) self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
self.bridge_file = None
async def server_auth(self, password_requested): async def server_auth(self, password_requested):
if password_requested and not self.password: if password_requested and not self.password:
@ -99,54 +102,45 @@ class FactorioContext(CommonContext):
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] " self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")") f"{cleaned_text}\")")
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}.zip"
async def game_watcher(ctx: FactorioContext, bridge_file: str):
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher") bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0 bridge_file = ctx.bridge_file
try: try:
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
if os.path.exists(bridge_file): if ctx.awaiting_bridge and os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.") ctx.awaiting_bridge = False
while not ctx.exit_event.is_set(): with open(bridge_file) as f:
if ctx.awaiting_bridge: data = json.load(f)
ctx.awaiting_bridge = False research_data = data["research_done"]
with open(bridge_file) as f: research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
data = json.load(f) victory = data["victory"]
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: if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True ctx.finished_game = True
if ctx.locations_checked != 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
if bridge_counter >= 60:
bridge_logger.info( bridge_logger.info(
"Did not find Factorio Bridge file, " f"New researches done: "
"waiting for mod to run, which requires the server to run, " f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
"which requires a player to be connected.") ctx.locations_checked = research_data
bridge_counter = 0 await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1) await asyncio.sleep(1)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
logging.error("Aborted Factorio Server Bridge") logging.error("Aborted Factorio Server Bridge")
def stream_factorio_output(pipe, queue): def stream_factorio_output(pipe, queue, process):
def queuer(): def queuer():
while 1: while process.poll() is None:
text = pipe.readline().strip() text = pipe.readline().strip()
if text: if text:
queue.put_nowait(text) queue.put_nowait(text)
@ -155,25 +149,32 @@ def stream_factorio_output(pipe, queue):
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True) thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
thread.start() thread.start()
return thread
async def factorio_server_watcher(ctx: FactorioContext): async def factorio_server_watcher(ctx: FactorioContext):
import subprocess savegame_name = os.path.abspath(ctx.savegame_name)
import factorio_rcon if not os.path.exists(savegame_name):
factorio_server_logger = logging.getLogger("FactorioServer") logger.info(f"Creating savegame {savegame_name}")
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)), subprocess.run((
executable, "--create", savegame_name
))
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
*(str(elem) for elem in server_args)),
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
encoding="utf-8") encoding="utf-8")
factorio_server_logger.info("Started Factorio Server") factorio_server_logger.info("Started Factorio Server")
factorio_queue = Queue() factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue) stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue) stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
script_folder = None
progression_watcher = None
try: try:
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
if factorio_process.poll():
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty(): while not factorio_queue.empty():
msg = factorio_queue.get() msg = factorio_queue.get()
factorio_server_logger.info(msg) factorio_server_logger.info(msg)
@ -183,14 +184,6 @@ async def factorio_server_watcher(ctx: FactorioContext):
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("/sc game.print('Starting Archipelago Bridge')") ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/ap-sync") 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 not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg: if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
ctx.awaiting_bridge = True ctx.awaiting_bridge = True
if ctx.rcon_client: if ctx.rcon_client:
@ -205,45 +198,110 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") 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.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
ctx.send_index += 1 ctx.send_index += 1
await asyncio.sleep(1) await asyncio.sleep(0.1)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
logging.error("Aborted Factorio Server Bridge") logging.error("Aborted Factorio Server Bridge")
ctx.rcon_client = None
ctx.exit_event.set()
finally: finally:
factorio_process.terminate() factorio_process.terminate()
if progression_watcher:
await progression_watcher
async def main(): 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((
executable, "--create", savegame_name, "--preset", "archipelago"
))
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)
write_folder = None
rcon_client = None
try:
while not ctx.auth or not write_folder:
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
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)
if not write_folder and "Write data path:" in msg:
write_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
bridge_file = os.path.join(write_folder, "script-output", "ap_bridge.json")
if os.path.exists(bridge_file):
os.remove(bridge_file)
ctx.bridge_file = bridge_file
logging.info(f"Bridge File Path: {bridge_file}")
await asyncio.sleep(0.01)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.exit_event.set()
else:
logger.info(f"Got World Information from AP Mod for seed {ctx.seed_name} in slot {ctx.auth}")
finally:
factorio_process.terminate()
async def main(ui=None):
ctx = FactorioContext(None, None, True) ctx = FactorioContext(None, None, True)
# testing shortcuts ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
# ctx.server_address = "localhost" if ui:
# ctx.auth = "Nauvis" input_task = None
if ctx.server_task is None: ui_app = ui(ctx)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await asyncio.sleep(3) else:
input_task = asyncio.create_task(console_loop(ctx), name="Input") 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
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer") factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait() await ctx.exit_event.wait()
ctx.server_address = None ctx.server_address = None
ctx.snes_reconnect_address = None
await asyncio.gather(input_task, factorio_server_task) await progression_watcher
await factorio_server_task
if ctx.server is not None and not ctx.server.socket.closed: if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close() await ctx.server.socket.close()
if ctx.server_task is not None: if ctx.server_task is not None:
await ctx.server_task await ctx.server_task
await factorio_server_task
while ctx.input_requests > 0: while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None) ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1 ctx.input_requests -= 1
await input_task if ui_task:
await ui_task
if input_task:
input_task.cancel()
class FactorioJSONtoTextParser(JSONtoTextParser): class FactorioJSONtoTextParser(JSONtoTextParser):

View File

@ -13,38 +13,8 @@ os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1" os.environ["KIVY_NO_ARGS"] = "1"
import asyncio import asyncio
from CommonClient import server_loop, logger from CommonClient import logger
from FactorioClient import FactorioContext, factorio_server_watcher from FactorioClient import main
async def main():
ctx = FactorioContext(None, None, True)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
ui_app = FactorioManager(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await ctx.exit_event.wait() # wait for signal to exit application
ui_app.stop()
ctx.server_address = None
ctx.snes_reconnect_address = None
# allow tasks to quit
if ui_task:
await ui_task
if factorio_server_task:
await factorio_server_task
if ctx.server_task:
await ctx.server_task
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task:
await ctx.server_task
while ctx.input_requests > 0: # clear queue for shutdown
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
from kivy.app import App from kivy.app import App
@ -166,5 +136,6 @@ Builder.load_string('''
if __name__ == '__main__': if __name__ == '__main__':
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(main()) ui_app = FactorioManager
loop.run_until_complete(main(ui_app))
loop.close() loop.close()

View File

@ -190,8 +190,6 @@ function dumpInfo(force)
local data_collection = { local data_collection = {
["research_done"] = research_done, ["research_done"] = research_done,
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"), ["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
["slot_name"] = SLOT_NAME,
["seed_name"] = SEED_NAME
} }
for tech_name, tech in pairs(force.technologies) do for tech_name, tech in pairs(force.technologies) do
@ -247,3 +245,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
game.print("Unknown Technology " .. tech_name) game.print("Unknown Technology " .. tech_name)
end end
end) end)
commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call)
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME}))
end)

View File

@ -84,7 +84,7 @@ class Factorio(World):
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 2), super(Factorio, self).get_required_client_version()) return max((0, 1, 4), super(Factorio, self).get_required_client_version())
options = factorio_options options = factorio_options