""" foreground_fix.py Wartet auf den Start von CrimsonDesert.exe und bringt das Spielfenster automatisch in den Vordergrund. Verwendung: pythonw foreground_fix.py [--timeout 120] [--process CrimsonDesert] Abhängigkeiten (im .venv): pip install psutil pywin32 """ import argparse import ctypes import logging import sys import time import psutil import win32api import win32con import win32gui import win32process from pathlib import Path _LOG_PATH = Path(__file__).resolve().parent / "foreground_fix.log" logging.basicConfig( filename=str(_LOG_PATH), level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s", ) # Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen. MIN_WINDOW_WIDTH = 800 # Anzahl der Fokus-Versuche und Pause dazwischen FOCUS_RETRIES = 15 FOCUS_RETRY_DELAY = 2.0 def find_process_pids(name: str) -> set[int]: pids = set() for proc in psutil.process_iter(["pid", "name"]): try: if name.lower() in proc.info["name"].lower(): pids.add(proc.info["pid"]) except (psutil.NoSuchProcess, psutil.AccessDenied): pass return pids def find_game_window(pids: set[int]) -> int | None: found = [] def callback(hwnd, _): if not win32gui.IsWindowVisible(hwnd): return True try: _, pid = win32process.GetWindowThreadProcessId(hwnd) except Exception: return True if pid not in pids: return True try: rect = win32gui.GetWindowRect(hwnd) except Exception: return True width = rect[2] - rect[0] height = rect[3] - rect[1] if width >= MIN_WINDOW_WIDTH: title = win32gui.GetWindowText(hwnd) logging.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid) found.append((hwnd, title, width, height)) return True win32gui.EnumWindows(callback, None) if not found: return None # Fenster mit Titel bevorzugen, dann nach Größe sortieren found.sort(key=lambda x: (bool(x[1]), x[2] * x[3]), reverse=True) return found[0][0] def wait_for_stable_size(hwnd: int, polls: int = 3, interval: float = 0.5, timeout: float = 5.0) -> tuple[int, int]: """Wartet bis die Fenstergröße sich stabilisiert hat (kein Resize mehr). Gibt die finale (width, height) zurück.""" last_size = (0, 0) stable_count = 0 deadline = time.time() + timeout while time.time() < deadline: try: rect = win32gui.GetWindowRect(hwnd) size = (rect[2] - rect[0], rect[3] - rect[1]) except Exception: break if size == last_size: stable_count += 1 if stable_count >= polls: break else: stable_count = 0 last_size = size time.sleep(interval) logging.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)", hwnd, last_size[0], last_size[1], stable_count) return last_size def is_foreground(hwnd: int) -> bool: """Prüft ob das angegebene Fenster aktuell das Vordergrundfenster ist.""" return win32gui.GetForegroundWindow() == hwnd def bring_to_front(hwnd: int) -> bool: """ Bringt das Fenster in den Vordergrund. Gibt True zurück wenn erfolgreich. Verwendet Alt-Key + AttachThreadInput als Workaround. """ try: # Alt-Taste simulieren — hebt die SetForegroundWindow-Sperre auf win32api.keybd_event(win32con.VK_MENU, 0, 0, 0) time.sleep(0.05) fg_hwnd = win32gui.GetForegroundWindow() fg_tid, _ = win32process.GetWindowThreadProcessId(fg_hwnd) tgt_tid, _ = win32process.GetWindowThreadProcessId(hwnd) attached = False if fg_tid != tgt_tid: ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, True) attached = True win32gui.ShowWindow(hwnd, win32con.SW_SHOW) win32gui.BringWindowToTop(hwnd) win32gui.SetForegroundWindow(hwnd) ctypes.windll.user32.SetFocus(hwnd) ctypes.windll.user32.SetActiveWindow(hwnd) if attached: ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False) win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0) result = is_foreground(hwnd) logging.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)", result, win32gui.GetForegroundWindow(), hwnd) return result except Exception as e: logging.exception("bring_to_front failed: %s", e) return False def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--timeout", type=int, default=120) parser.add_argument("--process", type=str, required=True, help="Prozessname des Spiels (ohne .exe)") args = parser.parse_args() deadline = time.time() + args.timeout process_name = args.process # --- Phase 1: Warten auf den Prozess --- pids: set[int] = set() while time.time() < deadline: pids = find_process_pids(process_name) if pids: break time.sleep(0.5) else: sys.exit(1) # --- Phase 2: Warten auf das Hauptfenster --- hwnd: int | None = None while time.time() < deadline: pids = find_process_pids(process_name) hwnd = find_game_window(pids) if hwnd: logging.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd)) # Warten bis das Spiel sein Fenster vollständig aufgebaut hat time.sleep(3) break time.sleep(0.5) else: logging.error("Timeout waiting for game window") sys.exit(1) # --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren --- # Beobachtet über die gesamte Retry-Dauer, ob sich das Fenster ändert # (Splash -> Hauptfenster), und fokussiert jedes neue Fenster erneut. focused_hwnd: int | None = None for attempt in range(FOCUS_RETRIES): pids = find_process_pids(process_name) if not pids: logging.warning("Game process gone, stopping") break current_hwnd = find_game_window(pids) if current_hwnd and current_hwnd != hwnd: new_title = win32gui.GetWindowText(current_hwnd) old_title = win32gui.GetWindowText(hwnd) if hwnd else "" # Nicht von einem Fenster mit Titel auf ein titelloses wechseln if not new_title and old_title: logging.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)", current_hwnd, hwnd, old_title) else: logging.info("Window changed: old=%s new=%s title=%r", hwnd, current_hwnd, new_title) hwnd = current_hwnd focused_hwnd = None # neues Fenster muss neu fokussiert werden time.sleep(1) if not hwnd: time.sleep(FOCUS_RETRY_DELAY) continue # Bereits erfolgreich fokussiert — weiter beobachten ob Fenster wechselt if focused_hwnd == hwnd and is_foreground(hwnd): time.sleep(FOCUS_RETRY_DELAY) continue # Warten bis Fenstergröße stabil ist (Resize abgeschlossen) wait_for_stable_size(hwnd) logging.info("Focus attempt %d/%d for hwnd=%s title=%r", attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd)) success = bring_to_front(hwnd) if success: logging.info("Focus succeeded on attempt %d", attempt + 1) focused_hwnd = hwnd time.sleep(FOCUS_RETRY_DELAY) logging.info("Done. Final focused hwnd=%s", focused_hwnd) if __name__ == "__main__": main()