From bc028a63cd1ff14fd0d2635d57c42a2e100e997f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 May 2021 12:49:01 +0200 Subject: [PATCH 1/9] first version of a Factorio Graphical Client --- FactorioClientGUI.py | 342 +++++++++++++++++++++++++++++++++++++ factorio_client_setup.py | 138 +++++++++++++++ factorio_inno_setup_38.iss | 80 +++++++++ 3 files changed, 560 insertions(+) create mode 100644 FactorioClientGUI.py create mode 100644 factorio_client_setup.py create mode 100644 factorio_inno_setup_38.iss diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py new file mode 100644 index 00000000..3c0b85fb --- /dev/null +++ b/FactorioClientGUI.py @@ -0,0 +1,342 @@ +import os +import logging +logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, filename="log.txt", filemode="w") +import json +import string +os.environ["KIVY_NO_CONSOLELOG"] = "1" +os.environ["KIVY_NO_FILELOG"] = "1" +os.environ["KIVY_NO_ARGS"] = "1" +from concurrent.futures import ThreadPoolExecutor + + +import asyncio +from queue import Queue +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger +from MultiServer import mark_raw + +import Utils +import random +from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus + +from worlds.factorio.Technologies import lookup_id_to_name + +rcon_port = 24242 +rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) +save_name = "Archipelago" + + + +options = Utils.get_options() +executable = options["factorio_options"]["executable"] +bin_dir = os.path.dirname(executable) +if not os.path.isdir(bin_dir): + raise FileNotFoundError(bin_dir) +if not os.path.exists(executable): + if os.path.exists(executable + ".exe"): + executable = executable + ".exe" + else: + raise FileNotFoundError(executable) + +import sys +server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) + +threadpool = ThreadPoolExecutor(10) + + +class FactorioCommandProcessor(ClientCommandProcessor): + @mark_raw + def _cmd_factorio(self, text: str) -> bool: + """Send the following command to the bound Factorio Server.""" + if self.ctx.rcon_client: + result = self.ctx.rcon_client.send_command(text) + if result: + self.output(result) + return True + return False + + def _cmd_connect(self, address: str = "") -> bool: + """Connect to a MultiWorld Server""" + if not self.ctx.auth: + self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.") + return super(FactorioCommandProcessor, self)._cmd_connect(address) + + +class FactorioContext(CommonContext): + command_processor = FactorioCommandProcessor + + def __init__(self, *args, **kwargs): + super(FactorioContext, self).__init__(*args, **kwargs) + self.send_index = 0 + self.rcon_client = None + self.raw_json_text_parser = RawJSONtoTextParser(self) + + async def server_auth(self, password_requested): + if password_requested and not self.password: + await super(FactorioContext, self).server_auth(password_requested) + + await self.send_msgs([{"cmd": 'Connect', + 'password': self.password, 'name': self.auth, 'version': Utils._version_tuple, + 'tags': ['AP'], + 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" + }]) + + def on_print(self, args: dict): + logger.info(args["text"]) + if self.rcon_client: + cleaned_text = args['text'].replace('"', '') + self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") + + def on_print_json(self, args: dict): + if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: + pass # don't want info on other player's local pickups. + text = self.raw_json_text_parser(args["data"]) + logger.info(text) + if self.rcon_client: + cleaned_text = text.replace('"', '') + self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") + +async def game_watcher(ctx: FactorioContext, bridge_file: str): + bridge_logger = logging.getLogger("FactorioWatcher") + from worlds.factorio.Technologies import lookup_id_to_name + bridge_counter = 0 + try: + while not ctx.exit_event.is_set(): + if os.path.exists(bridge_file): + bridge_logger.info("Found Factorio Bridge file.") + while not ctx.exit_event.is_set(): + with open(bridge_file) as f: + data = json.load(f) + research_data = data["research_done"] + research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} + victory = data["victory"] + ctx.auth = data["slot_name"] + ctx.seed_name = data["seed_name"] + + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + if ctx.locations_checked != research_data: + bridge_logger.info(f"New researches done: " + f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") + ctx.locations_checked = research_data + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + await asyncio.sleep(1) + else: + bridge_counter += 1 + if bridge_counter >= 60: + bridge_logger.info( + "Did not find Factorio Bridge file, " + "waiting for mod to run, which requires the server to run, " + "which requires a player to be connected.") + bridge_counter = 0 + await asyncio.sleep(1) + except Exception as e: + logging.exception(e) + logging.error("Aborted Factorio Server Bridge") + + +def stream_factorio_output(pipe, queue): + def queuer(): + while 1: + text = pipe.readline().strip() + if text: + queue.put_nowait(text) + + from threading import Thread + + thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True) + thread.start() + + +async def factorio_server_watcher(ctx: FactorioContext): + import subprocess + import factorio_rcon + factorio_server_logger = logging.getLogger("FactorioServer") + factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)), + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.DEVNULL, + encoding="utf-8") + factorio_server_logger.info("Started Factorio Server") + factorio_queue = Queue() + stream_factorio_output(factorio_process.stdout, factorio_queue) + stream_factorio_output(factorio_process.stderr, factorio_queue) + script_folder = None + progression_watcher = None + try: + while not ctx.exit_event.is_set(): + while not factorio_queue.empty(): + msg = factorio_queue.get() + factorio_server_logger.info(msg) + if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: + ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) + # trigger lua interface confirmation + ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") + ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") + ctx.rcon_client.send_command("/ap-sync") + if not script_folder and "Write data path:" in msg: + script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip() + bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json") + if os.path.exists(bridge_file): + os.remove(bridge_file) + logging.info(f"Bridge File Path: {bridge_file}") + progression_watcher= asyncio.create_task( + game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") + if ctx.rcon_client: + while ctx.send_index < len(ctx.items_received): + transfer_item: NetworkItem = ctx.items_received[ctx.send_index] + item_id = transfer_item.item + player_name = ctx.player_names[transfer_item.player] + if item_id not in lookup_id_to_name: + logging.error(f"Cannot send unknown item ID: {item_id}") + else: + item_name = lookup_id_to_name[item_id] + factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") + ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}') + ctx.send_index += 1 + await asyncio.sleep(1) + factorio_process.terminate() + await progression_watcher + + except Exception as e: + logging.exception(e) + logging.error("Aborted Factorio Server Bridge") + + +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 + await ui_task + await factorio_server_task + await ctx.server_task + + if ctx.server is not None and not ctx.server.socket.closed: + await ctx.server.socket.close() + if ctx.server_task is not None: + 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.uix.label import Label +from kivy.uix.gridlayout import GridLayout +from kivy.uix.textinput import TextInput +from kivy.uix.recycleview import RecycleView +from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelHeader +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) + + + 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 File 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 = TabbedPanelHeader(text=display_name) + self.tabs.add_widget(panel) + panel.content = UILog(bridge_logger) + 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) + +Builder.load_string(''' + + tab_width: 200 +: + canvas.before: + Color: + rgba: 0.2, 0.2, 0.2, 1 + Rectangle: + size: self.size + pos: self.pos + text_size: self.width, None + size_hint_y: None + height: self.texture_size[1] + font_size: dp(20) +: + viewclass: 'Row' + scroll_y: 0 + effect_cls: "ScrollEffect" + RecycleBoxLayout: + default_size: None, dp(20) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: dp(3) +''') + +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()}) + + def update_text_width(self, *_): + self.message.text_size = (self.message.width * 0.9, None) + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/factorio_client_setup.py b/factorio_client_setup.py new file mode 100644 index 00000000..7342cf00 --- /dev/null +++ b/factorio_client_setup.py @@ -0,0 +1,138 @@ +import os +import shutil +import sys +import sysconfig +from pathlib import Path +import cx_Freeze + +is_64bits = sys.maxsize > 2 ** 32 + +folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(), + version=sysconfig.get_python_version()) +buildfolder = Path("build_factorio", folder) +sbuildfolder = str(buildfolder) +libfolder = Path(buildfolder, "lib") +library = Path(libfolder, "library.zip") +print("Outputting to: " + sbuildfolder) + +icon = "icon.ico" + +if os.path.exists("X:/pw.txt"): + print("Using signtool") + with open("X:/pw.txt") as f: + pw = f.read() + signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p ' + pw + r' /fd sha256 /tr http://timestamp.digicert.com/ ' +else: + signtool = None + +from hashlib import sha3_512 +import base64 + + +def _threaded_hash(filepath): + hasher = sha3_512() + hasher.update(open(filepath, "rb").read()) + return base64.b85encode(hasher.digest()).decode() + + +os.makedirs(buildfolder, exist_ok=True) + + +def manifest_creation(): + hashes = {} + manifestpath = os.path.join(buildfolder, "manifest.json") + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor() + for dirpath, dirnames, filenames in os.walk(buildfolder): + for filename in filenames: + path = os.path.join(dirpath, filename) + hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path) + import json + from Utils import _version_tuple + manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), + "hashes": {path: hash.result() for path, hash in hashes.items()}, + "version": _version_tuple} + json.dump(manifest, open(manifestpath, "wt"), indent=4) + print("Created Manifest") + + +scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"} + +exes = [] + +for script, scriptname in scripts.items(): + exes.append(cx_Freeze.Executable( + script=script, + target_name=scriptname + ("" if sys.platform == "linux" else ".exe"), + icon=icon, + )) +exes.append(cx_Freeze.Executable( + script="FactorioClientGUI.py", + target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"), + icon=icon, + base="Win32GUI" +)) + +import datetime + +buildtime = datetime.datetime.utcnow() + +cx_Freeze.setup( + name="Archipelago", + version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}", + description="Archipelago", + executables=exes, + options={ + "build_exe": { + "packages": ["websockets", "kivy"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["kivy"], + "include_files": [], + "include_msvcr": True, + "replace_paths": [("*", "")], + "optimize": 2, + "build_exe": buildfolder + }, + }, +) + + +def installfile(path, keep_content=False): + lbuildfolder = buildfolder + print('copying', path, '->', lbuildfolder) + if path.is_dir(): + lbuildfolder /= path.name + if lbuildfolder.is_dir() and not keep_content: + shutil.rmtree(lbuildfolder) + shutil.copytree(path, lbuildfolder, dirs_exist_ok=True) + elif path.is_file(): + shutil.copy(path, lbuildfolder) + else: + print('Warning,', path, 'not found') + + +extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"] +from kivy_deps import sdl2, glew +for folder in sdl2.dep_bins+glew.dep_bins: + shutil.copytree(folder, buildfolder, dirs_exist_ok=True) +for data in extra_data: + installfile(Path(data)) + + +os.makedirs(buildfolder / "Players", exist_ok=True) +shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml") + +if signtool: + for exe in exes: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(buildfolder, exe.target_name)) + +alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr" +for file in os.listdir(alttpr_sprites_folder): + if file != ".gitignore": + os.remove(alttpr_sprites_folder / file) + +manifest_creation() diff --git a/factorio_inno_setup_38.iss b/factorio_inno_setup_38.iss new file mode 100644 index 00000000..fc300e5f --- /dev/null +++ b/factorio_inno_setup_38.iss @@ -0,0 +1,80 @@ +#define sourcepath "build_factorio\exe.win-amd64-3.8\" +#define MyAppName "Archipelago Factorio Client" +#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe" +#define MyAppIcon "icon.ico" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}} +AppName={#MyAppName} +AppVerName={#MyAppName} +DefaultDirName={commonappdata}\{#MyAppName} +DisableProgramGroupPage=yes +DefaultGroupName=Archipelago +OutputDir=setups +OutputBaseFilename=Setup {#MyAppName} +Compression=lzma2 +SolidCompression=yes +LZMANumBlockThreads=8 +ArchitecturesInstallIn64BitMode=x64 +ChangesAssociations=yes +ArchitecturesAllowed=x64 +AllowNoIcons=yes +SetupIconFile={#MyAppIcon} +UninstallDisplayIcon={app}\{#MyAppExeName} +SignTool= signtool +LicenseFile= LICENSE +WizardStyle= modern +SetupLogging=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; + + +[Dirs] +NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; + +[Files] +Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall +Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; +Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." + +[UninstallDelete] +Type: dirifempty; Name: "{app}" + + +[Code] +// See: https://stackoverflow.com/a/51614652/2287576 +function IsVCRedist64BitNeeded(): boolean; +var + strVersion: string; +begin + if (RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then + begin + // Is the installed version at least the packaged one ? + Log('VC Redist x64 Version : found ' + strVersion); + Result := (CompareStr(strVersion, 'v14.28.29325') < 0); + end + else + begin + // Not even an old version installed + Log('VC Redist x64 is not already installed'); + Result := True; + end; +end; + + From f78bb2078d5fbe7323bb9c6619aaba19b3c98a7f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 May 2021 13:51:27 +0200 Subject: [PATCH 2/9] make sure Factorio subprocess is terminated properly --- FactorioClientGUI.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index 3c0b85fb..c075576b 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -196,13 +196,16 @@ async def factorio_server_watcher(ctx: FactorioContext): ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}') ctx.send_index += 1 await asyncio.sleep(1) - factorio_process.terminate() - await progression_watcher + except Exception as e: logging.exception(e) logging.error("Aborted Factorio Server Bridge") + finally: + factorio_process.terminate() + if progression_watcher: + await progression_watcher async def main(): ctx = FactorioContext(None, None, True) @@ -245,7 +248,7 @@ class FactorioManager(App): super(FactorioManager, self).__init__() self.ctx = ctx self.commandprocessor = ctx.command_processor(ctx) - + self.icon = "data/icon.png" def build(self): self.grid = GridLayout() From 0175c8ab8ab56905fcb8e2ab7e2deae720f0ad3b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 May 2021 16:09:10 +0200 Subject: [PATCH 3/9] move FactorioClient log to logs folder --- FactorioClientGUI.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index c075576b..68357289 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -1,6 +1,8 @@ import os import logging -logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, filename="log.txt", filemode="w") +os.makedirs("logs", exist_ok=True) +logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) +logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) import json import string os.environ["KIVY_NO_CONSOLELOG"] = "1" From 252bb69808346a2634c1d4a8a018f418aa63e055 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 25 May 2021 01:03:04 +0200 Subject: [PATCH 4/9] FactorioClient: Read Bridge file after a server log indicates that the file was written --- FactorioClient.py | 57 ++++--- FactorioClientGUI.py | 200 +------------------------ Main.py | 5 +- Utils.py | 2 +- data/factorio/mod_template/control.lua | 3 +- 5 files changed, 44 insertions(+), 223 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index 1b24fb02..20b5e8e9 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -65,6 +65,7 @@ class FactorioContext(CommonContext): super(FactorioContext, self).__init__(*args, **kwargs) self.send_index = 0 self.rcon_client = None + self.awaiting_bridge = False self.raw_json_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested): @@ -86,10 +87,10 @@ class FactorioContext(CommonContext): def on_print_json(self, args: dict): if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: pass # don't want info on other player's local pickups. - copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently - logger.info(self.jsontotextparser(args["data"])) + text = self.raw_json_text_parser(args["data"]) + logger.info(text) if self.rcon_client: - cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '') + cleaned_text = text.replace('"', '') self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") async def game_watcher(ctx: FactorioContext, bridge_file: str): @@ -97,27 +98,29 @@ async def game_watcher(ctx: FactorioContext, bridge_file: str): from worlds.factorio.Technologies import lookup_id_to_name bridge_counter = 0 try: - while 1: + while not ctx.exit_event.is_set(): if os.path.exists(bridge_file): bridge_logger.info("Found Factorio Bridge file.") - while 1: - with open(bridge_file) as f: - data = json.load(f) - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} - victory = data["victory"] - ctx.auth = data["slot_name"] - ctx.seed_name = data["seed_name"] + while not ctx.exit_event.is_set(): + if ctx.awaiting_bridge: + ctx.awaiting_bridge = False + with open(bridge_file) as f: + data = json.load(f) + research_data = data["research_done"] + research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} + victory = data["victory"] + ctx.auth = data["slot_name"] + ctx.seed_name = data["seed_name"] - if not ctx.finished_game and victory: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True - if ctx.locations_checked != research_data: - bridge_logger.info(f"New researches done: " - f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") - ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + if ctx.locations_checked != research_data: + bridge_logger.info(f"New researches done: " + f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") + ctx.locations_checked = research_data + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) await asyncio.sleep(1) else: bridge_counter += 1 @@ -160,8 +163,9 @@ async def factorio_server_watcher(ctx: FactorioContext): stream_factorio_output(factorio_process.stdout, factorio_queue) stream_factorio_output(factorio_process.stderr, factorio_queue) script_folder = None + progression_watcher = None try: - while 1: + while not ctx.exit_event.is_set(): while not factorio_queue.empty(): msg = factorio_queue.get() factorio_server_logger.info(msg) @@ -177,7 +181,10 @@ async def factorio_server_watcher(ctx: FactorioContext): if os.path.exists(bridge_file): os.remove(bridge_file) logging.info(f"Bridge File Path: {bridge_file}") - asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") + progression_watcher= asyncio.create_task( + game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") + if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg: + ctx.awaiting_bridge = True if ctx.rcon_client: while ctx.send_index < len(ctx.items_received): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] @@ -192,10 +199,16 @@ async def factorio_server_watcher(ctx: FactorioContext): ctx.send_index += 1 await asyncio.sleep(1) + except Exception as e: logging.exception(e) logging.error("Aborted Factorio Server Bridge") + finally: + factorio_process.terminate() + if progression_watcher: + await progression_watcher + async def main(): ctx = FactorioContext(None, None, True) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index 68357289..a2bc5806 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -3,211 +3,15 @@ import logging os.makedirs("logs", exist_ok=True) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) -import json -import string os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_ARGS"] = "1" -from concurrent.futures import ThreadPoolExecutor import asyncio -from queue import Queue -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger -from MultiServer import mark_raw +from CommonClient import server_loop, logger +from FactorioClient import FactorioContext, factorio_server_watcher -import Utils -import random -from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus - -from worlds.factorio.Technologies import lookup_id_to_name - -rcon_port = 24242 -rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) -save_name = "Archipelago" - - - -options = Utils.get_options() -executable = options["factorio_options"]["executable"] -bin_dir = os.path.dirname(executable) -if not os.path.isdir(bin_dir): - raise FileNotFoundError(bin_dir) -if not os.path.exists(executable): - if os.path.exists(executable + ".exe"): - executable = executable + ".exe" - else: - raise FileNotFoundError(executable) - -import sys -server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) - -threadpool = ThreadPoolExecutor(10) - - -class FactorioCommandProcessor(ClientCommandProcessor): - @mark_raw - def _cmd_factorio(self, text: str) -> bool: - """Send the following command to the bound Factorio Server.""" - if self.ctx.rcon_client: - result = self.ctx.rcon_client.send_command(text) - if result: - self.output(result) - return True - return False - - def _cmd_connect(self, address: str = "") -> bool: - """Connect to a MultiWorld Server""" - if not self.ctx.auth: - self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.") - return super(FactorioCommandProcessor, self)._cmd_connect(address) - - -class FactorioContext(CommonContext): - command_processor = FactorioCommandProcessor - - def __init__(self, *args, **kwargs): - super(FactorioContext, self).__init__(*args, **kwargs) - self.send_index = 0 - self.rcon_client = None - self.raw_json_text_parser = RawJSONtoTextParser(self) - - async def server_auth(self, password_requested): - if password_requested and not self.password: - await super(FactorioContext, self).server_auth(password_requested) - - await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': self.auth, 'version': Utils._version_tuple, - 'tags': ['AP'], - 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" - }]) - - def on_print(self, args: dict): - logger.info(args["text"]) - if self.rcon_client: - cleaned_text = args['text'].replace('"', '') - self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") - - def on_print_json(self, args: dict): - if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: - pass # don't want info on other player's local pickups. - text = self.raw_json_text_parser(args["data"]) - logger.info(text) - if self.rcon_client: - cleaned_text = text.replace('"', '') - self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") - -async def game_watcher(ctx: FactorioContext, bridge_file: str): - bridge_logger = logging.getLogger("FactorioWatcher") - from worlds.factorio.Technologies import lookup_id_to_name - bridge_counter = 0 - try: - while not ctx.exit_event.is_set(): - if os.path.exists(bridge_file): - bridge_logger.info("Found Factorio Bridge file.") - while not ctx.exit_event.is_set(): - with open(bridge_file) as f: - data = json.load(f) - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} - victory = data["victory"] - ctx.auth = data["slot_name"] - ctx.seed_name = data["seed_name"] - - if not ctx.finished_game and victory: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - - if ctx.locations_checked != research_data: - bridge_logger.info(f"New researches done: " - f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") - ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) - await asyncio.sleep(1) - else: - bridge_counter += 1 - if bridge_counter >= 60: - bridge_logger.info( - "Did not find Factorio Bridge file, " - "waiting for mod to run, which requires the server to run, " - "which requires a player to be connected.") - bridge_counter = 0 - await asyncio.sleep(1) - except Exception as e: - logging.exception(e) - logging.error("Aborted Factorio Server Bridge") - - -def stream_factorio_output(pipe, queue): - def queuer(): - while 1: - text = pipe.readline().strip() - if text: - queue.put_nowait(text) - - from threading import Thread - - thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True) - thread.start() - - -async def factorio_server_watcher(ctx: FactorioContext): - import subprocess - import factorio_rcon - factorio_server_logger = logging.getLogger("FactorioServer") - factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)), - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.DEVNULL, - encoding="utf-8") - factorio_server_logger.info("Started Factorio Server") - factorio_queue = Queue() - stream_factorio_output(factorio_process.stdout, factorio_queue) - stream_factorio_output(factorio_process.stderr, factorio_queue) - script_folder = None - progression_watcher = None - try: - while not ctx.exit_event.is_set(): - while not factorio_queue.empty(): - msg = factorio_queue.get() - factorio_server_logger.info(msg) - if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: - ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) - # trigger lua interface confirmation - ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") - ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") - ctx.rcon_client.send_command("/ap-sync") - if not script_folder and "Write data path:" in msg: - script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip() - bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json") - if os.path.exists(bridge_file): - os.remove(bridge_file) - logging.info(f"Bridge File Path: {bridge_file}") - progression_watcher= asyncio.create_task( - game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") - if ctx.rcon_client: - while ctx.send_index < len(ctx.items_received): - transfer_item: NetworkItem = ctx.items_received[ctx.send_index] - item_id = transfer_item.item - player_name = ctx.player_names[transfer_item.player] - if item_id not in lookup_id_to_name: - logging.error(f"Cannot send unknown item ID: {item_id}") - else: - item_name = lookup_id_to_name[item_id] - factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") - ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}') - ctx.send_index += 1 - await asyncio.sleep(1) - - - except Exception as e: - logging.exception(e) - logging.error("Aborted Factorio Server Bridge") - - finally: - factorio_process.terminate() - if progression_watcher: - await progression_watcher async def main(): ctx = FactorioContext(None, None, True) diff --git a/Main.py b/Main.py index ed0ac0e1..f3d8e429 100644 --- a/Main.py +++ b/Main.py @@ -502,7 +502,10 @@ def main(args, seed=None): minimum_versions = {"server": (0, 1, 1), "clients": client_versions} games = {} for slot in world.player_ids: - client_versions[slot] = (0, 0, 3) + if world.game[slot] == "Factorio": + client_versions[slot] = (1, 1, 2) + else: + client_versions[slot] = (0, 0, 3) games[slot] = world.game[slot] connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names} diff --git a/Utils.py b/Utils.py index b3f43d97..3c38e94f 100644 --- a/Utils.py +++ b/Utils.py @@ -12,7 +12,7 @@ class Version(typing.NamedTuple): minor: int build: int -__version__ = "0.1.1" +__version__ = "0.1.2" _version_tuple = tuplize_version(__version__) import builtins diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index 17d80e1c..dcb0b2e9 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -134,7 +134,7 @@ end) -- for testing script.on_event(defines.events.on_tick, function(event) - if event.tick%600 == 300 then + if event.tick%3600 == 300 then dumpInfo(game.forces["player"]) end end) @@ -186,6 +186,7 @@ function dumpInfo(force) end end game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0) + log("Archipelago Bridge File written for game tick ".. game.tick .. ".") -- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0) -- game.print("Sent progress to Archipelago.") end From 573931930c4a82ddaeb54477e9811bc758fc12e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 25 May 2021 01:06:15 +0200 Subject: [PATCH 5/9] remove debugging helper --- data/factorio/mod_template/control.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index dcb0b2e9..5d3a1868 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -133,11 +133,11 @@ script.on_init(function() end) -- for testing -script.on_event(defines.events.on_tick, function(event) - if event.tick%3600 == 300 then - dumpInfo(game.forces["player"]) - end -end) +-- script.on_event(defines.events.on_tick, function(event) +-- if event.tick%3600 == 300 then +-- dumpInfo(game.forces["player"]) +-- end +-- end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) From aa6f65ee1fe373b51a24d8010663ffd9083f87e7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 27 May 2021 12:14:20 +0200 Subject: [PATCH 6/9] Prevent logical lockout from Pedestal/Pyramid Fairy in ice rod hunt --- FactorioClientGUI.py | 38 +++++++++++++++++++++----------------- worlds/alttp/ItemPool.py | 29 +++++++++++++++-------------- worlds/alttp/Rom.py | 12 ++++++++---- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py index a2bc5806..d4f4bf19 100644 --- a/FactorioClientGUI.py +++ b/FactorioClientGUI.py @@ -1,5 +1,6 @@ import os import logging + os.makedirs("logs", exist_ok=True) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) @@ -7,7 +8,6 @@ os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_ARGS"] = "1" - import asyncio from CommonClient import server_loop, logger from FactorioClient import FactorioContext, factorio_server_watcher @@ -21,7 +21,7 @@ async def main(): 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 + await ctx.exit_event.wait() # wait for signal to exit application ui_app.stop() ctx.server_address = None ctx.snes_reconnect_address = None @@ -35,7 +35,7 @@ async def main(): if ctx.server_task is not None: await ctx.server_task - while ctx.input_requests > 0: # clear queue for shutdown + while ctx.input_requests > 0: # clear queue for shutdown ctx.input_queue.put_nowait(None) ctx.input_requests -= 1 @@ -96,6 +96,7 @@ class FactorioManager(App): except Exception as e: logger.exception(e) + class LogtoUI(logging.Handler): def __init__(self, on_log): super(LogtoUI, self).__init__(logging.DEBUG) @@ -104,6 +105,23 @@ class LogtoUI(logging.Handler): 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()}) + + def update_text_width(self, *_): + self.message.text_size = (self.message.width * 0.9, None) + + Builder.load_string(''' tab_width: 200 @@ -131,20 +149,6 @@ Builder.load_string(''' spacing: dp(3) ''') -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()}) - - def update_text_width(self, *_): - self.message.text_size = (self.message.width * 0.9, None) - if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 0b83bb91..d03b686b 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -241,25 +241,13 @@ def generate_itempool(world, player: int): else: world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) - if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: - region = world.get_region('Light World', player) - loc = ALttPLocation(player, "Murahdahla", parent=region) - loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player) - - region.locations.append(loc) - world.dynamic_locations.append(loc) - - world.clear_location_cache() - - world.push_item(loc, ItemFactory('Triforce', player), False) - loc.event = True - loc.locked = True if world.goal[player] == 'icerodhunt': world.progression_balancing[player] = False loc = world.get_location('Turtle Rock - Boss', player) - world.push_item(loc, ItemFactory('Triforce', player), False) + world.push_item(loc, ItemFactory('Triforce Piece', player), False) + world.treasure_hunt_count[player] = 1 if world.boss_shuffle[player] != 'none': if 'turtle rock-' not in world.boss_shuffle[player]: world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' @@ -295,6 +283,19 @@ def generate_itempool(world, player: int): for item in itempool: world.push_precollected(ItemFactory(item, player)) + if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: + region = world.get_region('Light World', player) + + loc = ALttPLocation(player, "Murahdahla", parent=region) + loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player) + + region.locations.append(loc) + world.dynamic_locations.append(loc) + world.clear_location_cache() + + world.push_item(loc, ItemFactory('Triforce', player), False) + loc.event = True + loc.locked = True world.get_location('Ganon', player).event = True world.get_location('Ganon', player).locked = True diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 5a426d6d..92ed48f4 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2337,9 +2337,13 @@ def write_strings(rom, world, player, team): tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \ f'have beaten Agahnim atop Ganons Tower' elif world.goal[player] == "icerodhunt": - tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx... Ganon is invincible!' + tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx, then talk to Murahdahla... Ganon is invincible!' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' + tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ + "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ + "hidden in a hollow tree. " \ + "If you bring me the Triforce piece from Turtle Rock, I can reassemble it." else: if world.crystals_needed_for_ganon[player] == 1: tt['sign_ganon'] = 'You need a crystal to beat Ganon.' @@ -2354,7 +2358,7 @@ def write_strings(rom, world, player, team): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] - if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: + if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' if world.goal[player] == 'triforcehunt' and world.players > 1: @@ -2364,12 +2368,12 @@ def write_strings(rom, world, player, team): if world.treasure_hunt_count[player] > 1: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ - "hidden in a hollow tree. If you bring\n%d triforce pieces out of %d, I can reassemble it." % \ + "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) else: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ - "hidden in a hollow tree. If you bring\n%d triforce piece out of %d, I can reassemble it." % \ + "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) elif world.goal[player] in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' From a993bed8dcd9b6ab07748996810e2d82ee15e6e5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 27 May 2021 12:26:08 +0200 Subject: [PATCH 7/9] move factorio_client_setup.py into setup.py --- factorio_client_setup.py | 138 --------------------------------------- setup.py | 85 ++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 143 deletions(-) delete mode 100644 factorio_client_setup.py diff --git a/factorio_client_setup.py b/factorio_client_setup.py deleted file mode 100644 index 7342cf00..00000000 --- a/factorio_client_setup.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import shutil -import sys -import sysconfig -from pathlib import Path -import cx_Freeze - -is_64bits = sys.maxsize > 2 ** 32 - -folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(), - version=sysconfig.get_python_version()) -buildfolder = Path("build_factorio", folder) -sbuildfolder = str(buildfolder) -libfolder = Path(buildfolder, "lib") -library = Path(libfolder, "library.zip") -print("Outputting to: " + sbuildfolder) - -icon = "icon.ico" - -if os.path.exists("X:/pw.txt"): - print("Using signtool") - with open("X:/pw.txt") as f: - pw = f.read() - signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p ' + pw + r' /fd sha256 /tr http://timestamp.digicert.com/ ' -else: - signtool = None - -from hashlib import sha3_512 -import base64 - - -def _threaded_hash(filepath): - hasher = sha3_512() - hasher.update(open(filepath, "rb").read()) - return base64.b85encode(hasher.digest()).decode() - - -os.makedirs(buildfolder, exist_ok=True) - - -def manifest_creation(): - hashes = {} - manifestpath = os.path.join(buildfolder, "manifest.json") - from concurrent.futures import ThreadPoolExecutor - pool = ThreadPoolExecutor() - for dirpath, dirnames, filenames in os.walk(buildfolder): - for filename in filenames: - path = os.path.join(dirpath, filename) - hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path) - import json - from Utils import _version_tuple - manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), - "hashes": {path: hash.result() for path, hash in hashes.items()}, - "version": _version_tuple} - json.dump(manifest, open(manifestpath, "wt"), indent=4) - print("Created Manifest") - - -scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"} - -exes = [] - -for script, scriptname in scripts.items(): - exes.append(cx_Freeze.Executable( - script=script, - target_name=scriptname + ("" if sys.platform == "linux" else ".exe"), - icon=icon, - )) -exes.append(cx_Freeze.Executable( - script="FactorioClientGUI.py", - target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"), - icon=icon, - base="Win32GUI" -)) - -import datetime - -buildtime = datetime.datetime.utcnow() - -cx_Freeze.setup( - name="Archipelago", - version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}", - description="Archipelago", - executables=exes, - options={ - "build_exe": { - "packages": ["websockets", "kivy"], - "includes": [], - "excludes": ["numpy", "Cython", "PySide2", "PIL", - "pandas"], - "zip_include_packages": ["*"], - "zip_exclude_packages": ["kivy"], - "include_files": [], - "include_msvcr": True, - "replace_paths": [("*", "")], - "optimize": 2, - "build_exe": buildfolder - }, - }, -) - - -def installfile(path, keep_content=False): - lbuildfolder = buildfolder - print('copying', path, '->', lbuildfolder) - if path.is_dir(): - lbuildfolder /= path.name - if lbuildfolder.is_dir() and not keep_content: - shutil.rmtree(lbuildfolder) - shutil.copytree(path, lbuildfolder, dirs_exist_ok=True) - elif path.is_file(): - shutil.copy(path, lbuildfolder) - else: - print('Warning,', path, 'not found') - - -extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"] -from kivy_deps import sdl2, glew -for folder in sdl2.dep_bins+glew.dep_bins: - shutil.copytree(folder, buildfolder, dirs_exist_ok=True) -for data in extra_data: - installfile(Path(data)) - - -os.makedirs(buildfolder / "Players", exist_ok=True) -shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml") - -if signtool: - for exe in exes: - print(f"Signing {exe.target_name}") - os.system(signtool + os.path.join(buildfolder, exe.target_name)) - -alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr" -for file in os.listdir(alttpr_sprites_folder): - if file != ".gitignore": - os.remove(alttpr_sprites_folder / file) - -manifest_creation() diff --git a/setup.py b/setup.py index 1c90f819..8bc3f45d 100644 --- a/setup.py +++ b/setup.py @@ -38,15 +38,15 @@ def _threaded_hash(filepath): os.makedirs(buildfolder, exist_ok=True) -def manifest_creation(): +def manifest_creation(folder): hashes = {} - manifestpath = os.path.join(buildfolder, "manifest.json") + manifestpath = os.path.join(folder, "manifest.json") from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor() - for dirpath, dirnames, filenames in os.walk(buildfolder): + for dirpath, dirnames, filenames in os.walk(folder): for filename in filenames: path = os.path.join(dirpath, filename) - hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path) + hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path) import json from Utils import _version_tuple manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), @@ -161,4 +161,79 @@ for file in os.listdir(alttpr_sprites_folder): if file != ".gitignore": os.remove(alttpr_sprites_folder / file) -manifest_creation() +manifest_creation(buildfolder) + +buildfolder = Path("build_factorio", folder) +sbuildfolder = str(buildfolder) +libfolder = Path(buildfolder, "lib") +library = Path(libfolder, "library.zip") +print("Outputting Factorio Client to: " + sbuildfolder) + +os.makedirs(buildfolder, exist_ok=True) + +scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"} + +exes = [] + +for script, scriptname in scripts.items(): + exes.append(cx_Freeze.Executable( + script=script, + target_name=scriptname + ("" if sys.platform == "linux" else ".exe"), + icon=icon, + )) +exes.append(cx_Freeze.Executable( + script="FactorioClientGUI.py", + target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"), + icon=icon, + base="Win32GUI" +)) + +import datetime + +buildtime = datetime.datetime.utcnow() + +cx_Freeze.setup( + name="Archipelago Factorio Client", + version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}", + description="Archipelago Factorio Client", + executables=exes, + options={ + "build_exe": { + "packages": ["websockets", "kivy"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["kivy"], + "include_files": [], + "include_msvcr": True, + "replace_paths": [("*", "")], + "optimize": 2, + "build_exe": buildfolder + }, + }, +) + + +extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"] +from kivy_deps import sdl2, glew +for folder in sdl2.dep_bins+glew.dep_bins: + shutil.copytree(folder, buildfolder, dirs_exist_ok=True) +for data in extra_data: + installfile(Path(data)) + + +os.makedirs(buildfolder / "Players", exist_ok=True) +shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml") + +if signtool: + for exe in exes: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(buildfolder, exe.target_name)) + +alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr" +for file in os.listdir(alttpr_sprites_folder): + if file != ".gitignore": + os.remove(alttpr_sprites_folder / file) + +manifest_creation(buildfolder) From 6e916ebd456e3c23f6204f6175e94d225717b48d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 29 May 2021 06:23:35 +0200 Subject: [PATCH 8/9] bake correct minimum version for Factorio into multidata --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index f3d8e429..41cdd7b3 100644 --- a/Main.py +++ b/Main.py @@ -503,7 +503,7 @@ def main(args, seed=None): games = {} for slot in world.player_ids: if world.game[slot] == "Factorio": - client_versions[slot] = (1, 1, 2) + client_versions[slot] = (0, 1, 2) else: client_versions[slot] = (0, 0, 3) games[slot] = world.game[slot] From 1d843467056665d7d84994e59d925ec9d1c31ba1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 29 May 2021 20:02:36 +0200 Subject: [PATCH 9/9] Factorio: Don't trigger bridge file on receiving a technology from server --- data/factorio/mod_template/control.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index 5d3a1868..c7dfe867 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -142,7 +142,9 @@ end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) local technology = event.research - dumpInfo(technology.force) + if technology.researched and string.find(technology.name, "ap%-") == 1 then + dumpInfo(technology.force) --is sendable + end if FREE_SAMPLES == 0 then return -- Nothing else to do end