diff --git a/CommonClient.py b/CommonClient.py index c984bea0..6078c380 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -8,12 +8,8 @@ import websockets import Utils from MultiServer import CommandProcessor - from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus from Utils import Version - -# logging note: -# logging.* gets send to only the text console, logger.* gets send to the WebUI as well, if it's initialized. from worlds import network_data_package, AutoWorldRegister logger = logging.getLogger("Client") @@ -93,7 +89,7 @@ class CommonContext(): command_processor = ClientCommandProcessor game: None - def __init__(self, server_address, password, found_items: bool): + def __init__(self, server_address, password): # server state self.server_address = server_address self.password = password @@ -104,7 +100,6 @@ class CommonContext(): # own state self.finished_game = False self.ready = False - self.found_items = found_items self.team = None self.slot = None self.auth = None diff --git a/FactorioClient.py b/FactorioClient.py index a6a9fe1e..9798c6c4 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -20,23 +20,116 @@ from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextP 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)) +os.makedirs("logs", exist_ok=True) -logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) -factorio_server_logger = logging.getLogger("FactorioServer") -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) +# Log to file in gui case +if getattr(sys, "frozen", False) and not "--nogui" in sys.argv: + logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, + filename=os.path.join("logs", "FactorioClient.txt"), filemode="w") +else: + logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) + logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) -server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) +gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv + +if gui_enabled: + os.environ["KIVY_NO_CONSOLELOG"] = "1" + os.environ["KIVY_NO_FILELOG"] = "1" + os.environ["KIVY_NO_ARGS"] = "1" + from kivy.app import App + from kivy.base import ExceptionHandler, ExceptionManager, Config + from kivy.uix.gridlayout import GridLayout + from kivy.uix.textinput import TextInput + from kivy.uix.recycleview import RecycleView + from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem + from kivy.lang import Builder + + + class FactorioManager(App): + def __init__(self, ctx): + super(FactorioManager, self).__init__() + self.ctx = ctx + self.commandprocessor = ctx.command_processor(ctx) + self.icon = r"data/icon.png" + + def build(self): + self.grid = GridLayout() + self.grid.cols = 1 + + self.tabs = TabbedPanel() + self.tabs.default_tab_text = "All" + self.title = "Archipelago Factorio Client" + pairs = [ + ("Client", "Archipelago"), + ("FactorioServer", "Factorio Server Log"), + ("FactorioWatcher", "Bridge Data Log"), + ] + self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs)) + for logger_name, display_name in pairs: + bridge_logger = logging.getLogger(logger_name) + panel = TabbedPanelItem(text=display_name) + panel.content = UILog(bridge_logger) + self.tabs.add_widget(panel) + + self.grid.add_widget(self.tabs) + textinput = TextInput(size_hint_y=None, height=30, multiline=False) + textinput.bind(on_text_validate=self.on_message) + self.grid.add_widget(textinput) + self.commandprocessor("/help") + return self.grid + + def on_stop(self): + self.ctx.exit_event.set() + + def on_message(self, textinput: TextInput): + try: + input_text = textinput.text.strip() + textinput.text = "" + + if self.ctx.input_requests > 0: + self.ctx.input_requests -= 1 + self.ctx.input_queue.put_nowait(input_text) + elif input_text: + self.commandprocessor(input_text) + except Exception as e: + logger.exception(e) + + def on_address(self, text: str): + print(text) + + + class LogtoUI(logging.Handler): + def __init__(self, on_log): + super(LogtoUI, self).__init__(logging.DEBUG) + self.on_log = on_log + + def handle(self, record: logging.LogRecord) -> None: + self.on_log(record) + + + class UILog(RecycleView): + cols = 1 + + def __init__(self, *loggers_to_handle, **kwargs): + super(UILog, self).__init__(**kwargs) + self.data = [] + for logger in loggers_to_handle: + logger.addHandler(LogtoUI(self.on_log)) + + def on_log(self, record: logging.LogRecord) -> None: + self.data.append({"text": record.getMessage()}) + + + class E(ExceptionHandler): + def handle_exception(self, inst): + logger.exception(inst) + return ExceptionManager.RAISE + + + ExceptionManager.add_handler(E()) + + Config.set("input", "mouse", "mouse,disable_multitouch") + Builder.load_file(Utils.local_path("data", "client.kv")) class FactorioCommandProcessor(ClientCommandProcessor): @@ -56,7 +149,7 @@ class FactorioCommandProcessor(ClientCommandProcessor): """Connect to a MultiWorld Server""" if not self.ctx.auth: if self.ctx.rcon_client: - get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code + 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) @@ -66,8 +159,8 @@ class FactorioContext(CommonContext): command_processor = FactorioCommandProcessor game = "Factorio" - def __init__(self, *args, **kwargs): - super(FactorioContext, self).__init__(*args, **kwargs) + def __init__(self, server_address, password): + super(FactorioContext, self).__init__(server_address, password) self.send_index = 0 self.rcon_client = None self.awaiting_bridge = False @@ -92,8 +185,6 @@ class FactorioContext(CommonContext): f"{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(copy.deepcopy(args["data"])) logger.info(text) if self.rcon_client: @@ -118,7 +209,8 @@ async def game_watcher(ctx: FactorioContext): if data["slot_name"] != ctx.auth: logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}") elif data["seed_name"] != ctx.seed_name: - logger.warning(f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") + logger.warning( + f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") else: data = data["info"] research_data = data["research_done"] @@ -246,7 +338,6 @@ async def factorio_spinup_server(ctx: FactorioContext): rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) get_info(ctx, rcon_client) - await asyncio.sleep(0.01) except Exception as e: @@ -261,12 +352,12 @@ async def factorio_spinup_server(ctx: FactorioContext): factorio_process.terminate() -async def main(ui=None): - ctx = FactorioContext(None, None, True) +async def main(args): + ctx = FactorioContext(args.connect, args.password) ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - if ui: + if gui_enabled: input_task = None - ui_app = ui(ctx) + ui_app = FactorioManager(ctx) ui_task = asyncio.create_task(ui_app.async_run(), name="UI") else: input_task = asyncio.create_task(console_loop(ctx), name="Input") @@ -314,8 +405,36 @@ class FactorioJSONtoTextParser(JSONtoTextParser): if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio') + parser.add_argument('--connect', default=None, help='Address of the multiworld host.') + parser.add_argument('--password', default=None, help='Password of the multiworld host.') + if not Utils.is_frozen(): # Frozen state has no cmd window in the first place + parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.") + parser.add_argument('factorio_server_args', nargs='*', help="All remaining arguments get passed " + "into the Factorio server startup.") + args = parser.parse_args() colorama.init() + rcon_port = args.rcon_port + rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) + + factorio_server_logger = logging.getLogger("FactorioServer") + 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) + + server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *args.factorio_server_args) + loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + loop.run_until_complete(main(args)) loop.close() colorama.deinit() diff --git a/FactorioClientGUI.py b/FactorioClientGUI.py deleted file mode 100644 index cc6c3063..00000000 --- a/FactorioClientGUI.py +++ /dev/null @@ -1,141 +0,0 @@ -import os -import logging -import sys -os.makedirs("logs", exist_ok=True) -if getattr(sys, "frozen", False): - logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, - filename=os.path.join("logs", "FactorioClient.txt"), filemode="w") -else: - logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) - logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) -os.environ["KIVY_NO_CONSOLELOG"] = "1" -os.environ["KIVY_NO_FILELOG"] = "1" -os.environ["KIVY_NO_ARGS"] = "1" - -import asyncio -from CommonClient import logger -from FactorioClient import main - - -from kivy.app import App -from kivy.uix.label import Label -from kivy.base import ExceptionHandler, ExceptionManager, Config -from kivy.uix.gridlayout import GridLayout -from kivy.uix.textinput import TextInput -from kivy.uix.recycleview import RecycleView -from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem -from kivy.lang import Builder - - -class FactorioManager(App): - def __init__(self, ctx): - super(FactorioManager, self).__init__() - self.ctx = ctx - self.commandprocessor = ctx.command_processor(ctx) - self.icon = r"data/icon.png" - - def build(self): - self.grid = GridLayout() - self.grid.cols = 1 - self.tabs = TabbedPanel() - self.tabs.default_tab_text = "All" - self.title = "Archipelago Factorio Client" - pairs = [ - ("Client", "Archipelago"), - ("FactorioServer", "Factorio Server Log"), - ("FactorioWatcher", "Bridge Data Log"), - ] - self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs)) - for logger_name, display_name in pairs: - bridge_logger = logging.getLogger(logger_name) - panel = TabbedPanelItem(text=display_name) - panel.content = UILog(bridge_logger) - self.tabs.add_widget(panel) - - self.grid.add_widget(self.tabs) - textinput = TextInput(size_hint_y=None, height=30, multiline=False) - textinput.bind(on_text_validate=self.on_message) - self.grid.add_widget(textinput) - self.commandprocessor("/help") - return self.grid - - def on_stop(self): - self.ctx.exit_event.set() - - def on_message(self, textinput: TextInput): - try: - input_text = textinput.text.strip() - textinput.text = "" - - if self.ctx.input_requests > 0: - self.ctx.input_requests -= 1 - self.ctx.input_queue.put_nowait(input_text) - elif input_text: - self.commandprocessor(input_text) - except Exception as e: - logger.exception(e) - - -class LogtoUI(logging.Handler): - def __init__(self, on_log): - super(LogtoUI, self).__init__(logging.DEBUG) - self.on_log = on_log - - def handle(self, record: logging.LogRecord) -> None: - self.on_log(record) - - -class UILog(RecycleView): - cols = 1 - - def __init__(self, *loggers_to_handle, **kwargs): - super(UILog, self).__init__(**kwargs) - self.data = [] - for logger in loggers_to_handle: - logger.addHandler(LogtoUI(self.on_log)) - - def on_log(self, record: logging.LogRecord) -> None: - self.data.append({"text": record.getMessage()}) - - -class E(ExceptionHandler): - def handle_exception(self, inst): - logger.exception(inst) - return ExceptionManager.RAISE - -ExceptionManager.add_handler(E()) - - -Config.set("input", "mouse", "mouse,disable_multitouch") -Builder.load_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() - ui_app = FactorioManager - loop.run_until_complete(main(ui_app)) - loop.close() diff --git a/LttPClient.py b/LttPClient.py index 6a4060b3..44e3ef23 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -59,8 +59,8 @@ class Context(CommonContext): command_processor = LttPCommandProcessor game = "A Link to the Past" - def __init__(self, snes_address, server_address, password, found_items): - super(Context, self).__init__(server_address, password, found_items) + def __init__(self, snes_address, server_address, password): + super(Context, self).__init__(server_address, password) # snes stuff self.snes_address = snes_address diff --git a/Utils.py b/Utils.py index 662a6b6e..b6e03e58 100644 --- a/Utils.py +++ b/Utils.py @@ -84,7 +84,7 @@ def cache_argsless(function): return _wrap -def is_bundled() -> bool: +def is_frozen() -> bool: return getattr(sys, 'frozen', False) @@ -92,7 +92,7 @@ def local_path(*path): if local_path.cached_path: return os.path.join(local_path.cached_path, *path) - elif is_bundled(): + elif is_frozen(): if hasattr(sys, "_MEIPASS"): # we are running in a PyInstaller bundle local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member diff --git a/data/client.kv b/data/client.kv new file mode 100644 index 00000000..052ec723 --- /dev/null +++ b/data/client.kv @@ -0,0 +1,24 @@ + + 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) \ No newline at end of file diff --git a/setup.py b/setup.py index 36573edb..c13a141a 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,7 @@ scripts = {"LttPClient.py": "ArchipelagoLttPClient", "MultiMystery.py": "ArchipelagoMultiMystery", "MultiServer.py": "ArchipelagoServer", "Mystery.py": "ArchipelagoMystery", - "LttPAdjuster.py": "ArchipelagoLttPAdjuster", - "FactorioClient.py": "ArchipelagoFactorioClient"} + "LttPAdjuster.py": "ArchipelagoLttPAdjuster"} exes = [] @@ -153,22 +152,14 @@ 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"), +exes = [ + cx_Freeze.Executable( + script="FactorioClient.py", + target_name="ArchipelagoFactorioClient" + ("" if sys.platform == "linux" else ".exe"), icon=icon, - base="Win32GUI" -)) + base="Win32GUI" if sys.platform == "win32" else None +)] import datetime diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index ae222f01..5810e0a7 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -31,7 +31,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_bundled +from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen from worlds.alttp.Items import ItemFactory, item_table from worlds.alttp.EntranceShuffle import door_addresses import Patch @@ -1843,7 +1843,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr option_name: True } - data_dir = local_path("data") if is_bundled() else None + data_dir = local_path("data") if is_frozen() else None offsets_array = build_offset_collections(options, data_dir) restore_maseya_colors(rom, offsets_array) if mode == 'default': diff --git a/worlds/oribf/Rules.py b/worlds/oribf/Rules.py index 346810c8..fdf8a54d 100644 --- a/worlds/oribf/Rules.py +++ b/worlds/oribf/Rules.py @@ -11,7 +11,8 @@ def set_rules(world): apply_or_ruleset(world.world, world.player, logicset) -tautology = lambda state: True +def tautology(state): + return True def add_or_rule_check_first(world, location: str, player: int, conditionsets):