using System.Diagnostics; using SpdUp.Core; using SpdUp.Models; namespace SpdUp.Services; /// /// Detects running games and known launchers. /// public sealed class GameDetectionService : IDisposable { private readonly Timer _pollTimer; private readonly AppSettings _settings; private readonly HashSet _knownGameProcesses = new(StringComparer.OrdinalIgnoreCase); private string? _detectedGameProcess; public event Action? GameStarted; public event Action? GameStopped; /// /// Well-known launcher processes that indicate a game may launch. /// private static readonly HashSet LauncherProcesses = new(StringComparer.OrdinalIgnoreCase) { "steam", "steamwebhelper", "EpicGamesLauncher", "Battle.net", "Agent" }; /// /// Heuristic: processes that look like games (high-memory GPU-using apps not in system dirs). /// private static readonly HashSet ExcludedProcessNames = new(StringComparer.OrdinalIgnoreCase) { "explorer", "svchost", "System", "Idle", "csrss", "smss", "lsass", "services", "wininit", "winlogon", "dwm", "taskhostw", "RuntimeBroker", "ShellExperienceHost", "SearchHost", "SearchIndexer", "fontdrvhost", "conhost", "dllhost", "sihost", "ctfmon", "TextInputHost", "SecurityHealthSystray", "SecurityHealthService", "devenv", "Code", "chrome", "msedge", "firefox", "Discord", "Spotify", "slack", "Teams" }; public GameDetectionService(AppSettings settings) { _settings = settings; // Build known game process name set from settings. foreach (var g in _settings.Games) { if (!string.IsNullOrWhiteSpace(g.ProcessName)) _knownGameProcesses.Add(g.ProcessName); } _pollTimer = new Timer(PollForGames, null, Timeout.Infinite, Timeout.Infinite); } public string? CurrentGameProcess => _detectedGameProcess; public void Start(int intervalMs = 3000) { _pollTimer.Change(0, intervalMs); Logger.Info("Game detection started."); } public void Stop() { _pollTimer.Change(Timeout.Infinite, Timeout.Infinite); Logger.Info("Game detection stopped."); } public void AddGameProcess(string processName) { _knownGameProcesses.Add(processName); } private void PollForGames(object? state) { try { var running = Process.GetProcesses(); string? foundGame = null; // First: check known game processes foreach (var proc in running) { try { if (_knownGameProcesses.Contains(proc.ProcessName)) { foundGame = proc.ProcessName; break; } } catch { /* access denied */ } finally { proc.Dispose(); } } // Second: heuristic – detect GPU-heavy, non-system, non-launcher processes if (foundGame is null) { foreach (var proc in Process.GetProcesses()) { try { if (ExcludedProcessNames.Contains(proc.ProcessName)) continue; if (LauncherProcesses.Contains(proc.ProcessName)) continue; if (proc.Id <= 4) continue; // Heuristic: >300 MB working set and has a main window => likely a game if (proc.WorkingSet64 > 300_000_000 && proc.MainWindowHandle != IntPtr.Zero) { // Check it's not from system folders try { var path = proc.MainModule?.FileName; if (path is not null && !path.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.Windows), StringComparison.OrdinalIgnoreCase)) { foundGame = proc.ProcessName; break; } } catch { /* access denied */ } } } catch { } finally { proc.Dispose(); } } } // State machine: detect start / stop if (foundGame is not null && _detectedGameProcess is null) { _detectedGameProcess = foundGame; Logger.Info($"Game detected: {foundGame}"); GameStarted?.Invoke(foundGame); } else if (foundGame is null && _detectedGameProcess is not null) { var prev = _detectedGameProcess; _detectedGameProcess = null; Logger.Info($"Game stopped: {prev}"); GameStopped?.Invoke(prev); } } catch (Exception ex) { Logger.Error("Game detection poll error", ex); } } public void Dispose() { _pollTimer.Dispose(); } }