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:
parent
b8d127a0e2
commit
e78415c5eb
|
|
@ -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
|
||||
13
core/main.py
13
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...")
|
||||
|
|
|
|||
Loading…
Reference in New Issue