- .NET 8 WPF app with full MVVM architecture - One-click boost: CPU, RAM, GPU, network, services, timer resolution - Real-time system monitor (LibreHardwareMonitor) - Auto game detection (30+ games) - RAM optimizer with kernel-level cleanup - Network optimizer (TCP registry tweaks) - Service manager, process manager, shader cache cleaner - Desktop widget overlay - Debug log viewer - Custom dark gaming theme with trans pride colors - 84 unit tests (xUnit) - Inno Setup installer script - Landing page website
158 lines
5.3 KiB
C#
158 lines
5.3 KiB
C#
using System.Diagnostics;
|
||
using SpdUp.Core;
|
||
using SpdUp.Models;
|
||
|
||
namespace SpdUp.Services;
|
||
|
||
/// <summary>
|
||
/// Detects running games and known launchers.
|
||
/// </summary>
|
||
public sealed class GameDetectionService : IDisposable
|
||
{
|
||
private readonly Timer _pollTimer;
|
||
private readonly AppSettings _settings;
|
||
private readonly HashSet<string> _knownGameProcesses = new(StringComparer.OrdinalIgnoreCase);
|
||
private string? _detectedGameProcess;
|
||
|
||
public event Action<string>? GameStarted;
|
||
public event Action<string>? GameStopped;
|
||
|
||
/// <summary>
|
||
/// Well-known launcher processes that indicate a game may launch.
|
||
/// </summary>
|
||
private static readonly HashSet<string> LauncherProcesses = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"steam", "steamwebhelper",
|
||
"EpicGamesLauncher",
|
||
"Battle.net", "Agent"
|
||
};
|
||
|
||
/// <summary>
|
||
/// Heuristic: processes that look like games (high-memory GPU-using apps not in system dirs).
|
||
/// </summary>
|
||
private static readonly HashSet<string> 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();
|
||
}
|
||
}
|