Skip to content
97 changes: 78 additions & 19 deletions app/task/Okww/tools/account_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def _write_diagnostic(text: str) -> None:
_WUWA_CLASS = "UnrealWindow"
_WUWA_PROCESS = "Client-Win64-Shipping.exe"

# 游戏窗口就绪宽限期:进程拉起到 UnrealWindow 窗口可见通常存在启动延迟,且设备
# 性能越差窗口创建/亮相越慢。账号切换紧随脚本配置的定长 WaitTime 之后立即执行,
# 须在宽限期内轮询等待窗口就绪,否则慢设备会误报「未找到鸣潮游戏窗口」。
_GAME_WINDOW_WAIT_SECONDS = 60.0
# 窗口轮询间隔。
_WINDOW_POLL_INTERVAL = 1.0
# 启动界面稳定等待:窗口已出现但尚未进入可执行登录态(仍停在警告/加载过渡帧)时,
# 在宽限期内轮询等待「登录页」或「已登录主菜单」出现;超时说明可能已直接进入游戏
# 主场景,交由返回登录流程兜底,避免在过渡帧上盲目按 ESC。
_SWITCH_STABILIZE_SECONDS = 45.0

# 截图基准分辨率(16:9),OCR 与点击均在此坐标空间计算后再映射回真实窗口
_FRAME_WIDTH = 1920
_FRAME_HEIGHT = 1080
Expand Down Expand Up @@ -132,33 +143,49 @@ def _window_area(hwnd: int) -> int:
return 0


def _find_game_hwnd() -> int:
def _find_game_hwnd(*, wait: bool = True) -> int:
"""按窗口类 + 所属进程名定位鸣潮主窗口。

用 ``EnumWindows + psutil.Process(pid).name()`` 而非 process_iter 的 name 属性,
规避提权进程 name 读取被拒导致的漏判;同进程存在多个窗口时取面积最大者。

Args:
wait: 为 True 时在宽限期 ``_GAME_WINDOW_WAIT_SECONDS`` 内轮询等待窗口就绪,
以吸收进程拉起后窗口延迟亮相的启动阶段;为 False 时单次枚举立即返回。

Raises:
RuntimeError: 宽限期结束仍未定位到鸣潮游戏窗口。
"""
candidates: list[int] = []
deadline = time.monotonic() + _GAME_WINDOW_WAIT_SECONDS
while True:
candidates: list[int] = []

def _enum(hwnd: int, _lparam: int) -> bool:
try:
if not win32gui.IsWindowVisible(hwnd):
return True
if win32gui.GetClassName(hwnd) != _WUWA_CLASS:
def _enum(hwnd: int, _lparam: int) -> bool:
try:
if not win32gui.IsWindowVisible(hwnd):
return True
if win32gui.GetClassName(hwnd) != _WUWA_CLASS:
return True
except Exception:
return True
except Exception:
_, pid = win32process.GetWindowThreadProcessId(hwnd)
if pid and _process_name(pid) == _WUWA_PROCESS:
candidates.append(hwnd)
return True
_, pid = win32process.GetWindowThreadProcessId(hwnd)
if pid and _process_name(pid) == _WUWA_PROCESS:
candidates.append(hwnd)
return True

win32gui.EnumWindows(_enum, 0)
if not candidates:
raise RuntimeError(
f"未找到鸣潮游戏窗口(进程 {_WUWA_PROCESS},窗口类 {_WUWA_CLASS})"
win32gui.EnumWindows(_enum, 0)
if candidates:
return max(candidates, key=_window_area)
if not wait or time.monotonic() >= deadline:
break
logger.info(
"鸣潮游戏进程已启动但窗口暂未就绪,"
f"{_WINDOW_POLL_INTERVAL:g} 秒后重试..."
)
return max(candidates, key=_window_area)
time.sleep(_WINDOW_POLL_INTERVAL)
raise RuntimeError(
f"未找到鸣潮游戏窗口(进程 {_WUWA_PROCESS},窗口类 {_WUWA_CLASS})"
)


# ── 截图 / 交互(前台 pyautogui + DPI 适配)─────────────────────────────
Expand Down Expand Up @@ -389,8 +416,39 @@ def _logout_from_menu(hwnd: int, on_log: Callable[[str], None]) -> None:
raise RuntimeError("未找到「确认登出」入口,请人工确认主菜单登出按钮位置")


def _wait_for_actionable_state(
hwnd: int, on_log: Callable[[str], None]
) -> None:
"""等待进入可执行的登录态(登录页或已登录主菜单)。

游戏窗口刚出现时可能仍停在启动过渡帧(警告弹窗、加载条、自动登录等),此时登录页
与主菜单两态都不命中。若立即按 ESC 走返回登录,可能落在不匹配的画面上。故在宽限期
``_SWITCH_STABILIZE_SECONDS`` 内轮询等待两态之一出现:

- 命中任意态 → 返回,由调用方按对应态执行(登录页→选择登录,主菜单→登出);
- 超时仍未命中 → 可能已直接进入游戏主场景(无法用登录/菜单特征识别),交由调用方
走「ESC→终端→返回登录」兜底。

进度日志按约 5 条/轮询节流,避免每秒向调度台刷屏。
"""
deadline = time.monotonic() + _SWITCH_STABILIZE_SECONDS
iter_count = 0
while time.monotonic() < deadline:
if _on_login_page(hwnd) or _on_logged_in_menu(hwnd):
return
if iter_count % 5 == 0:
on_log("窗口已出现但尚未进入登录态,等待启动过渡帧稳定...")
iter_count += 1
time.sleep(1)
on_log(
f"等待登录界面稳定超时({_SWITCH_STABILIZE_SECONDS:g}s),按未知状态走返回登录流程"
)


def _switch_to_login(hwnd: int, on_log: Callable[[str], None]) -> None:
"""回到登录界面;已处于登录页直接返回,主菜单走登出,其余走游戏内返回。"""
"""回到登录界面;已处于登录页直接返回,主菜单走登出,其余(含游戏主场景)走游戏内返回。"""
_wait_for_actionable_state(hwnd, on_log)

if _on_login_page(hwnd):
on_log("已处于登录界面,跳过返回登录")
return
Expand Down Expand Up @@ -590,7 +648,8 @@ def _on_log(msg: str) -> None:
_select_and_login(hwnd, suffix, _on_log)
except Exception:
try:
_save_error_screenshot(_find_game_hwnd())
# wait=False:主流程已等待过窗口,此处单次枚举即可,避免失败后再空等宽限期。
_save_error_screenshot(_find_game_hwnd(wait=False))
except Exception:
pass
raise
Expand Down
3 changes: 2 additions & 1 deletion res/version.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"OK-WW与OK-NTE专项 多用户切换时等待上一用户的游戏进程完全退出,避免误复用正在退出的旧游戏窗口导致任务被中止 by [@AthenaHibou](https://github.com/AthenaHibou)",
"OK-NTE专项 修复任务报告剩余体力计算不准确的问题:体力不足以刷满设定目标时误显「剩余体力: 0」,改为按实际刷本消耗(异象界域双倍/单倍次数、异象追猎实际消耗)精确计算剩余体力 by [@AthenaHibou](https://github.com/AthenaHibou)",
"前端类型治理 清理脚本与 Electron 基础边界中的 any,并修复 BetterGI 设置会话无法接收完成和错误事件的问题",
"HSR专项 修复 MAS 管控任务的文本与 JSON 配置项在输入过程中被重渲染清空、导致只有数字项能填写的问题(如货币战争的首领/投资重开条件、策略文件、开拓者名称) by [@qiyinxi](https://github.com/qiyinxi)"
"HSR专项 修复 MAS 管控任务的文本与 JSON 配置项在输入过程中被重渲染清空、导致只有数字项能填写的问题(如货币战争的首领/投资重开条件、策略文件、开拓者名称) by [@qiyinxi](https://github.com/qiyinxi)",
"OK-WW专项 修复账号切换在游戏启动阶段定位不到窗口、或在过渡帧上就直接操作而失败的问题,改为分两段在宽限期内轮询等待:先等窗口就绪,再等界面进入可执行的登录态,兼容低性能设备的启动延迟"
]
},
"v5.5.0-beta.2": {
Expand Down
Loading