""" EU-Utility - Game Overlay Integration Attaches the Activity Bar directly to the EU game window as a child window. This makes the overlay "burned into" the game - it moves and resizes with the game window. """ import sys from typing import Optional from PyQt6.QtCore import QTimer, QObject, pyqtSignal from PyQt6.QtWidgets import QApplication try: import ctypes from ctypes import wintypes WINDOWS_AVAILABLE = True except ImportError: WINDOWS_AVAILABLE = False class GameOverlayIntegration(QObject): """ Integrates the Activity Bar with the EU game window. Features: - Detects game by process name (not just window title) - Attaches overlay as child window (moves with game) - Hides/shows based on game process running """ game_started = pyqtSignal() game_stopped = pyqtSignal() def __init__(self, activity_bar, parent=None): super().__init__(parent) self.activity_bar = activity_bar self._game_window = None self._game_process_id = None self._is_attached = False # EU process names self.EU_PROCESSES = ["entropia.exe", "entropiauniverse.exe"] # Timer to check game status self._check_timer = QTimer(self) self._check_timer.timeout.connect(self._check_game_status) self._check_timer.start(2000) # Check every 2 seconds if WINDOWS_AVAILABLE: self.user32 = ctypes.windll.user32 self.kernel32 = ctypes.windll.kernel32 def _check_game_status(self): """Check if game process is running and manage overlay.""" if not WINDOWS_AVAILABLE: return try: # Find game process game_pid = self._find_game_process() if game_pid and not self._game_process_id: # Game just started print(f"[GameOverlay] EU process detected (PID: {game_pid})") self._game_process_id = game_pid self._attach_to_game() self.game_started.emit() elif not game_pid and self._game_process_id: # Game just stopped print("[GameOverlay] EU process ended") self._detach_from_game() self._game_process_id = None self.game_stopped.emit() elif game_pid and self._game_process_id: # Game still running - update position if attached if self._is_attached: self._update_overlay_position() except Exception as e: print(f"[GameOverlay] Error checking game: {e}") def _find_game_process(self) -> Optional[int]: """Find EU game process by name.""" if not WINDOWS_AVAILABLE: return None try: # Use tasklist to find process import subprocess result = subprocess.run( ['tasklist', '/FO', 'CSV', '/NH'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: for line in result.stdout.split('\n'): for proc_name in self.EU_PROCESSES: if proc_name.lower() in line.lower(): # Extract PID from CSV parts = line.split('","') if len(parts) >= 2: try: pid = int(parts[1]) return pid except ValueError: continue except Exception as e: print(f"[GameOverlay] Error finding process: {e}") return None def _attach_to_game(self): """Attach overlay to game window.""" if not self.activity_bar or not WINDOWS_AVAILABLE: return try: # Find game window self._game_window = self._find_game_window() if self._game_window: # Set activity bar as child of game window # This makes it move with the game window self._set_window_parent(self.activity_bar.winId(), self._game_window) self._is_attached = True print("[GameOverlay] Attached to game window") # Show the overlay self.activity_bar.show() else: print("[GameOverlay] Game process found but window not found yet") except Exception as e: print(f"[GameOverlay] Error attaching: {e}") def _detach_from_game(self): """Detach overlay from game window.""" if not self.activity_bar or not WINDOWS_AVAILABLE: return try: if self._is_attached: # Remove parent (make top-level again) self._set_window_parent(self.activity_bar.winId(), 0) self._is_attached = False print("[GameOverlay] Detached from game window") # Hide overlay self.activity_bar.hide() self._game_window = None except Exception as e: print(f"[GameOverlay] Error detaching: {e}") def _find_game_window(self) -> Optional[int]: """Find game window by process ID.""" if not WINDOWS_AVAILABLE or not self._game_process_id: return None found_hwnd = [None] def callback(hwnd, extra): if not self.user32.IsWindowVisible(hwnd): return True # Get process ID for this window pid = wintypes.DWORD() self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) if pid.value == self._game_process_id: # Check if it's the main game window (has a title) text = ctypes.create_unicode_buffer(256) self.user32.GetWindowTextW(hwnd, text, 256) if text.value and "Entropia" in text.value: found_hwnd[0] = hwnd return False # Stop enumeration return True # EnumWindows callback type EnumWindowsProc = ctypes.WINFUNCTYPE( wintypes.BOOL, wintypes.HWND, wintypes.LPARAM ) proc = EnumWindowsProc(callback) self.user32.EnumWindows(proc, 0) return found_hwnd[0] def _set_window_parent(self, child_hwnd: int, parent_hwnd: int): """Set window parent using SetParent API.""" if not WINDOWS_AVAILABLE: return # WS_CHILD style WS_CHILD = 0x40000000 WS_POPUP = 0x80000000 # Get current style GWL_STYLE = -16 current_style = self.user32.GetWindowLongW(child_hwnd, GWL_STYLE) if parent_hwnd: # Make it a child window new_style = (current_style | WS_CHILD) & ~WS_POPUP self.user32.SetWindowLongW(child_hwnd, GWL_STYLE, new_style) self.user32.SetParent(child_hwnd, parent_hwnd) else: # Make it a top-level window again new_style = (current_style | WS_POPUP) & ~WS_CHILD self.user32.SetWindowLongW(child_hwnd, GWL_STYLE, new_style) self.user32.SetParent(child_hwnd, 0) def _update_overlay_position(self): """Update overlay position to match game window.""" if not self._is_attached or not self._game_window or not WINDOWS_AVAILABLE: return try: # Get game window rect rect = wintypes.RECT() if self.user32.GetWindowRect(self._game_window, ctypes.byref(rect)): # Position activity bar at bottom of game window width = rect.right - rect.left height = 56 # Activity bar height x = rect.left + (width - self.activity_bar.width()) // 2 y = rect.bottom - height - 10 # 10px margin from bottom self.activity_bar.move(x, y) except Exception as e: pass # Ignore errors during position update def stop(self): """Stop the overlay integration.""" self._check_timer.stop() self._detach_from_game() # Singleton instance _overlay_integration = None def get_game_overlay_integration(activity_bar=None, parent=None): """Get or create the game overlay integration.""" global _overlay_integration if _overlay_integration is None: if activity_bar is None: raise ValueError("activity_bar required for first initialization") _overlay_integration = GameOverlayIntegration(activity_bar, parent) return _overlay_integration