using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ResourceMonitorService.Configuration; using ResourceMonitorService.Models; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; namespace ResourceMonitorService.Services { public interface IGameDetectionService { Task GetCurrentlyRunningGameAsync(); Task> GetAllDetectedGamesAsync(); Task IsGameRunningFullscreenAsync(); Task GetGameFpsAsync(string processName); } public class GameDetectionService : IGameDetectionService { private readonly ILogger _logger; private readonly MonitoringSettings _settings; // Windows API imports for fullscreen detection [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] private static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } public GameDetectionService(ILogger logger, IOptions settings) { _logger = logger; _settings = settings.Value; } public async Task GetCurrentlyRunningGameAsync() { try { return await Task.Run(() => { var processes = Process.GetProcesses(); foreach (var process in processes) { try { if (process.MainModule?.FileName == null) continue; var filePath = process.MainModule.FileName; var gameInfo = DetectGameFromPath(filePath, process); if (gameInfo != null) { gameInfo.IsFullscreen = IsGameRunningFullscreenAsync().Result; gameInfo.FPS = GetGameFpsAsync(process.ProcessName).Result; return gameInfo; } } catch (Exception ex) { // Handle access exceptions silently - some processes can't be accessed _logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName); } } return null; }); } catch (Exception ex) { _logger.LogError(ex, "Error detecting currently running game"); return null; } } public async Task> GetAllDetectedGamesAsync() { try { return await Task.Run(() => { var games = new List(); var processes = Process.GetProcesses(); foreach (var process in processes) { try { if (process.MainModule?.FileName == null) continue; var filePath = process.MainModule.FileName; var gameInfo = DetectGameFromPath(filePath, process); if (gameInfo != null) { games.Add(gameInfo); } } catch (Exception ex) { _logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName); } } return games; }); } catch (Exception ex) { _logger.LogError(ex, "Error getting all detected games"); return new List(); } } public async Task IsGameRunningFullscreenAsync() { try { return await Task.Run(() => { var foregroundWindow = GetForegroundWindow(); if (foregroundWindow == IntPtr.Zero) return false; if (!IsWindowVisible(foregroundWindow)) return false; if (!GetWindowRect(foregroundWindow, out RECT rect)) return false; // Get screen dimensions var primaryScreen = System.Windows.Forms.Screen.PrimaryScreen; if (primaryScreen == null) return false; var screenWidth = primaryScreen.Bounds.Width; var screenHeight = primaryScreen.Bounds.Height; // Check if window covers the entire screen var windowWidth = rect.Right - rect.Left; var windowHeight = rect.Bottom - rect.Top; return windowWidth >= screenWidth && windowHeight >= screenHeight; }); } catch (Exception ex) { _logger.LogWarning(ex, "Could not determine if game is running fullscreen"); return false; } } public async Task GetGameFpsAsync(string processName) { try { return await Task.Run(() => { // This is a simplified FPS detection - in reality, you'd need more sophisticated methods // such as hooking into DirectX/OpenGL or using external tools like RTSS // For now, we'll return 0 as a placeholder // In a real implementation, you might: // 1. Use Windows Performance Toolkit (WPT) ETW events // 2. Hook into D3D11/D3D12 present calls // 3. Use NVIDIA's NVAPI or AMD's ADL // 4. Parse log files from games that output FPS return 0f; }); } catch (Exception ex) { _logger.LogWarning(ex, "Could not get FPS for process {ProcessName}", processName); return 0f; } } private GameInfo? DetectGameFromPath(string filePath, Process process) { try { // First check configured game root folders var gameFromRootFolder = DetectGameFromRootFolders(filePath, process); if (gameFromRootFolder != null) { return gameFromRootFolder; } // Check each configured game platform path foreach (var platformPath in _settings.GamePlatformPaths) { if (filePath.Contains(platformPath, StringComparison.OrdinalIgnoreCase)) { var platform = GetPlatformFromPath(platformPath); var gameName = ExtractGameNameFromPath(filePath, platformPath); return new GameInfo { GameName = gameName, ExecutableName = Path.GetFileName(filePath), FullPath = filePath, ProcessId = process.Id, MemoryUsage = (ulong)process.WorkingSet64, CpuTime = process.TotalProcessorTime, StartTime = process.StartTime, Platform = platform, IsFullscreen = false, // Will be set by caller FPS = 0f // Will be set by caller }; } } // Additional checks for common game launchers and executables var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant(); // Exclude known system processes and applications var systemExclusions = new[] { "officeclicktorun", "winword", "excel", "powerpoint", "outlook", "teams", "skype", "chrome", "firefox", "edge", "explorer", "notepad", "calculator", "cmd", "powershell", "taskmgr", "svchost", "dwm", "csrss", "winlogon", "lsass", "services", "wininit", "audiodg", "conhost", "rundll32", "msiexec", "setup", "installer", "update", "vshost", "devenv" }; // Skip if it's a known system process if (systemExclusions.Any(exclusion => fileName.Contains(exclusion))) { return null; } var knownGameExecutables = new[] { "game", "launcher", "client" // Removed generic terms like "main", "start", "run" that match too many system processes }; var gameIndicators = new[] { "unreal", "unity", "godot", "gamemaker", "rpgmaker", "steam", "epic", "origin", "uplay", "battle.net" }; // Check if it's likely a game based on executable name or path // Made the condition more restrictive to reduce false positives if ((knownGameExecutables.Any(exe => fileName.Equals(exe) || fileName.StartsWith(exe + ".")) || gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase))) && !filePath.Contains("Program Files\\Common Files", StringComparison.OrdinalIgnoreCase)) { // Try to determine platform and game name from other indicators var platform = DeterminePlatformFromProcess(process, filePath); var gameName = DetermineGameNameFromProcess(process, filePath); if (!string.IsNullOrEmpty(gameName)) { return new GameInfo { GameName = gameName, ExecutableName = Path.GetFileName(filePath), FullPath = filePath, ProcessId = process.Id, MemoryUsage = (ulong)process.WorkingSet64, CpuTime = process.TotalProcessorTime, StartTime = process.StartTime, Platform = platform, IsFullscreen = false, FPS = 0f }; } } return null; } catch (Exception ex) { _logger.LogWarning(ex, "Error detecting game from path {FilePath}", filePath); return null; } } private string GetPlatformFromPath(string platformPath) { return platformPath.ToLowerInvariant() switch { var path when path.Contains("steamapps") => "Steam", var path when path.Contains("epic games") => "Epic Games Store", var path when path.Contains("gog galaxy") => "GOG Galaxy", var path when path.Contains("origin games") => "EA Origin", var path when path.Contains("ubisoft game launcher") => "Ubisoft Connect", _ => "Unknown" }; } private string ExtractGameNameFromPath(string filePath, string platformPath) { try { var parts = filePath.Split(new[] { platformPath }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 1) { var gamePath = parts[1]; var gameFolder = gamePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[0]; return gameFolder; } } catch (Exception ex) { _logger.LogWarning(ex, "Could not extract game name from path {FilePath}", filePath); } return Path.GetFileNameWithoutExtension(filePath); } private string DeterminePlatformFromProcess(Process process, string filePath) { try { // Check parent processes for launcher indicators var currentProcess = process; for (int i = 0; i < 3; i++) // Check up to 3 levels up { try { var parentId = GetParentProcessId(currentProcess.Id); if (parentId == 0) break; var parentProcess = Process.GetProcessById(parentId); var parentName = parentProcess.ProcessName.ToLowerInvariant(); if (parentName.Contains("steam")) return "Steam"; if (parentName.Contains("epic")) return "Epic Games Store"; if (parentName.Contains("origin")) return "EA Origin"; if (parentName.Contains("uplay") || parentName.Contains("ubisoft")) return "Ubisoft Connect"; if (parentName.Contains("gog")) return "GOG Galaxy"; currentProcess = parentProcess; } catch { break; } } // Fallback: check file path for platform indicators if (filePath.Contains("Program Files (x86)")) return "Windows Store/Other"; if (filePath.Contains("WindowsApps")) return "Microsoft Store"; return "Standalone"; } catch (Exception ex) { _logger.LogWarning(ex, "Could not determine platform for process {ProcessName}", process.ProcessName); return "Unknown"; } } private string DetermineGameNameFromProcess(Process process, string filePath) { try { // Try to get a meaningful name from various sources // 1. Try from file properties var versionInfo = FileVersionInfo.GetVersionInfo(filePath); if (!string.IsNullOrEmpty(versionInfo.ProductName) && !versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase)) { return versionInfo.ProductName; } // 2. Try from directory name var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory)) { var directoryName = Path.GetFileName(directory); if (!string.IsNullOrEmpty(directoryName) && !directoryName.Equals("bin", StringComparison.OrdinalIgnoreCase) && !directoryName.Equals("exe", StringComparison.OrdinalIgnoreCase)) { return directoryName; } } // 3. Fallback to executable name return Path.GetFileNameWithoutExtension(filePath); } catch (Exception ex) { _logger.LogWarning(ex, "Could not determine game name for process {ProcessName}", process.ProcessName); return process.ProcessName; } } private GameInfo? DetectGameFromRootFolders(string filePath, Process process) { try { foreach (var rootFolder in _settings.GameRootFolders) { if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase)) { var gameName = ExtractGameNameFromRootFolder(filePath, rootFolder); return new GameInfo { GameName = gameName, ExecutableName = Path.GetFileName(filePath), FullPath = filePath, ProcessId = process.Id, MemoryUsage = (ulong)process.WorkingSet64, CpuTime = process.TotalProcessorTime, StartTime = process.StartTime, Platform = "Standalone", // Games in root folders are typically standalone IsFullscreen = false, // Will be set by caller FPS = 0f // Will be set by caller }; } } return null; } catch (Exception ex) { _logger.LogWarning(ex, "Error detecting game from root folders for path {FilePath}", filePath); return null; } } private string ExtractGameNameFromRootFolder(string filePath, string rootFolder) { try { // Remove the root folder from the path to get the relative game path var relativePath = filePath.Substring(rootFolder.Length).TrimStart('\\', '/'); // Split by directory separator and take the first part as the game folder var pathParts = relativePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); if (pathParts.Length > 0) { var gameFolder = pathParts[0]; // If the game folder name is reasonable, use it if (!string.IsNullOrEmpty(gameFolder) && !gameFolder.Equals("bin", StringComparison.OrdinalIgnoreCase) && !gameFolder.Equals("exe", StringComparison.OrdinalIgnoreCase) && !gameFolder.Equals("data", StringComparison.OrdinalIgnoreCase)) { return gameFolder; } } // Fallback: try to get the game name from file properties var versionInfo = FileVersionInfo.GetVersionInfo(filePath); if (!string.IsNullOrEmpty(versionInfo.ProductName) && !versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase)) { return versionInfo.ProductName; } // Last resort: use the executable name return Path.GetFileNameWithoutExtension(filePath); } catch (Exception ex) { _logger.LogWarning(ex, "Could not extract game name from root folder path {FilePath}", filePath); return Path.GetFileNameWithoutExtension(filePath); } } private int GetParentProcessId(int processId) { try { using var searcher = new System.Management.ManagementObjectSearcher( $"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}"); using var collection = searcher.Get(); foreach (System.Management.ManagementObject obj in collection) { return Convert.ToInt32(obj["ParentProcessId"]); } } catch (Exception ex) { _logger.LogTrace(ex, "Could not get parent process ID for {ProcessId}", processId); } return 0; } } }