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()