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
|
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...")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue