import os import re import sys import time import json import ctypes import logging from datetime import datetime from urllib.parse import urlparse, urljoin, quote from colorama import init, Fore, Style # =================== Setup & Logging =================== init() def _env(key, default=None): return os.environ.get(key, default) LOG_LEVEL_NAME = _env("MYTHMU_LOG_LEVEL", "INFO").upper() LOG_TO_FILE = _env("MYTHMU_LOG_FILE", "").strip() LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.INFO) class ClientFormatter(logging.Formatter): def format(self, record): ts = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] color_map = { "DEBUG": Fore.BLUE, "INFO": Fore.YELLOW, "WARNING": Fore.MAGENTA, "ERROR": Fore.RED, "CRITICAL": Fore.RED + Style.BRIGHT, } color = color_map.get(record.levelname, Fore.WHITE) return f"{color}{ts} - CLIENT - {record.levelname} - {record.getMessage()}{Style.RESET_ALL}" log = logging.getLogger("MythMuClient") log.setLevel(logging.DEBUG) _stream = logging.StreamHandler() _stream.setLevel(LOG_LEVEL) _stream.setFormatter(ClientFormatter()) log.handlers.clear() log.addHandler(_stream) if LOG_TO_FILE: try: _fh = logging.FileHandler(LOG_TO_FILE, encoding="utf-8") _fh.setLevel(logging.DEBUG) _fh.setFormatter(logging.Formatter("%(asctime)s - CLIENT - %(levelname)s - %(message)s")) log.addHandler(_fh) log.info(f"File logging enabled -> {LOG_TO_FILE}") except Exception as e: log.error(f"Could not open log file '{LOG_TO_FILE}': {e}") log.info(f"Log level: {LOG_LEVEL_NAME}") ENABLE_DEV_FEATURES = _env("MYTHMU_DEV_TOOLS", "1") not in ("0", "false", "False") try: from cefpython3 import cefpython as cef except Exception as e: log.critical(f"CEF IMPORT ERROR: {e}") sys.exit(1) # =================== API (ONLY login + game/gl) =================== def _norm_base(s: str) -> str: return (s or "").strip().rstrip("/") API_BASE = _env("MYTHMU_API_BASE", "http://127.0.0.1") if API_BASE and not API_BASE.startswith(("http://", "https://")): API_BASE = "http://" + API_BASE API_BASE = _norm_base(API_BASE) API_LOGIN_URL = urljoin(API_BASE + "/", "api/auth/login") API_GAME_GL_URL = urljoin(API_BASE + "/", "api/game/gl") API_ORIGIN = f"{urlparse(API_BASE).scheme}://{urlparse(API_BASE).netloc}" log.info(f"API_BASE = {API_BASE}") log.info(f"API_LOGIN_URL = {API_LOGIN_URL}") log.info(f"API_GAME_GL = {API_GAME_GL_URL}") log.info(f"API_ORIGIN = {API_ORIGIN}") # =================== Paths & helpers =================== def resource_path(rel: str) -> str: try: base = sys._MEIPASS log.debug(f"resource_path: _MEIPASS={base}") except Exception: base = os.path.abspath(".") log.debug(f"resource_path: cwd={base}") if hasattr(sys, "frozen") and not hasattr(sys, "_MEIPASS"): base = os.path.join(os.path.abspath(os.path.dirname(sys.executable)), "LauncherData") log.debug(f"resource_path: frozen -> LauncherData={base}") p = os.path.join(base, rel) log.debug(f"resource_path('{rel}') -> {p}") return p def _file_url(path: str) -> str: ap = os.path.abspath(path).replace("\\", "/") furl = "file:///" + quote(ap) log.debug(f"_file_url({path}) -> {furl}") return furl def set_icon(browser): try: if os.name != "nt": log.debug("set_icon: non-Windows, skip") return base = os.path.abspath(os.path.dirname(sys.executable)) if hasattr(sys, "frozen") else os.path.abspath(".") icon = os.path.join(base, "icon.ico") if not os.path.exists(icon): icon2 = os.path.join(os.path.abspath("."), "icon.ico") if os.path.exists(icon2): icon = icon2 else: log.debug("set_icon: icon.ico not found, skip") return hwnd = browser.GetWindowHandle() user32 = ctypes.windll.user32 WM_SETICON = 0x0080 ICON_SMALL, ICON_BIG = 0, 1 LR_LOADFROMFILE, IMAGE_ICON = 0x0010, 1 hbig = user32.LoadImageW(0, icon, IMAGE_ICON, 0, 0, LR_LOADFROMFILE) if hbig: user32.SendMessageW(hwnd, WM_SETICON, ICON_BIG, hbig) hsm = user32.LoadImageW(0, icon, IMAGE_ICON, 0, 0, LR_LOADFROMFILE) if hsm: user32.SendMessageW(hwnd, WM_SETICON, ICON_SMALL, hsm) log.info(f"Window icon set: {icon}") except Exception as e: log.warning(f"Icon warn: {e}") def _base_dir(): if hasattr(sys, "_MEIPASS"): p = sys._MEIPASS log.debug(f"_base_dir: _MEIPASS={p}") return p if not hasattr(sys, "frozen"): try: cef_dir = os.path.dirname(cef.__file__) if os.path.exists(os.path.join(cef_dir, "subprocess.exe")): log.debug(f"_base_dir: cef dir={cef_dir}") return cef_dir else: log.debug("_base_dir: no subprocess.exe in cef dir, using cwd") return os.path.abspath(".") except NameError: log.debug("_base_dir: NameError on cef.__file__, using cwd") return os.path.abspath(".") exec_dir = os.path.join(os.path.abspath(os.path.dirname(sys.executable)), "LauncherData") log.debug(f"_base_dir: frozen LauncherData={exec_dir}") return exec_dir def get_flash_dll(): try: p = resource_path(os.path.join("Plugins", "pepflashplayer.dll")) if not os.path.exists(p): here = os.path.dirname(os.path.realpath(__file__)) p2 = os.path.join(here, "Plugins", "pepflashplayer.dll") if os.path.exists(p2): log.info(f"PepperFlash DLL (local): {p2}") return p2 log.error("Missing Plugins\\pepflashplayer.dll") return None log.info(f"PepperFlash DLL (bundle): {p}") return p except Exception as e: log.error(f"pepflash error: {e}") return None # =================== (rest of your code is unchanged until main) =================== # ... all your functions (inline login, open_game_from_redirect_url, rewrite, guards, handlers, etc.) ... # =================== Main =================== def main(): cef_inited = False try: base = _base_dir() subproc = os.path.join(base, "subprocess.exe") log.info(f"CEF base dir: {base}") if not os.path.exists(subproc): log.critical(f"Missing CEF subprocess: {subproc}") sys.exit(1) dll = get_flash_dll() if not dll: sys.exit(1) settings = { "resources_dir_path": base, "locales_dir_path": os.path.join(base, "locales"), "browser_subprocess_path": subproc, "log_severity": cef.LOGSEVERITY_DISABLE, } switches = { "ppapi-flash-path": dll, "allow-outdated-plugins": True, "no-sandbox": "", "disable-gpu-vsync": "", "disable-features": "VizDisplayCompositor", "allow-file-access-from-files": "", "allow-file-access": "", "disable-gpu": "", } log.info("CEF settings: " + json.dumps(settings, indent=2)) log.info("CEF switches: " + json.dumps(switches, indent=2)) cef.Initialize(settings=settings, switches=switches) cef_inited = True log.info("CEF initialized") browser = cef.CreateBrowserSync(url="about:blank", window_title="MythMu") set_icon(browser) # JS bridge bindings = cef.JavascriptBindings(bindToFrames=False, bindToPopups=False) def _open_game_from_redirect_url(final_url=None): log.info(f"[JS bridge] openGameFromRedirectUrl called: {final_url}") try: open_game_from_redirect_url(browser, final_url or "") except Exception as e: log.error(f"openGameFromRedirectUrl error: {e}") bindings.SetFunction("openGameFromRedirectUrl", _open_game_from_redirect_url) browser.SetJavascriptBindings(bindings) log.info("Javascript bridge registered") handler = AppHandler(browser) browser.SetClientHandler(handler) if ENABLE_DEV_FEATURES: try: browser.ShowDevTools() log.info("DevTools opened (ENABLE_DEV_FEATURES=1)") except Exception as e: log.warning(f"DevTools warn: {e}") else: log.info("DevTools disabled (ENABLE_DEV_FEATURES=0)") # === Fix: Reset resize cursor when refocusing window === import threading def force_cursor_reset(hwnd=None): try: user32 = ctypes.windll.user32 if not hwnd: hwnd = browser.GetWindowHandle() user32.SetCursor(user32.LoadCursorW(0, 32512)) # IDC_ARROW log.debug("Cursor reset to IDC_ARROW") except Exception as e: log.warning(f"Cursor reset failed: {e}") def on_got_focus(browser, **_): log.debug("Browser gained focus — resetting cursor") threading.Timer(0.05, lambda: force_cursor_reset(browser.GetWindowHandle())).start() focus_handler = type("FocusHandler", (), {"OnGotFocus": on_got_focus})() browser.SetClientHandler(focus_handler) log.info("Focus handler for cursor reset registered") # === End fix === log_local_assets_presence() load_local_login(browser) log.info("Entering CEF message loop") cef.MessageLoop() except Exception as e: log.critical(f"CRITICAL in main: {e}") finally: try: if cef_inited: cef.Shutdown() log.info("CEF shutdown completed") except Exception as e: log.error(f"Shutdown error: {e}") if __name__ == "__main__": main()