feat: Game Overlay Integration - Process-based detection + window attachment

New file: core/game_overlay_integration.py
- Detects EU game process by name (entropia.exe, entropiauniverse.exe)
- Attaches Activity Bar as child window of game window
- Overlay moves and resizes with game window ('burned in' effect)
- Shows/hides based on game process running
- 2-second polling interval to check game status

Integration:
- Added to main.py alongside overlay_controller
- Properly starts and stops with app
- Console messages when game detected/ended

This makes the overlay truly part of the game window!
This commit is contained in:
LemonNexus 2026-02-16 00:58:26 +00:00
parent b8d127a0e2
commit e78415c5eb
2 changed files with 269 additions and 0 deletions

View File

@ -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

View File

@ -184,6 +184,14 @@ class EUUtilityApp:
parent=self.app # Pass QApplication as parent 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) # Set mode from settings (default: game focused - Blish HUD style)
settings = get_settings() settings = get_settings()
mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game') mode_str = settings.get('activity_bar.overlay_mode', 'overlay_game')
@ -521,6 +529,11 @@ class EUUtilityApp:
"""Quit the application.""" """Quit the application."""
print("[Core] Shutting down...") 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 # Stop overlay controller
if hasattr(self, 'overlay_controller') and self.overlay_controller: if hasattr(self, 'overlay_controller') and self.overlay_controller:
print("[Core] Stopping Overlay Controller...") print("[Core] Stopping Overlay Controller...")