"""Game window enumeration + capture. On Windows we use pygetwindow to list visible top-level windows by title and pywin32 (PrintWindow) to grab a single window — that works even when the window is partially covered or not focused, which a regular mss screen grab does not. On non-Windows we degrade gracefully: list_windows() returns [] and capture_window() falls back to a full-screen grab via mss. This is mostly so the GUI imports cleanly during dev on Linux — actual gameplay recognition is a Windows-only feature. """ from __future__ import annotations import platform import sys from dataclasses import dataclass from typing import List, Optional from PIL import Image @dataclass class WindowInfo: handle: int # HWND on Windows, 0 elsewhere title: str left: int top: int width: int height: int def _is_windows() -> bool: return platform.system() == "Windows" def list_windows() -> List[WindowInfo]: """Return visible top-level windows with a title. Windows-only; [] otherwise.""" if not _is_windows(): return [] try: import pygetwindow as gw # type: ignore except Exception: return [] out: List[WindowInfo] = [] for w in gw.getAllWindows(): try: if not w.title: continue if w.width <= 50 or w.height <= 50: continue if not w.visible: continue except Exception: continue try: out.append(WindowInfo( handle=int(w._hWnd) if hasattr(w, "_hWnd") else 0, title=w.title, left=w.left, top=w.top, width=w.width, height=w.height, )) except Exception: continue return out def find_sephiria() -> Optional[WindowInfo]: """Best-effort: pick the first window whose title contains 'Sephiria' / '세피리아'.""" for w in list_windows(): t = w.title.lower() if "sephiria" in t or "세피리아" in w.title: return w return None def capture_window(info: WindowInfo) -> Image.Image: """Capture a single window into a PIL RGB image. On Windows uses PrintWindow with PW_RENDERFULLCONTENT (works on hidden windows / behind-other-windows). On other OSes falls back to an mss region grab of the window's bounding rectangle on the primary monitor. """ if _is_windows() and info.handle: try: return _capture_window_win(info) except Exception: pass # Fallback: mss region grab return _capture_region_mss(info.left, info.top, info.width, info.height) def _capture_window_win(info: WindowInfo) -> Image.Image: import ctypes from ctypes import wintypes hwnd = info.handle user32 = ctypes.windll.user32 gdi32 = ctypes.windll.gdi32 # Use the actual client/window rect — pygetwindow's width/height can # include shadow/DWM regions; GetWindowRect is more reliable. rect = wintypes.RECT() user32.GetWindowRect(hwnd, ctypes.byref(rect)) width = rect.right - rect.left height = rect.bottom - rect.top if width <= 0 or height <= 0: raise RuntimeError("window has zero size") hwndDC = user32.GetWindowDC(hwnd) mfcDC = gdi32.CreateCompatibleDC(hwndDC) bitmap = gdi32.CreateCompatibleBitmap(hwndDC, width, height) gdi32.SelectObject(mfcDC, bitmap) PW_RENDERFULLCONTENT = 0x00000002 ok = user32.PrintWindow(hwnd, mfcDC, PW_RENDERFULLCONTENT) # Read pixels class BITMAPINFOHEADER(ctypes.Structure): _fields_ = [ ("biSize", wintypes.DWORD), ("biWidth", ctypes.c_long), ("biHeight", ctypes.c_long), ("biPlanes", wintypes.WORD), ("biBitCount", wintypes.WORD), ("biCompression", wintypes.DWORD), ("biSizeImage", wintypes.DWORD), ("biXPelsPerMeter", ctypes.c_long), ("biYPelsPerMeter", ctypes.c_long), ("biClrUsed", wintypes.DWORD), ("biClrImportant", wintypes.DWORD), ] class BITMAPINFO(ctypes.Structure): _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wintypes.DWORD * 3)] bmi = BITMAPINFO() bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) bmi.bmiHeader.biWidth = width bmi.bmiHeader.biHeight = -height # top-down bmi.bmiHeader.biPlanes = 1 bmi.bmiHeader.biBitCount = 32 bmi.bmiHeader.biCompression = 0 # BI_RGB buffer = ctypes.create_string_buffer(width * height * 4) gdi32.GetDIBits(mfcDC, bitmap, 0, height, buffer, ctypes.byref(bmi), 0) gdi32.DeleteObject(bitmap) gdi32.DeleteDC(mfcDC) user32.ReleaseDC(hwnd, hwndDC) img = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA", 0, 1) return img.convert("RGB") def _capture_region_mss(left: int, top: int, width: int, height: int) -> Image.Image: import mss with mss.mss() as sct: region = {"left": left, "top": top, "width": width, "height": height} shot = sct.grab(region) img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX") return img