diff --git a/core/game_overlay_integration.py b/core/game_overlay_integration.py new file mode 100644 index 0000000..206c34c --- /dev/null +++ b/core/game_overlay_integration.py @@ -0,0 +1,256 @@ +""" +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 diff --git a/core/main.py b/core/main.py index be570c7..88c379a 100644 --- a/core/main.py +++ b/core/main.py @@ -184,6 +184,14 @@ class EUUtilityApp: parent=self.app # Pass QApplication as parent ) + # Start game overlay integration (process-based detection + window attachment) + from core.game_overlay_integration import get_game_overlay_integration + self.game_overlay = get_game_overlay_integration(self.activity_bar, parent=self.app) + + # Connect signals + self.game_overlay.game_started.connect(lambda: print("[Main] Game detected - overlay attached")) + self.game_overlay.game_stopped.connect(lambda: print("[Main] Game ended - overlay detached")) + # Set mode from settings (default: game focused - Blish HUD style) settings = get_settings() mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game') @@ -521,6 +529,11 @@ class EUUtilityApp: """Quit the application.""" print("[Core] Shutting down...") + # Stop game overlay integration + if hasattr(self, 'game_overlay') and self.game_overlay: + print("[Core] Stopping Game Overlay Integration...") + self.game_overlay.stop() + # Stop overlay controller if hasattr(self, 'overlay_controller') and self.overlay_controller: print("[Core] Stopping Overlay Controller...")