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 new file mode 100644 index 00000000..d4f4bf19 --- /dev/null +++ b/FactorioClientGUI.py @@ -0,0 +1,155 @@ +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")) +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 + + +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) + self.icon = "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 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) + + +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 +: + 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) +''') + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/Main.py b/Main.py index ed0ac0e1..41cdd7b3 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] = (0, 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..c7dfe867 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -133,16 +133,18 @@ script.on_init(function() end) -- for testing -script.on_event(defines.events.on_tick, function(event) - if event.tick%600 == 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) 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 @@ -186,6 +188,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 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; + + 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) 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.'