diff --git a/GenHub/Directory.Packages.props b/GenHub/Directory.Packages.props
index 1be55ad77..09d58be56 100644
--- a/GenHub/Directory.Packages.props
+++ b/GenHub/Directory.Packages.props
@@ -30,6 +30,7 @@
+
diff --git a/GenHub/GenHub.Core/Constants/ActionSetConstants.cs b/GenHub/GenHub.Core/Constants/ActionSetConstants.cs
new file mode 100644
index 000000000..a7c8a9517
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/ActionSetConstants.cs
@@ -0,0 +1,195 @@
+namespace GenHub.Core.Constants;
+
+///
+/// Centralized constants for ActionSet fixes, registry keys, and file operations.
+///
+public static class ActionSetConstants
+{
+ // RegistryKeys moved to GenHub.Core.Constants.RegistryConstants.cs
+
+ ///
+ /// File names and content.
+ ///
+ public static class FileNames
+ {
+ ///
+ /// Gets the desktop.ini file name used for folder customization.
+ ///
+ public const string DesktopIni = "desktop.ini";
+
+ ///
+ /// Gets the Generals.exe file name.
+ ///
+ public const string GeneralsExe = "generals.exe";
+
+ ///
+ /// Gets the Game.dat file name.
+ ///
+ public const string GameDat = "Game.dat";
+
+ ///
+ /// Gets the game.exe file name, often used for Zero Hour.
+ ///
+ public const string GameExe = "game.exe"; // Often used for ZH
+ }
+
+ ///
+ /// Initialization file sections and keys.
+ ///
+ public static class IniFiles
+ {
+ // Sections
+
+ ///
+ /// Gets the [.ShellClassInfo] section name for desktop.ini files.
+ ///
+ public const string ShellClassInfoSection = "[.ShellClassInfo]";
+
+ ///
+ /// Gets the TheSuperHackers section name for Options.ini files.
+ ///
+ public const string TheSuperHackersSection = "TheSuperHackers";
+
+ // Keys
+
+ ///
+ /// Gets the ThisPCPolicy key name used to disable OneDrive sync.
+ ///
+ public const string ThisPCPolicyKey = "ThisPCPolicy";
+
+ ///
+ /// Gets the ThisPCPolicy value to disable OneDrive cloud sync.
+ ///
+ public const string ThisPCPolicyValue = "DisableCloudSync";
+
+ ///
+ /// Gets the ConfirmFileOp key name used in desktop.ini files.
+ ///
+ public const string ConfirmFileOpKey = "ConfirmFileOp";
+
+ // TheSuperHackers keys
+
+ ///
+ /// Gets the ScrollEdgeZone key name for edge scrolling settings.
+ ///
+ public const string ScrollEdgeZoneKey = "ScrollEdgeZone";
+
+ ///
+ /// Gets the ScrollEdgeSpeed key name for edge scrolling settings.
+ ///
+ public const string ScrollEdgeSpeedKey = "ScrollEdgeSpeed";
+
+ ///
+ /// Gets the ScrollEdgeAcceleration key name for edge scrolling settings.
+ ///
+ public const string ScrollEdgeAccelerationKey = "ScrollEdgeAcceleration";
+ }
+
+ ///
+ /// Firewall rule names and protocols.
+ ///
+ public static class FirewallRules
+ {
+ ///
+ /// Gets the prefix used for firewall rule names for GenPatcher compatibility.
+ ///
+ public const string Prefix = "GP"; // Compatibility with GenPatcher
+
+ ///
+ /// Gets the firewall rule name for UDP port 16000.
+ ///
+ public const string PortRuleUdp16000 = "GP Open UDP Port 16000";
+
+ ///
+ /// Gets the firewall rule name for UDP port 16001.
+ ///
+ public const string PortRuleUdp16001 = "GP Open UDP Port 16001";
+
+ ///
+ /// Gets the firewall rule name for TCP port 16001.
+ ///
+ public const string PortRuleTcp16001 = "GP Open TCP Port 16001";
+
+ ///
+ /// Gets the firewall rule name for Generals.exe.
+ ///
+ public const string GeneralsRule = "GP Command & Conquer Generals";
+
+ ///
+ /// Gets the firewall rule name for Generals Game.dat.
+ ///
+ public const string GeneralsGameDatRule = "GP Command & Conquer Generals Game.dat";
+
+ ///
+ /// Gets the firewall rule name for Zero Hour.
+ ///
+ public const string ZeroHourRule = "GP Command & Conquer Generals Zero Hour";
+
+ ///
+ /// Gets the firewall rule name for Zero Hour Game.dat.
+ ///
+ public const string ZeroHourGameDatRule = "GP Command & Conquer Generals Zero Hour Game.dat";
+
+ ///
+ /// Gets the UDP protocol string.
+ ///
+ public const string ProtocolUdp = "UDP";
+
+ ///
+ /// Gets the TCP protocol string.
+ ///
+ public const string ProtocolTcp = "TCP";
+ }
+
+ ///
+ /// Constants for Malwarebytes detection and paths.
+ ///
+ public static class Malwarebytes
+ {
+ ///
+ /// Gets the registry uninstall key path for detecting Malwarebytes.
+ ///
+ public const string RegistryUninstallKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
+
+ ///
+ /// Gets the DisplayName value name in the registry.
+ ///
+ public const string DisplayNameValue = "DisplayName";
+
+ ///
+ /// Gets the string to check for in DisplayName to identify Malwarebytes.
+ ///
+ public const string NameContains = "Malwarebytes";
+
+ ///
+ /// Gets the array of executable paths for Malwarebytes applications.
+ ///
+ public static readonly string[] ExecutablePaths =
+ [
+ Path.Combine("Malwarebytes", "Anti-Malware", "mbam.exe"),
+ Path.Combine("Malwarebytes", "Anti-Malware", "mbamtray.exe")
+ ];
+ }
+
+ ///
+ /// File and directory paths used by ActionSets.
+ ///
+ public static class Paths
+ {
+ ///
+ /// Gets the directory name for sub-action set markers.
+ ///
+ public const string SubActionSetMarkers = "sub_markers";
+ }
+
+ ///
+ /// Validation constants for file operations.
+ ///
+ public static class Validation
+ {
+ ///
+ /// Minimum file size for VCRedist installers (1000 KB).
+ ///
+ public const long VCRedistMinSize = 1000 * 1024;
+ }
+}
diff --git a/GenHub/GenHub.Core/Constants/ExternalUrls.cs b/GenHub/GenHub.Core/Constants/ExternalUrls.cs
new file mode 100644
index 000000000..d74ee37de
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/ExternalUrls.cs
@@ -0,0 +1,85 @@
+namespace GenHub.Core.Constants;
+
+///
+/// Constants for external URLs used for downloading dependencies or tools.
+///
+public static class ExternalUrls
+{
+ ///
+ /// Download URL for Visual C++ 2010 Redistributable Package (x86).
+ /// Required for Generals and Zero Hour to run.
+ ///
+ public const string VCRedist2010DownloadUrl = "https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe";
+
+ ///
+ /// Gets the primary download URL for DirectX runtime (Microsoft Official).
+ ///
+ public const string DirectXRuntimeDownloadUrlPrimary = "https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe";
+
+ ///
+ /// Gets the secondary download URL for DirectX runtime (Gentool).
+ ///
+ public const string DirectXRuntimeDownloadUrlMirror1 = "https://gentool.net/program_data/genpatcher/drtx.dat";
+
+ ///
+ /// Download URL for Generals 1.08 official patch.
+ ///
+ public const string Generals108PatchUrl = "https://gentool.net/program_data/genpatcher/10gn.dat";
+
+ ///
+ /// Gets the primary download URL for Zero Hour 1.04 patch (CNCNZ).
+ ///
+ public const string ZeroHour104PatchUrlPrimary = "http://http.cncnz.com/patches/GeneralsZH-104-english.exe";
+
+ ///
+ /// Gets the secondary download URL for Zero Hour 1.04 patch (Gentool).
+ ///
+ public const string ZeroHour104PatchUrlMirror1 = "https://gentool.net/program_data/genpatcher/10zh.dat";
+
+ ///
+ /// Gets the primary download URL for GenTool (Gentool).
+ ///
+ public const string GenToolDownloadUrlPrimary = "https://gentool.net/program_data/genpatcher/gent.dat";
+
+ ///
+ /// Gets the secondary download URL for GenTool (Legi.cc).
+ ///
+ public const string GenToolDownloadUrlMirror1 = "https://legi.cc/gp2/f/gent.dat";
+
+ ///
+ /// Gets the primary download URL for Visual C++ 2005 Redistributable (Gentool).
+ ///
+ public const string VCRedist2005DownloadUrlPrimary = "https://gentool.net/program_data/genpatcher/vcredist_x86-2005.exe";
+
+ ///
+ /// Gets the secondary download URL for Visual C++ 2005 Redistributable (Legi.cc).
+ ///
+ public const string VCRedist2005DownloadUrlMirror1 = "https://legi.cc/gp2/f/vc05.dat";
+
+ ///
+ /// Gets the primary download URL for Visual C++ 2008 Redistributable (Gentool).
+ ///
+ public const string VCRedist2008DownloadUrlPrimary = "https://gentool.net/program_data/genpatcher/vcredist_x86-2008.exe";
+
+ ///
+ /// Gets the secondary download URL for Visual C++ 2008 Redistributable (Legi.cc).
+ ///
+ public const string VCRedist2008DownloadUrlMirror1 = "https://legi.cc/gp2/f/vc08.dat";
+
+ // Legacy support
+
+ ///
+ /// Legacy download URL for DirectX runtime.
+ ///
+ public const string DirectXRuntimeDownloadUrl = DirectXRuntimeDownloadUrlPrimary;
+
+ ///
+ /// Legacy download URL for Zero Hour 1.04 patch.
+ ///
+ public const string ZeroHour104PatchUrl = ZeroHour104PatchUrlPrimary;
+
+ ///
+ /// Download URL for Intel Graphics Drivers.
+ ///
+ public const string IntelDriverDownloadUrl = "https://www.intel.com/content/www/us/en/download-center/home";
+}
diff --git a/GenHub/GenHub.Core/Constants/GameClientConstants.cs b/GenHub/GenHub.Core/Constants/GameClientConstants.cs
index d2f59ea90..e27631483 100644
--- a/GenHub/GenHub.Core/Constants/GameClientConstants.cs
+++ b/GenHub/GenHub.Core/Constants/GameClientConstants.cs
@@ -127,6 +127,18 @@ public static class GameClientConstants
///
public const string ZeroHourShortName = "Zero Hour";
+ /// BrowserEngine.dll filename.
+ public const string BrowserEngineDll = "BrowserEngine.dll";
+
+ /// BrowserEngine.dll backup filename.
+ public const string BrowserEngineDllBak = "BrowserEngine.dll.bak";
+
+ /// dbghelp.dll filename.
+ public const string DbgHelpDll = "dbghelp.dll";
+
+ /// dbghelp.dll backup filename.
+ public const string DbgHelpDllBak = "dbghelp.dll.bak";
+
///
/// DLLs required for standard game installations.
///
diff --git a/GenHub/GenHub.Core/Constants/GameSettingsConstants.cs b/GenHub/GenHub.Core/Constants/GameSettingsConstants.cs
index e689b8b2a..f1575d7f0 100644
--- a/GenHub/GenHub.Core/Constants/GameSettingsConstants.cs
+++ b/GenHub/GenHub.Core/Constants/GameSettingsConstants.cs
@@ -178,4 +178,173 @@ public static class ResolutionPresets
"7680x4320", // 8K
];
}
+
+ ///
+ /// Optimal settings for game performance and compatibility.
+ ///
+ public static class OptimalSettings
+ {
+ // Video
+
+ ///
+ /// Gets the optimal anti-aliasing value (1 = 2x).
+ ///
+ public const int AntiAliasing = 1;
+
+ ///
+ /// Gets the optimal texture reduction value (0 = no reduction).
+ ///
+ public const int TextureReduction = 0;
+
+ ///
+ /// Gets a value indicating whether extra animations are enabled.
+ ///
+ public const bool ExtraAnimations = true;
+
+ ///
+ /// Gets the optimal gamma correction value (50 = neutral).
+ ///
+ public const int Gamma = 50;
+
+ ///
+ /// Gets a value indicating whether shadow decals are enabled.
+ ///
+ public const bool UseShadowDecals = true;
+
+ ///
+ /// Gets a value indicating whether shadow volumes are enabled.
+ ///
+ public const bool UseShadowVolumes = false;
+
+ ///
+ /// Gets a value indicating whether windowed mode is enabled.
+ ///
+ public const bool Windowed = false;
+
+ ///
+ /// Gets the optimal default resolution width (1920).
+ ///
+ public const int DefaultResolutionWidth = 1920;
+
+ ///
+ /// Gets the optimal default resolution height (1080).
+ ///
+ public const int DefaultResolutionHeight = 1080;
+
+ // Audio
+
+ ///
+ /// Gets the optimal volume level (70), common for SFX, Music, and Voice.
+ ///
+ public const int VolumeLevel = 70; // Common for SFX, Music, Voice
+
+ ///
+ /// Gets a value indicating whether audio is enabled.
+ ///
+ public const bool AudioEnabled = true;
+
+ ///
+ /// Gets the optimal number of sounds (16).
+ ///
+ public const int NumSounds = 16;
+
+ // Network
+
+ ///
+ /// Gets the optimal GameSpy IP address (0.0.0.0 for local).
+ ///
+ public const string GameSpyIPAddress = "0.0.0.0";
+
+ // TheSuperHackers
+
+ ///
+ /// Gets the building occlusion setting ("yes").
+ ///
+ public const string BuildingOcclusion = "yes";
+
+ ///
+ /// Gets the campaign difficulty setting ("0").
+ ///
+ public const string CampaignDifficulty = "0";
+
+ ///
+ /// Gets the dynamic LOD setting ("no").
+ ///
+ public const string DynamicLOD = "no";
+
+ ///
+ /// Gets the firewall port override setting ("16001").
+ ///
+ public const string FirewallPortOverride = "16001";
+
+ ///
+ /// Gets the heat effects setting ("no").
+ ///
+ public const string HeatEffects = "no";
+
+ ///
+ /// Gets the ideal static game LOD setting ("High").
+ ///
+ public const string IdealStaticGameLOD = "High";
+
+ ///
+ /// Gets the language filter setting ("false").
+ ///
+ public const string LanguageFilter = "false";
+
+ ///
+ /// Gets the max particle count setting ("1000").
+ ///
+ public const string MaxParticleCount = "1000";
+
+ ///
+ /// Gets the retaliation setting ("yes").
+ ///
+ public const string Retaliation = "yes";
+
+ ///
+ /// Gets the scroll factor setting ("60").
+ ///
+ public const string ScrollFactor = "60";
+
+ ///
+ /// Gets the send delay setting ("no").
+ ///
+ public const string SendDelay = "no";
+
+ ///
+ /// Gets the show soft water edge setting ("yes").
+ ///
+ public const string ShowSoftWaterEdge = "yes";
+
+ ///
+ /// Gets the show trees setting ("yes").
+ ///
+ public const string ShowTrees = "yes";
+
+ ///
+ /// Gets the static game LOD setting ("Custom").
+ ///
+ public const string StaticGameLOD = "Custom";
+
+ ///
+ /// Gets the use alternate mouse setting ("no").
+ ///
+ public const string UseAlternateMouse = "no";
+
+ ///
+ /// Gets the use cloud map setting ("yes").
+ ///
+ public const string UseCloudMap = "yes";
+
+ ///
+ /// Gets the use double click attack move setting ("no").
+ ///
+ public const string UseDoubleClickAttackMove = "no";
+
+ ///
+ /// Gets the use light map setting ("yes").
+ ///
+ public const string UseLightMap = "yes";
+ }
}
diff --git a/GenHub/GenHub.Core/Constants/RegistryConstants.cs b/GenHub/GenHub.Core/Constants/RegistryConstants.cs
new file mode 100644
index 000000000..153dc434a
--- /dev/null
+++ b/GenHub/GenHub.Core/Constants/RegistryConstants.cs
@@ -0,0 +1,110 @@
+namespace GenHub.Core.Constants;
+
+///
+/// Constants for Windows Registry keys and values.
+///
+public static class RegistryConstants
+{
+ // ===== EA App / Origin Keys =====
+
+ /// Registry key path for Generals command and conquer.
+ public const string EAAppGeneralsKeyPath = @"SOFTWARE\Electronic Arts\EA Games\Generals";
+
+ /// Registry key path for Zero Hour.
+ public const string EAAppZeroHourKeyPath = @"SOFTWARE\Electronic Arts\EA Games\Command and Conquer Generals Zero Hour";
+
+ /// Registry key path for Generals Ergc (Serial).
+ public const string EAAppGeneralsErgcKeyPath = @"SOFTWARE\Electronic Arts\EA Games\Generals\ergc";
+
+ /// Registry key path for Zero Hour Ergc (Serial).
+ public const string EAAppZeroHourErgcKeyPath = @"SOFTWARE\Electronic Arts\EA Games\Command and Conquer Generals Zero Hour\ergc";
+
+ // ===== VCRedist Keys =====
+
+ /// Registry key for VCRedist 2010 x86 (32-bit).
+ public const string VCRedist2010x86Key = @"SOFTWARE\Microsoft\VisualStudio\10.0\VC\VCRedist\x86";
+
+ /// Registry key for VCRedist 2010 x86 (64-bit environment / WOW6432Node).
+ public const string VCRedist2010x86KeyWow64 = @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\10.0\VC\VCRedist\x86";
+
+ // ===== Value Names =====
+
+ /// Registry value name for 'Install Path'.
+ public const string InstallPathValueName = "Install Path";
+
+ /// Registry value name for 'Version'.
+ public const string VersionValueName = "Version";
+
+ /// Registry value name for 'Installed'.
+ public const string InstalledValueName = "Installed";
+
+ // ===== Registry Versions (DWORD) =====
+
+ /// Registry version for Generals 1.08 (0x10008).
+ public const int GeneralsVersionDWord = 0x10008;
+
+ /// Registry version for Zero Hour 1.04 (0x10004).
+ public const int ZeroHourVersionDWord = 0x10004;
+
+ // ===== Windows System Keys =====
+
+ /// Registry key path for Windows Compatibility Flags (AppCompatLayers).
+ public const string AppCompatLayersKeyPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers";
+
+ // ===== The First Decade Keys =====
+
+ /// Registry key path for The First Decade.
+ public const string TheFirstDecadeKeyPath = @"SOFTWARE\EA Games\Command & Conquer The First Decade";
+
+ /// Registry value name for TFD Version.
+ public const string TfdVersionValue = "1.03";
+
+ // ===== C&C Online (Revora) Keys =====
+
+ /// Registry key path for C&C Online (Root).
+ public const string CncOnlineKeyPath = @"SOFTWARE\Revora\CNCOnline";
+
+ /// Registry key path for C&C Online Generals.
+ public const string CncOnlineGeneralsKeyPath = @"SOFTWARE\Revora\CNCOnline\Generals";
+
+ /// Registry key path for C&C Online Zero Hour.
+ public const string CncOnlineZeroHourKeyPath = @"SOFTWARE\Revora\CNCOnline\ZeroHour";
+
+ /// C&C Online Version.
+ public const string CncOnlineVersion = "1.0";
+
+ /// C&C Online Generals Version.
+ public const string CncOnlineGeneralsVersion = "1.08";
+
+ /// C&C Online Zero Hour Version.
+ public const string CncOnlineZeroHourVersion = "1.04";
+
+ // ===== Malwarebytes Keys =====
+
+ /// Registry key path for Uninstall (used for detection).
+ public const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
+
+ /// Registry value name for DisplayName.
+ public const string DisplayNameValueName = "DisplayName";
+
+ // ===== Intel Graphics Keys =====
+
+ /// Registry key path for Intel Graphics Class.
+ public const string IntelGraphicsClassKeyPath = @"SYSTEM\CurrentControlSet\Control\Class\{4D36E968-E325-11CE-BFC1-08002BE10318}";
+
+ /// Registry key path for Intel MEWiz.
+ public const string IntelMEWizKeyPath = @"SOFTWARE\Intel\MEWiz1.0";
+
+ // ===== Windows Media Feature Pack =====
+
+ /// Registry key path for Windows Media Player Feature.
+ public const string WindowsMediaPlayerFeatureKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\WindowsFeatures\WindowsMediaPlayer";
+
+ // ===== Origin Keys =====
+
+ /// Registry key path for Origin.
+ public const string OriginKeyPath = @"SOFTWARE\Origin";
+
+ /// Registry value name for Origin Client Path.
+ public const string OriginClientPathValue = "ClientPath";
+}
diff --git a/GenHub/GenHub.Core/Features/ActionSets/ActionSetOrchestrator.cs b/GenHub/GenHub.Core/Features/ActionSets/ActionSetOrchestrator.cs
new file mode 100644
index 000000000..7b2c5ae24
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/ActionSets/ActionSetOrchestrator.cs
@@ -0,0 +1,158 @@
+namespace GenHub.Core.Features.ActionSets;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Core.Models.Results;
+using Microsoft.Extensions.Logging;
+
+///
+/// Implementation of the ActionSet orchestrator.
+///
+public class ActionSetOrchestrator : IActionSetOrchestrator
+{
+ private readonly IEnumerable _actionSets;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The initial collection of action sets.
+ /// The collection of action set providers.
+ /// The logger instance.
+ public ActionSetOrchestrator(
+ IEnumerable actionSets,
+ IEnumerable providers,
+ ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ var allSets = new List(actionSets ?? []);
+
+ if (providers != null)
+ {
+ foreach (var provider in providers)
+ {
+ try
+ {
+ allSets.AddRange(provider.GetActionSets());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to load action sets from provider {Provider}", provider.GetType().Name);
+ }
+ }
+ }
+
+ _actionSets = allSets;
+ }
+
+ ///
+ public IEnumerable GetAllActionSets() => _actionSets;
+
+ ///
+ public async Task> GetApplicableCoreFixesAsync(GameInstallation installation)
+ {
+ var applicable = new List();
+ foreach (var actionSet in _actionSets.Where(x => x.IsCoreFix))
+ {
+ if (await actionSet.IsApplicableAsync(installation))
+ {
+ applicable.Add(actionSet);
+ }
+ }
+
+ return applicable;
+ }
+
+ ///
+ public async Task> ApplyActionSetsAsync(
+ GameInstallation installation,
+ IEnumerable actionSets,
+ CancellationToken ct = default)
+ {
+ int successCount = 0;
+ var errors = new List();
+ var actionSetsList = actionSets.ToList();
+ int totalCount = actionSetsList.Count;
+
+ _logger.LogInformation("Starting to apply {TotalCount} action sets to {Installation}", totalCount, installation.InstallationPath);
+
+ for (int i = 0; i < actionSetsList.Count; i++)
+ {
+ var actionSet = actionSetsList[i];
+ if (ct.IsCancellationRequested)
+ {
+ _logger.LogWarning("Action set application cancelled by user");
+ break;
+ }
+
+ // Double check applicability and applied state to avoid redundant work
+ if (!await actionSet.IsApplicableAsync(installation))
+ {
+ _logger.LogDebug("Skipping {Title} - not applicable", actionSet.Title);
+ continue;
+ }
+
+ if (await actionSet.IsAppliedAsync(installation))
+ {
+ _logger.LogDebug("Skipping {Title} - already applied", actionSet.Title);
+ continue;
+ }
+
+ _logger.LogInformation("Applying fix {Current}/{Total}: {Title}", i + 1, totalCount, actionSet.Title);
+
+ var result = await actionSet.ApplyAsync(installation, ct);
+ if (result.Success)
+ {
+ successCount++;
+ _logger.LogInformation("✓ Successfully applied {Title} ({Current}/{Total})", actionSet.Title, i + 1, totalCount);
+
+ if (result.Details?.Count > 0)
+ {
+ foreach (var detail in result.Details)
+ {
+ _logger.LogDebug(" {Detail}", detail);
+ }
+ }
+ }
+ else
+ {
+ var errorMsg = $"Failed to apply {actionSet.Title}: {result.ErrorMessage}";
+ errors.Add(errorMsg);
+ _logger.LogWarning("✗ {ErrorMsg}", errorMsg);
+
+ if (result.Details?.Count > 0)
+ {
+ foreach (var detail in result.Details)
+ {
+ _logger.LogDebug(" {Detail}", detail);
+ }
+ }
+
+ if (actionSet.IsCrucialFix)
+ {
+ _logger.LogError("Critical fix {Title} failed for {Installation}. Aborting sequence.", actionSet.Title, installation.InstallationPath);
+ errors.Add($"Critical fix '{actionSet.Title}' failed. Remaining fixes were not applied.");
+ return OperationResult.CreateFailure(errors, successCount);
+ }
+ }
+ }
+
+ _logger.LogInformation(
+ "Action set application completed: {SuccessCount}/{TotalCount} successful, {ErrorCount} errors",
+ successCount,
+ totalCount,
+ errors.Count);
+
+ if (errors.Count > 0)
+ {
+ return OperationResult.CreateFailure(errors, successCount);
+ }
+
+ return OperationResult.CreateSuccess(successCount);
+ }
+}
diff --git a/GenHub/GenHub.Core/Features/ActionSets/BaseActionSet.cs b/GenHub/GenHub.Core/Features/ActionSets/BaseActionSet.cs
new file mode 100644
index 000000000..8f90b1e31
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/ActionSets/BaseActionSet.cs
@@ -0,0 +1,121 @@
+namespace GenHub.Core.Features.ActionSets;
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Abstract base class for action sets, providing common functionality.
+///
+public abstract class BaseActionSet : IActionSet
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ protected BaseActionSet(ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public abstract string Id { get; }
+
+ ///
+ public abstract string Title { get; }
+
+ ///
+ public abstract bool IsCoreFix { get; }
+
+ ///
+ public abstract bool IsCrucialFix { get; }
+
+ ///
+ public abstract Task IsApplicableAsync(GameInstallation installation);
+
+ ///
+ public abstract Task IsAppliedAsync(GameInstallation installation);
+
+ ///
+ public async Task ApplyAsync(GameInstallation installation, CancellationToken ct = default)
+ {
+ _logger.LogInformation("Applying ActionSet {Title} ({Id}) to {InstallationPath}...", Title, Id, installation.InstallationPath);
+ try
+ {
+ var result = await ApplyInternalAsync(installation, ct);
+ if (result.Success)
+ {
+ _logger.LogInformation("Successfully applied ActionSet {Title} ({Id})", Title, Id);
+ }
+ else
+ {
+ _logger.LogWarning("Failed to apply ActionSet {Title} ({Id}): {Error}", Title, Id, result.ErrorMessage);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error applying ActionSet {Title} ({Id})", Title, Id);
+ return new ActionSetResult(false, ex.Message);
+ }
+ }
+
+ ///
+ public async Task UndoAsync(GameInstallation installation, CancellationToken ct = default)
+ {
+ _logger.LogInformation("Undoing ActionSet {Title} ({Id}) from {InstallationPath}...", Title, Id, installation.InstallationPath);
+ try
+ {
+ var result = await UndoInternalAsync(installation, ct);
+ if (result.Success)
+ {
+ _logger.LogInformation("Successfully undid ActionSet {Title} ({Id})", Title, Id);
+ }
+ else
+ {
+ _logger.LogWarning("Failed to undo ActionSet {Title} ({Id}): {Error}", Title, Id, result.ErrorMessage);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error undoing ActionSet {Title} ({Id})", Title, Id);
+ return new ActionSetResult(false, ex.Message);
+ }
+ }
+
+ ///
+ /// Implements the specific application logic.
+ ///
+ /// The game installation.
+ /// The cancellation token.
+ /// The result of the operation.
+ protected abstract Task ApplyInternalAsync(GameInstallation installation, CancellationToken ct);
+
+ ///
+ /// Implements the specific undo logic.
+ ///
+ /// The game installation.
+ /// The cancellation token.
+ /// The result of the operation.
+ protected abstract Task UndoInternalAsync(GameInstallation installation, CancellationToken ct);
+
+ ///
+ /// Helper to return a successful result.
+ ///
+ /// A successful ActionSetResult.
+ protected ActionSetResult Success() => new(true);
+
+ ///
+ /// Helper to return a failed result.
+ ///
+ /// The error message.
+ /// A failed ActionSetResult.
+ protected ActionSetResult Failure(string message) => new(false, message);
+}
diff --git a/GenHub/GenHub.Core/Features/ActionSets/IActionSet.cs b/GenHub/GenHub.Core/Features/ActionSets/IActionSet.cs
new file mode 100644
index 000000000..d37d6fc73
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/ActionSets/IActionSet.cs
@@ -0,0 +1,112 @@
+namespace GenHub.Core.Features.ActionSets;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.GameInstallations;
+
+///
+/// Defines a set of actions to fix or enhance a game installation.
+///
+public interface IActionSet
+{
+ ///
+ /// Gets the unique identifier for this action set.
+ ///
+ string Id { get; }
+
+ ///
+ /// Gets the title of the action set.
+ ///
+ string Title { get; }
+
+ ///
+ /// Gets a value indicating whether this is a core fix applied by default.
+ ///
+ bool IsCoreFix { get; }
+
+ ///
+ /// Gets a value indicating whether this is a crucial fix for game stability.
+ ///
+ bool IsCrucialFix { get; }
+
+ ///
+ /// Checks if the action set is applicable to the current system and game installation.
+ ///
+ /// The game installation to check.
+ /// A task representing the asynchronous operation, returning true if applicable.
+ Task IsApplicableAsync(GameInstallation installation);
+
+ ///
+ /// Checks if the action set has already been applied.
+ ///
+ /// The game installation to check.
+ /// A task representing the asynchronous operation, returning true if applied.
+ Task IsAppliedAsync(GameInstallation installation);
+
+ ///
+ /// Applies the action set patches.
+ ///
+ /// The game installation to patch.
+ /// The cancellation token.
+ /// A task representing the asynchronous operation, returning the result of the action.
+ Task ApplyAsync(GameInstallation installation, CancellationToken ct = default);
+
+ ///
+ /// Undoes the action set patches if possible.
+ ///
+ /// The game installation to revert.
+ /// The cancellation token.
+ /// A task representing the asynchronous operation, returning the result of the undo operation.
+ Task UndoAsync(GameInstallation installation, CancellationToken ct = default);
+}
+
+///
+/// Represents the result of an action set operation.
+///
+/// Whether the operation succeeded.
+/// Error message if the operation failed.
+/// Detailed list of actions taken during the operation.
+public record ActionSetResult(bool Success, string? ErrorMessage = null, List? Details = null)
+{
+ ///
+ /// Gets the details list, creating one if needed.
+ ///
+ public List Details { get; init; } = Details ?? [];
+
+ ///
+ /// Creates a new ActionSetResult with an additional detail message.
+ ///
+ /// The detail message to add.
+ /// A new ActionSetResult with the detail added.
+ public ActionSetResult WithDetail(string detail)
+ {
+ var newDetails = new List(Details) { detail };
+ return this with { Details = newDetails };
+ }
+
+ ///
+ /// Creates a successful result with the given details.
+ ///
+ /// The details of what was done.
+ /// A successful ActionSetResult.
+ public static ActionSetResult SuccessWithDetails(params string[] details) =>
+ new(true, null, [.. details]);
+
+ ///
+ /// Creates a failed result with the given error and optional details.
+ ///
+ /// The error message.
+ /// Optional details of what was attempted.
+ /// A failed ActionSetResult.
+ public static ActionSetResult FailureWithDetails(string error, params string[] details) =>
+ new(false, error, [.. details]);
+
+ ///
+ /// Formats the details as a multi-line string for display.
+ ///
+ /// A formatted string of all details.
+ public string FormatDetails() => Details.Count > 0
+ ? string.Join("\n", Details)
+ : "No details available.";
+}
diff --git a/GenHub/GenHub.Core/Features/ActionSets/IActionSetOrchestrator.cs b/GenHub/GenHub.Core/Features/ActionSets/IActionSetOrchestrator.cs
new file mode 100644
index 000000000..51de34264
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/ActionSets/IActionSetOrchestrator.cs
@@ -0,0 +1,35 @@
+namespace GenHub.Core.Features.ActionSets;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Core.Models.Results;
+
+///
+/// Service responsible for managing and executing action sets.
+///
+public interface IActionSetOrchestrator
+{
+ ///
+ /// Gets all registered action sets.
+ ///
+ /// A list of action sets.
+ IEnumerable GetAllActionSets();
+
+ ///
+ /// Gets applicable core fixes for a given installation.
+ ///
+ /// The game installation.
+ /// A task returning the list of applicable core fixes.
+ Task> GetApplicableCoreFixesAsync(GameInstallation installation);
+
+ ///
+ /// Applies a collection of action sets to an installation.
+ ///
+ /// The installation.
+ /// The action sets to apply.
+ /// Cancellation token.
+ /// Operation result containing details of success/failure.
+ Task> ApplyActionSetsAsync(GameInstallation installation, IEnumerable actionSets, CancellationToken ct = default);
+}
diff --git a/GenHub/GenHub.Core/Features/ActionSets/IActionSetProvider.cs b/GenHub/GenHub.Core/Features/ActionSets/IActionSetProvider.cs
new file mode 100644
index 000000000..302bd9b5c
--- /dev/null
+++ b/GenHub/GenHub.Core/Features/ActionSets/IActionSetProvider.cs
@@ -0,0 +1,15 @@
+namespace GenHub.Core.Features.ActionSets;
+
+using System.Collections.Generic;
+
+///
+/// Defines a provider for discovering ActionSets.
+///
+public interface IActionSetProvider
+{
+ ///
+ /// Gets the action sets provided by this source.
+ ///
+ /// A collection of action sets.
+ IEnumerable GetActionSets();
+}
diff --git a/GenHub/GenHub.Core/Interfaces/Shortcuts/IShortcutService.cs b/GenHub/GenHub.Core/Interfaces/Shortcuts/IShortcutService.cs
index f9dce3aba..a7d7aa327 100644
--- a/GenHub/GenHub.Core/Interfaces/Shortcuts/IShortcutService.cs
+++ b/GenHub/GenHub.Core/Interfaces/Shortcuts/IShortcutService.cs
@@ -37,4 +37,22 @@ public interface IShortcutService
/// Optional custom name for the shortcut. If null, uses the profile name.
/// The full path to the shortcut file.
string GetShortcutPath(GameProfile profile, string? shortcutName = null);
+
+ ///
+ /// Creates a shortcut at the specified path.
+ ///
+ /// The path where the shortcut will be created.
+ /// The path to the target executable.
+ /// Optional command line arguments.
+ /// Optional working directory.
+ /// Optional description.
+ /// Optional icon path.
+ /// An operation result indicating success or failure.
+ Task> CreateShortcutAsync(
+ string shortcutPath,
+ string targetPath,
+ string? arguments = null,
+ string? workingDirectory = null,
+ string? description = null,
+ string? iconPath = null);
}
diff --git a/GenHub/GenHub.Core/Interfaces/Tools/IToolRegistry.cs b/GenHub/GenHub.Core/Interfaces/Tools/IToolRegistry.cs
index 5bf1804e7..50dc9298a 100644
--- a/GenHub/GenHub.Core/Interfaces/Tools/IToolRegistry.cs
+++ b/GenHub/GenHub.Core/Interfaces/Tools/IToolRegistry.cs
@@ -22,8 +22,8 @@ public interface IToolRegistry
/// Registers a new tool plugin with an assembly path (external tool).
///
/// The tool plugin to register.
- /// The path to the tool assembly.
- void RegisterTool(IToolPlugin plugin, string assemblyPath);
+ /// The path to the tool assembly. Null for built-in tools.
+ void RegisterTool(IToolPlugin plugin, string? assemblyPath = null);
///
/// Registers a new built-in tool plugin.
diff --git a/GenHub/GenHub.Core/Messages/ToolStatusMessage.cs b/GenHub/GenHub.Core/Messages/ToolStatusMessage.cs
new file mode 100644
index 000000000..29f3f9fe9
--- /dev/null
+++ b/GenHub/GenHub.Core/Messages/ToolStatusMessage.cs
@@ -0,0 +1,26 @@
+namespace GenHub.Core.Messages;
+
+///
+/// Defines the type of tool status message.
+///
+public enum MessageType
+{
+ /// Informational message.
+ Info,
+
+ /// Success message.
+ Success,
+
+ /// Error message.
+ Error,
+
+ /// Warning message.
+ Warning,
+}
+
+///
+/// Message sent when a tool's status changes.
+///
+/// The status message.
+/// The type of message.
+public record ToolStatusMessage(string Message, MessageType Type = MessageType.Info);
diff --git a/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherCatalog.cs b/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherCatalog.cs
index 3e0eca4f6..743b66944 100644
--- a/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherCatalog.cs
+++ b/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherCatalog.cs
@@ -15,5 +15,5 @@ public class GenPatcherCatalog
///
/// Gets or sets the list of content items.
///
- public List Items { get; set; } = new();
+ public List Items { get; set; } = [];
}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherContentRegistry.cs b/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherContentRegistry.cs
index af2a42d42..b4cfdf90e 100644
--- a/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherContentRegistry.cs
+++ b/GenHub/GenHub.Core/Models/CommunityOutpost/GenPatcherContentRegistry.cs
@@ -73,8 +73,8 @@ public static class GenPatcherContentRegistry
["cbbs"] = new GenPatcherContentMetadata
{
ContentCode = "cbbs",
- DisplayName = "Control Bar - Basic",
- Description = "Basic control bar addon",
+ DisplayName = "Control Bar HD (Base)",
+ Description = "Base files for Control Bar HD",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.ControlBar,
@@ -83,8 +83,8 @@ public static class GenPatcherContentRegistry
["cben"] = new GenPatcherContentMetadata
{
ContentCode = "cben",
- DisplayName = "Control Bar - Enhanced",
- Description = "Enhanced control bar with additional features",
+ DisplayName = "Control Bar HD (English)",
+ Description = "English language pack for Control Bar HD",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.ControlBar,
@@ -93,8 +93,8 @@ public static class GenPatcherContentRegistry
["cbpc"] = new GenPatcherContentMetadata
{
ContentCode = "cbpc",
- DisplayName = "Control Bar - PC Style",
- Description = "PC-style control bar layout",
+ DisplayName = "Control Bar Pro (Common)",
+ Description = "Common files for Control Bar Pro (ExiLe/Xezon)",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.ControlBar,
@@ -103,8 +103,8 @@ public static class GenPatcherContentRegistry
["cbpr"] = new GenPatcherContentMetadata
{
ContentCode = "cbpr",
- DisplayName = "Control Bar - Pro",
- Description = "Professional control bar addon",
+ DisplayName = "Control Bar Pro (ExiLe)",
+ Description = "Base files for Control Bar Pro (ExiLe)",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.ControlBar,
@@ -113,8 +113,8 @@ public static class GenPatcherContentRegistry
["cbpx"] = new GenPatcherContentMetadata
{
ContentCode = "cbpx",
- DisplayName = "Control Bar - Extended",
- Description = "Extended control bar with extra functionality",
+ DisplayName = "Control Bar Pro (Xezon)",
+ Description = "Base files for Control Bar Pro (Xezon)",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.ControlBar,
@@ -153,32 +153,32 @@ public static class GenPatcherContentRegistry
InstallTarget = ContentInstallTarget.Workspace,
},
- // Hotkeys
+ // Hotkeys & WorldBuilder
["ewba"] = new GenPatcherContentMetadata
{
ContentCode = "ewba",
- DisplayName = "Easy Win Hotkeys - Advanced",
- Description = "Advanced hotkey configuration",
+ DisplayName = "Enhanced World Builder v2.7 by Adriane",
+ Description = "Advanced World Builder with new theme",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Hotkeys,
+ Category = GenPatcherContentCategory.Tools,
InstallTarget = ContentInstallTarget.Workspace,
},
["ewbi"] = new GenPatcherContentMetadata
{
ContentCode = "ewbi",
- DisplayName = "Easy Win Hotkeys - International",
- Description = "International hotkey layout",
+ DisplayName = "Enhanced World Builder v2.2",
+ Description = "Standard Enhanced World Builder",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Hotkeys,
+ Category = GenPatcherContentCategory.Tools,
InstallTarget = ContentInstallTarget.Workspace,
},
["hlde"] = new GenPatcherContentMetadata
{
ContentCode = "hlde",
- DisplayName = "Hotkeys - German",
- Description = "German hotkey configuration",
+ DisplayName = "Leikeze's Hotkeys Indicators (German)",
+ Description = "German version of Leikeze's indicators",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
LanguageCode = "de",
@@ -188,8 +188,8 @@ public static class GenPatcherContentRegistry
["hleg"] = new GenPatcherContentMetadata
{
ContentCode = "hleg",
- DisplayName = "Hotkeys - English (Grid)",
- Description = "English grid-based hotkey layout",
+ DisplayName = "Legionnaire's Hotkeys",
+ Description = "Legionnaire's custom hotkey layout",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
LanguageCode = "en",
@@ -199,8 +199,8 @@ public static class GenPatcherContentRegistry
["hlei"] = new GenPatcherContentMetadata
{
ContentCode = "hlei",
- DisplayName = "Hotkeys - English (Icons)",
- Description = "English icon-based hotkey layout",
+ DisplayName = "Leikeze's Hotkeys (Icons)",
+ Description = "Leikeze's hotkeys with icon support",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
LanguageCode = "en",
@@ -210,8 +210,8 @@ public static class GenPatcherContentRegistry
["hlen"] = new GenPatcherContentMetadata
{
ContentCode = "hlen",
- DisplayName = "Hotkeys - English",
- Description = "Standard English hotkey configuration",
+ DisplayName = "Leikeze's Hotkeys Indicators",
+ Description = "Leikeze's hotkey indicators overlay",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
LanguageCode = "en",
@@ -230,43 +230,65 @@ public static class GenPatcherContentRegistry
Category = GenPatcherContentCategory.Tools,
InstallTarget = ContentInstallTarget.Workspace,
},
+ ["gena"] = new GenPatcherContentMetadata
+ {
+ ContentCode = "gena",
+ DisplayName = "GenAssist",
+ Description = "GenAssist helper utility",
+ ContentType = ContentType.Addon,
+ TargetGame = GameType.ZeroHour,
+ Category = GenPatcherContentCategory.Tools,
+ InstallTarget = ContentInstallTarget.Workspace,
+ },
["genl"] = new GenPatcherContentMetadata
{
ContentCode = "genl",
DisplayName = "GenLauncher",
- Description = "Alternative launcher for Generals/Zero Hour",
+ Description = "GenLauncher standalone launcher utility",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.Tools,
InstallTarget = ContentInstallTarget.Workspace,
},
- ["gena"] = new GenPatcherContentMetadata
+
+ // Prerequisites - System dependencies (VCRedist, DirectX, etc.)
+ ["vc05"] = new GenPatcherContentMetadata
{
- ContentCode = "gena",
- DisplayName = "GenAssist",
- Description = "GenAssist helper utility",
+ ContentCode = "vc05",
+ DisplayName = "Visual C++ 2005 Redistributable",
+ Description = "Microsoft Visual C++ 2005 Redistributable Package",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Tools,
- InstallTarget = ContentInstallTarget.Workspace,
+ Category = GenPatcherContentCategory.Prerequisites,
+ InstallTarget = ContentInstallTarget.System,
},
- ["laun"] = new GenPatcherContentMetadata
+ ["vc08"] = new GenPatcherContentMetadata
{
- ContentCode = "laun",
- DisplayName = "Launcher",
- Description = "Game launcher component",
+ ContentCode = "vc08",
+ DisplayName = "Visual C++ 2008 Redistributable",
+ Description = "Microsoft Visual C++ 2008 Redistributable Package",
ContentType = ContentType.Addon,
TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Tools,
- InstallTarget = ContentInstallTarget.Workspace,
+ Category = GenPatcherContentCategory.Prerequisites,
+ InstallTarget = ContentInstallTarget.System,
+ },
+ ["vc10"] = new GenPatcherContentMetadata
+ {
+ ContentCode = "vc10",
+ DisplayName = "Visual C++ 2010 Redistributable",
+ Description = "Microsoft Visual C++ 2010 Redistributable Package",
+ ContentType = ContentType.Addon,
+ TargetGame = GameType.ZeroHour,
+ Category = GenPatcherContentCategory.Prerequisites,
+ InstallTarget = ContentInstallTarget.System,
},
// Maps and Missions - These go to user Documents directory
["maod"] = new GenPatcherContentMetadata
{
ContentCode = "maod",
- DisplayName = "Map Addon",
- Description = "Additional maps addon pack",
+ DisplayName = "AOD and comp-stomp maps",
+ Description = "Maps designed for AOD/Comp-Stomp",
ContentType = ContentType.MapPack,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.Maps,
@@ -275,8 +297,8 @@ public static class GenPatcherContentRegistry
["mmis"] = new GenPatcherContentMetadata
{
ContentCode = "mmis",
- DisplayName = "Missions Pack",
- Description = "Custom missions pack",
+ DisplayName = "Single player and multiplayer co-op missions",
+ Description = "Custom missions campaign",
ContentType = ContentType.Mission,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.Maps,
@@ -285,8 +307,8 @@ public static class GenPatcherContentRegistry
["mscr"] = new GenPatcherContentMetadata
{
ContentCode = "mscr",
- DisplayName = "Map Scripts",
- Description = "Map scripting resources",
+ DisplayName = "Scripted and no-money maps",
+ Description = "Maps with heavy scripting or no-money rules",
ContentType = ContentType.MapPack,
TargetGame = GameType.ZeroHour,
Category = GenPatcherContentCategory.Maps,
@@ -295,8 +317,8 @@ public static class GenPatcherContentRegistry
["mskr"] = new GenPatcherContentMetadata
{
ContentCode = "mskr",
- DisplayName = "Map Pack - Korean",
- Description = "Korean map pack",
+ DisplayName = "Skirmish and multiplayer maps",
+ Description = "Collection of skirmish and MP maps",
ContentType = ContentType.MapPack,
TargetGame = GameType.ZeroHour,
LanguageCode = "ko",
@@ -335,38 +357,6 @@ public static class GenPatcherContentRegistry
Category = GenPatcherContentCategory.Visuals,
InstallTarget = ContentInstallTarget.Workspace,
},
-
- // Prerequisites - System install
- ["vc05"] = new GenPatcherContentMetadata
- {
- ContentCode = "vc05",
- DisplayName = "VC++ 2005 Redistributable",
- Description = "Microsoft Visual C++ 2005 Redistributable (x86)",
- ContentType = ContentType.Addon,
- TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Prerequisites,
- InstallTarget = ContentInstallTarget.System,
- },
- ["vc08"] = new GenPatcherContentMetadata
- {
- ContentCode = "vc08",
- DisplayName = "VC++ 2008 Redistributable",
- Description = "Microsoft Visual C++ 2008 Redistributable (x86)",
- ContentType = ContentType.Addon,
- TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Prerequisites,
- InstallTarget = ContentInstallTarget.System,
- },
- ["vc10"] = new GenPatcherContentMetadata
- {
- ContentCode = "vc10",
- DisplayName = "VC++ 2010 Redistributable",
- Description = "Microsoft Visual C++ 2010 Redistributable (x86)",
- ContentType = ContentType.Addon,
- TargetGame = GameType.ZeroHour,
- Category = GenPatcherContentCategory.Prerequisites,
- InstallTarget = ContentInstallTarget.System,
- },
};
///
@@ -451,7 +441,6 @@ public static bool IsKnownCode(string contentCode)
// Determine target game based on version
// 108 = Generals 1.08, 104 = Zero Hour 1.04
var isGenerals = versionNumber == 8; // 1.08 is Generals
- var isZeroHour = versionNumber == 4; // 1.04 is Zero Hour
var targetGame = isGenerals ? GameType.Generals : GameType.ZeroHour;
var version = $"1.0{versionNumber}";
diff --git a/GenHub/GenHub.Core/Models/Notifications/NotificationAction.cs b/GenHub/GenHub.Core/Models/Notifications/NotificationAction.cs
index 3437c0f91..43c917c1a 100644
--- a/GenHub/GenHub.Core/Models/Notifications/NotificationAction.cs
+++ b/GenHub/GenHub.Core/Models/Notifications/NotificationAction.cs
@@ -5,46 +5,31 @@ namespace GenHub.Core.Models.Notifications;
///
/// Represents a single action button on a notification.
///
-public class NotificationAction
+public class NotificationAction(
+ string text,
+ Action callback,
+ NotificationActionStyle style = NotificationActionStyle.Primary,
+ bool dismissOnExecute = true)
{
///
/// Gets the text to display on the action button.
///
- public string Text { get; init; }
+ public string Text { get; init; } = text ?? throw new ArgumentNullException(nameof(text));
///
/// Gets the callback to execute when the action button is clicked.
///
- public Action? Callback { get; private set; }
+ public Action? Callback { get; private set; } = callback ?? throw new ArgumentNullException(nameof(callback));
///
/// Gets the style of the action button.
///
- public NotificationActionStyle Style { get; init; }
+ public NotificationActionStyle Style { get; init; } = style;
///
/// Gets a value indicating whether the notification should be dismissed after executing this action.
///
- public bool DismissOnExecute { get; init; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The text to display on the action button.
- /// The callback to execute when the action button is clicked.
- /// The style of the action button.
- /// Whether the notification should be dismissed after executing this action.
- public NotificationAction(
- string text,
- Action callback,
- NotificationActionStyle style = NotificationActionStyle.Primary,
- bool dismissOnExecute = true)
- {
- Text = text ?? throw new ArgumentNullException(nameof(text));
- Callback = callback ?? throw new ArgumentNullException(nameof(callback));
- Style = style;
- DismissOnExecute = dismissOnExecute;
- }
+ public bool DismissOnExecute { get; init; } = dismissOnExecute;
///
/// Clears the callback to prevent memory leaks.
@@ -53,4 +38,4 @@ public void ClearCallback()
{
Callback = null;
}
-}
\ No newline at end of file
+}
diff --git a/GenHub/GenHub.Core/Models/Notifications/NotificationMessage.cs b/GenHub/GenHub.Core/Models/Notifications/NotificationMessage.cs
index 85fc9d236..8bbf3b61e 100644
--- a/GenHub/GenHub.Core/Models/Notifications/NotificationMessage.cs
+++ b/GenHub/GenHub.Core/Models/Notifications/NotificationMessage.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using GenHub.Core.Models.Enums;
namespace GenHub.Core.Models.Notifications;
@@ -10,7 +12,7 @@ public record NotificationMessage
///
/// Gets the unique identifier for this notification.
///
- public Guid Id { get; init; }
+ public Guid Id { get; init; } = Guid.NewGuid();
///
/// Gets the type of notification.
@@ -30,7 +32,7 @@ public record NotificationMessage
///
/// Gets the timestamp when the notification was created.
///
- public DateTime Timestamp { get; init; }
+ public DateTime Timestamp { get; init; } = DateTime.UtcNow;
///
/// Gets the auto-dismiss timeout in milliseconds. Null means no auto-dismiss.
@@ -97,7 +99,7 @@ public NotificationMessage(
NotificationType type,
string title,
string message,
- int? autoDismissMilliseconds = 5000,
+ int? autoDismissMilliseconds = GenHub.Core.Constants.NotificationDurations.Medium,
string? actionText = null,
Action? action = null,
IReadOnlyList? actions = null,
diff --git a/GenHub/GenHub.Core/Models/Results/OperationResultOfT.cs b/GenHub/GenHub.Core/Models/Results/OperationResultOfT.cs
index e936cb58d..f9be2fd32 100644
--- a/GenHub/GenHub.Core/Models/Results/OperationResultOfT.cs
+++ b/GenHub/GenHub.Core/Models/Results/OperationResultOfT.cs
@@ -53,11 +53,21 @@ public static OperationResult CreateFailure(string error, TimeSpan elapsed =
/// The elapsed time.
/// A failed .
public static OperationResult CreateFailure(IEnumerable errors, TimeSpan elapsed = default)
+ {
+ return CreateFailure(errors, default, elapsed);
+ }
+
+ /// Creates a failed operation result with multiple error messages and partial data.
+ /// The error messages.
+ /// The partial data.
+ /// The elapsed time.
+ /// A failed .
+ public static OperationResult CreateFailure(IEnumerable errors, T? data, TimeSpan elapsed = default)
{
ArgumentNullException.ThrowIfNull(errors, nameof(errors));
if (!errors.Any())
throw new ArgumentException("Errors collection cannot be empty.", nameof(errors));
- return new OperationResult(false, default, errors, elapsed);
+ return new OperationResult(false, data, errors, elapsed);
}
/// Creates a failed operation result from another result, copying its errors.
diff --git a/GenHub/GenHub.Core/Services/Tools/ToolRegistry.cs b/GenHub/GenHub.Core/Services/Tools/ToolRegistry.cs
index 180c828bc..bbf437312 100644
--- a/GenHub/GenHub.Core/Services/Tools/ToolRegistry.cs
+++ b/GenHub/GenHub.Core/Services/Tools/ToolRegistry.cs
@@ -32,10 +32,13 @@ public IReadOnlyList GetAllTools()
}
///
- public void RegisterTool(IToolPlugin plugin, string assemblyPath)
+ public void RegisterTool(IToolPlugin plugin, string? assemblyPath = null)
{
_tools[plugin.Metadata.Id] = plugin;
- _toolAssemblyPaths[plugin.Metadata.Id] = assemblyPath;
+ if (assemblyPath != null)
+ {
+ _toolAssemblyPaths[plugin.Metadata.Id] = assemblyPath;
+ }
}
///
diff --git a/GenHub/GenHub.Core/Services/Tools/ToolService.cs b/GenHub/GenHub.Core/Services/Tools/ToolService.cs
index 7b4dbfa0c..5711cc43c 100644
--- a/GenHub/GenHub.Core/Services/Tools/ToolService.cs
+++ b/GenHub/GenHub.Core/Services/Tools/ToolService.cs
@@ -14,13 +14,13 @@ namespace GenHub.Core.Services.Tools;
/// Plugin loader for loading tool plugins.
/// Registry for managing tool plugins.
/// Service for managing user settings.
-/// Collection of built-in tool plugins.
+/// Collection of built-in tool plugins from DI.
/// Logger for logging tool service activities.
public class ToolService(
IToolPluginLoader pluginLoader,
IToolRegistry toolRegistry,
IUserSettingsService userSettingsService,
- IEnumerable builtInPlugins,
+ IEnumerable builtInTools,
ILogger logger)
: IToolManager
{
@@ -34,20 +34,20 @@ public async Task> AddToolAsync(string assemblyPath
if (!pluginLoader.ValidatePlugin(assemblyPath))
{
logger.LogWarning("Tool plugin validation failed for: {AssemblyPath}", assemblyPath);
- return await Task.FromResult(OperationResult.CreateFailure("Invalid tool plugin assembly."));
+ return OperationResult.CreateFailure("Invalid tool plugin assembly.");
}
var plugin = pluginLoader.LoadPluginFromAssembly(assemblyPath);
if (plugin == null)
{
logger.LogWarning("Failed to load tool plugin from assembly: {AssemblyPath}", assemblyPath);
- return await Task.FromResult(OperationResult.CreateFailure("Failed to load tool plugin from assembly."));
+ return OperationResult.CreateFailure("Failed to load tool plugin from assembly.");
}
if (toolRegistry.GetToolById(plugin.Metadata.Id) != null)
{
logger.LogWarning("Tool with ID {ToolId} is already registered", plugin.Metadata.Id);
- return await Task.FromResult(OperationResult.CreateFailure("A tool with the same ID is already registered."));
+ return OperationResult.CreateFailure("A tool with the same ID is already registered.");
}
toolRegistry.RegisterTool(plugin, assemblyPath);
@@ -76,7 +76,7 @@ public async Task> AddToolAsync(string assemblyPath
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while adding tool plugin from assembly: {AssemblyPath}", assemblyPath);
- return await Task.FromResult(OperationResult.CreateFailure("An error occurred while adding the tool plugin."));
+ return OperationResult.CreateFailure("An error occurred while adding the tool plugin.");
}
}
@@ -87,31 +87,31 @@ public IReadOnlyList GetAllTools()
}
///
- public async Task>> LoadSavedToolsAsync()
+ public Task>> LoadSavedToolsAsync()
{
try
{
var loadedPlugins = new List();
- // First, register all built-in plugins from DI
- foreach (var builtInPlugin in builtInPlugins)
+ // 1. Register all built-in plugins from DI
+ foreach (var builtIn in builtInTools)
{
- var existingTool = toolRegistry.GetToolById(builtInPlugin.Metadata.Id);
+ var existingTool = toolRegistry.GetToolById(builtIn.Metadata.Id);
if (existingTool == null)
{
- builtInPlugin.Metadata.IsBundled = true;
- toolRegistry.RegisterTool(builtInPlugin);
- loadedPlugins.Add(builtInPlugin);
- logger.LogDebug("Registered built-in tool plugin: {PluginName}", builtInPlugin.Metadata.Name);
+ builtIn.Metadata.IsBundled = true;
+ toolRegistry.RegisterTool(builtIn);
+ loadedPlugins.Add(builtIn);
+ logger.LogDebug("Registered built-in tool plugin: {PluginName}", builtIn.Metadata.Name);
}
else
{
loadedPlugins.Add(existingTool);
- logger.LogDebug("Built-in tool plugin {PluginName} already registered", builtInPlugin.Metadata.Name);
+ logger.LogDebug("Built-in tool plugin {PluginName} already registered", builtIn.Metadata.Name);
}
}
- // Then, load external plugins from saved paths
+ // 2. Load external plugins from saved paths
var settings = userSettingsService.Get();
var toolPaths = settings.InstalledToolAssemblyPaths ?? [];
@@ -126,14 +126,18 @@ public async Task>> LoadSavedToolsAsync()
{
logger.LogDebug("Processing tool path: {Path}", path);
- // Check if tool is already loaded in registry
+ // Check if tool is already loaded in registry by its path
var existingTools = toolRegistry.GetAllTools();
var existingTool = existingTools.FirstOrDefault(t => toolRegistry.GetToolAssemblyPath(t.Metadata.Id) == path);
if (existingTool != null)
{
// Tool already loaded, reuse it
- loadedPlugins.Add(existingTool);
+ if (!loadedPlugins.Contains(existingTool))
+ {
+ loadedPlugins.Add(existingTool);
+ }
+
logger.LogDebug("Tool plugin from {Path} already loaded, reusing existing instance.", path);
continue;
}
@@ -155,14 +159,15 @@ public async Task>> LoadSavedToolsAsync()
logger.LogInformation(
"Loaded {Count} tool plugins ({BuiltIn} built-in, {External} external).",
loadedPlugins.Count,
- builtInPlugins.Count(),
+ builtInTools.Count(),
toolPaths.Count);
- return await Task.FromResult(OperationResult>.CreateSuccess(loadedPlugins));
+
+ return Task.FromResult(OperationResult>.CreateSuccess(loadedPlugins));
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while loading saved tool plugins.");
- return await Task.FromResult(OperationResult>.CreateFailure("An error occurred while loading saved tool plugins."));
+ return Task.FromResult(OperationResult>.CreateFailure("An error occurred while loading saved tool plugins."));
}
}
@@ -174,24 +179,24 @@ public async Task> RemoveToolAsync(string toolId)
var tool = toolRegistry.GetToolById(toolId);
if (tool == null)
{
- return await Task.FromResult(OperationResult.CreateFailure("Tool not found."));
+ return OperationResult.CreateFailure("Tool not found.");
}
if (tool.Metadata.IsBundled)
{
logger.LogWarning("Attempted to remove bundled tool: {ToolName} ({ToolId})", tool.Metadata.Name, toolId);
- return await Task.FromResult(OperationResult.CreateFailure("Bundled tools cannot be removed."));
+ return OperationResult.CreateFailure("Bundled tools cannot be removed.");
}
var assemblyPath = toolRegistry.GetToolAssemblyPath(toolId);
if (assemblyPath == null)
{
- return await Task.FromResult(OperationResult.CreateFailure("Tool registration is incomplete (missing assembly path)."));
+ return OperationResult.CreateFailure("Tool registration is incomplete (missing assembly path).");
}
if (!toolRegistry.UnregisterTool(toolId))
{
- return await Task.FromResult(OperationResult.CreateFailure("Failed to unregister tool."));
+ return OperationResult.CreateFailure("Failed to unregister tool.");
}
userSettingsService.Update(settings =>
@@ -204,10 +209,10 @@ public async Task> RemoveToolAsync(string toolId)
logger.LogInformation("Tool with ID {ToolId} removed successfully.", toolId);
return OperationResult.CreateSuccess(true);
}
- catch
+ catch (Exception ex)
{
- logger.LogError("An error occurred while removing tool with ID: {ToolId}", toolId);
+ logger.LogError(ex, "An error occurred while removing tool with ID: {ToolId}", toolId);
return OperationResult.CreateFailure("An error occurred while removing the tool.");
}
}
-}
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Linux/Features/Shortcuts/LinuxShortcutService.cs b/GenHub/GenHub.Linux/Features/Shortcuts/LinuxShortcutService.cs
index 5c357c93b..d865f0f3e 100644
--- a/GenHub/GenHub.Linux/Features/Shortcuts/LinuxShortcutService.cs
+++ b/GenHub/GenHub.Linux/Features/Shortcuts/LinuxShortcutService.cs
@@ -120,6 +120,51 @@ public Task ShortcutExistsAsync(GameProfile profile)
return Task.FromResult(File.Exists(shortcutPath));
}
+ ///
+ public Task> CreateShortcutAsync(
+ string shortcutPath,
+ string targetPath,
+ string? arguments = null,
+ string? workingDirectory = null,
+ string? description = null,
+ string? iconPath = null)
+ {
+ try
+ {
+ var directory = Path.GetDirectoryName(shortcutPath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var name = Path.GetFileNameWithoutExtension(shortcutPath);
+ var comment = description ?? string.Empty;
+
+ var desktopEntry = BuildDesktopEntry(
+ name,
+ comment,
+ targetPath,
+ arguments ?? string.Empty,
+ workingDirectory ?? string.Empty,
+ iconPath ?? string.Empty);
+
+ File.WriteAllText(shortcutPath, desktopEntry, Encoding.UTF8);
+ MakeExecutable(shortcutPath);
+
+ logger.LogInformation(
+ "Created shortcut at {ShortcutPath} targeting {TargetPath}",
+ shortcutPath,
+ targetPath);
+
+ return Task.FromResult(OperationResult.CreateSuccess(true));
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to create shortcut at {ShortcutPath}", shortcutPath);
+ return Task.FromResult(OperationResult.CreateFailure($"Failed to create shortcut: {ex.Message}"));
+ }
+ }
+
///
public string GetShortcutPath(GameProfile profile, string? shortcutName = null)
{
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Converters/IsSubscribedConverterTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Converters/IsSubscribedConverterTests.cs
index 477a3fd86..9a1380299 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Converters/IsSubscribedConverterTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Converters/IsSubscribedConverterTests.cs
@@ -124,7 +124,7 @@ public void Convert_ReturnsFalse_WhenValuesCountTooLow()
public void ConvertBack_ReturnsEmptyArray()
{
// Act
- var result = IsSubscribedConverter.ConvertBack(true, [typeof(object), typeof(object), typeof(object)], null, CultureInfo.InvariantCulture);
+ var result = _converter.ConvertBack(true, [typeof(object), typeof(object), typeof(object)], null, CultureInfo.InvariantCulture);
// Assert
Assert.Empty(result);
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CommunityOutpost/GenPatcherContentRegistryTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CommunityOutpost/GenPatcherContentRegistryTests.cs
index ea7e275bd..f097f633f 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CommunityOutpost/GenPatcherContentRegistryTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/CommunityOutpost/GenPatcherContentRegistryTests.cs
@@ -23,7 +23,7 @@ public class GenPatcherContentRegistryTests
[InlineData("genl", "GenLauncher", ContentType.Addon, GameType.ZeroHour)]
[InlineData("10gn", "Generals 1.08", ContentType.GameClient, GameType.Generals)]
[InlineData("10zh", "Zero Hour 1.04", ContentType.GameClient, GameType.ZeroHour)]
- [InlineData("cbbs", "Control Bar - Basic", ContentType.Addon, GameType.ZeroHour)]
+ [InlineData("cbbs", "Control Bar HD (Base)", ContentType.Addon, GameType.ZeroHour)]
[InlineData("crzh", "Camera Mod - Zero Hour", ContentType.Addon, GameType.ZeroHour)]
public void GetMetadata_ReturnsCorrectMetadataForKnownCodes(
string contentCode,
@@ -183,6 +183,8 @@ public void GetKnownContentCodes_ReturnsNonEmptyCollection()
[InlineData("cbbs", GenPatcherContentCategory.ControlBar)]
[InlineData("crzh", GenPatcherContentCategory.Camera)]
[InlineData("hlen", GenPatcherContentCategory.Hotkeys)]
+ [InlineData("ewba", GenPatcherContentCategory.Tools)]
+ [InlineData("mskr", GenPatcherContentCategory.Maps)]
[InlineData("gent", GenPatcherContentCategory.Tools)]
[InlineData("maod", GenPatcherContentCategory.Maps)]
[InlineData("icon", GenPatcherContentCategory.Visuals)]
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/GameInstallationServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/GameInstallationServiceTests.cs
index 3df3cf8c6..05391f780 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/GameInstallationServiceTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameInstallations/GameInstallationServiceTests.cs
@@ -52,10 +52,6 @@ public GameInstallationServiceTests()
return Task.FromResult(clientResult);
});
- // Note: The service uses List, so the mock matches that concrete type.
- _clientOrchestratorMock.Setup(x => x.DetectGameClientsFromInstallationsAsync(It.IsAny>(), It.IsAny()))
- .ReturnsAsync(clientResult);
-
_service = new GameInstallationService(
_orchestratorMock.Object,
_clientOrchestratorMock.Object,
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
index 582a39975..ae40f8ce6 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
@@ -1,12 +1,14 @@
using System.Reactive.Linq;
using GenHub.Common.ViewModels;
using GenHub.Core.Interfaces.Common;
+using GenHub.Core.Interfaces.Content;
using GenHub.Core.Interfaces.GameInstallations;
using GenHub.Core.Interfaces.GameProfiles;
using GenHub.Core.Interfaces.GameSettings;
using GenHub.Core.Interfaces.GitHub;
using GenHub.Core.Interfaces.Manifest;
using GenHub.Core.Interfaces.Notifications;
+using GenHub.Core.Interfaces.Providers;
using GenHub.Core.Interfaces.Shortcuts;
using GenHub.Core.Interfaces.Steam;
using GenHub.Core.Interfaces.Storage;
@@ -18,12 +20,14 @@
using GenHub.Core.Models.Notifications;
using GenHub.Features.AppUpdate.Interfaces;
using GenHub.Features.Content.Services.ContentDiscoverers;
+using GenHub.Features.Content.Services.Publishers;
using GenHub.Features.Downloads.ViewModels;
using GenHub.Features.GameProfiles.Services;
using GenHub.Features.GameProfiles.ViewModels;
using GenHub.Features.Notifications.ViewModels;
using GenHub.Features.Settings.ViewModels;
using GenHub.Features.Tools.ViewModels;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
@@ -53,21 +57,23 @@ public void Constructor_CreatesValidInstance()
Mock.Of>(),
Mock.Of>());
- var notificationFeedVm = CreateNotificationFeedViewModel(mockNotificationService.Object);
-
// Act
+ var mockNotificationFeedVm = new Mock(
+ mockNotificationService.Object,
+ Mock.Of(),
+ Mock.Of>());
var vm = new MainViewModel(
- gameProfilesViewModel: CreateGameProfileLauncherViewModel(),
- downloadsViewModel: CreateDownloadsViewModel(),
- toolsViewModel: toolsVm,
- settingsViewModel: settingsVm,
- notificationManager: mockNotificationManager.Object,
- configurationProvider: configProvider,
- userSettingsService: userSettingsMock.Object,
- velopackUpdateManager: mockVelopackUpdateManager.Object,
- notificationService: mockNotificationService.Object,
- notificationFeedViewModel: notificationFeedVm,
- logger: mockLogger.Object);
+ CreateGameProfileLauncherViewModel(),
+ CreateDownloadsViewModel(),
+ toolsVm,
+ settingsVm,
+ mockNotificationManager.Object,
+ configProvider,
+ userSettingsMock.Object,
+ mockVelopackUpdateManager.Object,
+ mockNotificationService.Object,
+ mockNotificationFeedVm.Object,
+ mockLogger.Object);
// Assert
Assert.NotNull(vm);
@@ -95,20 +101,22 @@ public void SelectTabCommand_SetsSelectedTab(NavigationTab tab)
mockNotificationService.Object,
Mock.Of>(),
Mock.Of>());
- var notificationFeedVm = CreateNotificationFeedViewModel(mockNotificationService.Object);
-
+ var mockNotificationFeedVm = new Mock(
+ mockNotificationService.Object,
+ Mock.Of(),
+ Mock.Of>());
var vm = new MainViewModel(
- gameProfilesViewModel: CreateGameProfileLauncherViewModel(),
- downloadsViewModel: CreateDownloadsViewModel(),
- toolsViewModel: toolsVm,
- settingsViewModel: settingsVm,
- notificationManager: mockNotificationManager.Object,
- configurationProvider: configProvider,
- userSettingsService: userSettingsMock.Object,
- velopackUpdateManager: mockVelopackUpdateManager.Object,
- notificationService: mockNotificationService.Object,
- notificationFeedViewModel: notificationFeedVm,
- logger: mockLogger.Object);
+ CreateGameProfileLauncherViewModel(),
+ CreateDownloadsViewModel(),
+ toolsVm,
+ settingsVm,
+ mockNotificationManager.Object,
+ configProvider,
+ userSettingsMock.Object,
+ mockVelopackUpdateManager.Object,
+ mockNotificationService.Object,
+ mockNotificationFeedVm.Object,
+ mockLogger.Object);
vm.SelectTabCommand.Execute(tab);
Assert.Equal(tab, vm.SelectedTab);
}
@@ -133,20 +141,22 @@ public async Task InitializeAsync_MultipleCallsAreSafe()
mockNotificationService.Object,
Mock.Of>(),
Mock.Of>());
- var notificationFeedVm = CreateNotificationFeedViewModel(mockNotificationService.Object);
-
+ var mockNotificationFeedVm = new Mock(
+ mockNotificationService.Object,
+ Mock.Of(),
+ Mock.Of>());
var vm = new MainViewModel(
- gameProfilesViewModel: CreateGameProfileLauncherViewModel(),
- downloadsViewModel: CreateDownloadsViewModel(),
- toolsViewModel: toolsVm,
- settingsViewModel: settingsVm,
- notificationManager: mockNotificationManager.Object,
- configurationProvider: configProvider,
- userSettingsService: userSettingsMock.Object,
- velopackUpdateManager: mockVelopackUpdateManager.Object,
- notificationService: mockNotificationService.Object,
- notificationFeedViewModel: notificationFeedVm,
- logger: mockLogger.Object);
+ CreateGameProfileLauncherViewModel(),
+ CreateDownloadsViewModel(),
+ toolsVm,
+ settingsVm,
+ mockNotificationManager.Object,
+ configProvider,
+ userSettingsMock.Object,
+ mockVelopackUpdateManager.Object,
+ mockNotificationService.Object,
+ mockNotificationFeedVm.Object,
+ mockLogger.Object);
await vm.InitializeAsync(); // Should not throw
Assert.True(true);
}
@@ -172,20 +182,22 @@ public void CurrentTabViewModel_ReturnsCorrectViewModel(NavigationTab tab)
mockNotificationService.Object,
Mock.Of>(),
Mock.Of>());
- var notificationFeedVm = CreateNotificationFeedViewModel(mockNotificationService.Object);
-
+ var mockNotificationFeedVm = new Mock(
+ mockNotificationService.Object,
+ Mock.Of(),
+ Mock.Of>());
var vm = new MainViewModel(
- gameProfilesViewModel: CreateGameProfileLauncherViewModel(),
- downloadsViewModel: CreateDownloadsViewModel(),
- toolsViewModel: toolsVm,
- settingsViewModel: settingsVm,
- notificationManager: mockNotificationManager.Object,
- configurationProvider: configProvider,
- userSettingsService: userSettingsMock.Object,
- velopackUpdateManager: mockVelopackUpdateManager.Object,
- notificationService: mockNotificationService.Object,
- notificationFeedViewModel: notificationFeedVm,
- logger: mockLogger.Object);
+ CreateGameProfileLauncherViewModel(),
+ CreateDownloadsViewModel(),
+ toolsVm,
+ settingsVm,
+ mockNotificationManager.Object,
+ configProvider,
+ userSettingsMock.Object,
+ mockVelopackUpdateManager.Object,
+ mockNotificationService.Object,
+ mockNotificationFeedVm.Object,
+ mockLogger.Object);
vm.SelectTabCommand.Execute(tab);
var currentViewModel = vm.CurrentTabViewModel;
Assert.NotNull(currentViewModel);
@@ -236,7 +248,6 @@ private static (SettingsViewModel SettingsVm, Mock UserSet
var mockInstallationService = new Mock();
var mockStorageLocationService = new Mock();
var mockUserDataTracker = new Mock();
- var mockGitHubTokenStorage = new Mock();
var settingsVm = new SettingsViewModel(
mockUserSettings.Object,
@@ -250,8 +261,7 @@ private static (SettingsViewModel SettingsVm, Mock UserSet
mockConfigurationProvider.Object,
mockInstallationService.Object,
mockStorageLocationService.Object,
- mockUserDataTracker.Object,
- mockGitHubTokenStorage.Object);
+ mockUserDataTracker.Object);
return (settingsVm, mockUserSettings);
}
@@ -277,7 +287,7 @@ private static DownloadsViewModel CreateDownloadsViewModel()
var mockGitHubClient = new Mock();
var mockDiscovererLogger = new Mock>();
- // Instantiate the real class with the two mocks
+ // Instantiate the real class with the three mocks
var realGitHubDiscoverer = new GitHubTopicsDiscoverer(
mockGitHubClient.Object,
mockDiscovererLogger.Object);
@@ -335,6 +345,30 @@ private static GameProfileLauncherViewModel CreateGameProfileLauncherViewModel()
NullLogger.Instance);
}
+ private static SuperHackersProvider CreateSuperHackersProvider()
+ {
+ var discovererMock = new Mock();
+ discovererMock.Setup(x => x.SourceName).Returns("GitHubReleasesDiscoverer");
+
+ var resolverMock = new Mock();
+ resolverMock.Setup(x => x.ResolverId).Returns(GenHub.Core.Constants.SuperHackersConstants.ResolverId);
+
+ var delivererMock = new Mock();
+ delivererMock.Setup(x => x.SourceName).Returns(GenHub.Core.Constants.ContentSourceNames.GitHubDeliverer);
+
+ var gitHubApiClientMock = new Mock();
+
+ var loaderMock = new Mock();
+
+ return new SuperHackersProvider(
+ loaderMock.Object,
+ gitHubApiClientMock.Object,
+ [resolverMock.Object],
+ [delivererMock.Object],
+ new Mock().Object,
+ NullLogger.Instance);
+ }
+
private static Mock CreateNotificationServiceMock()
{
var mock = new Mock();
@@ -349,11 +383,4 @@ private static ProfileResourceService CreateProfileResourceService()
{
return new ProfileResourceService(NullLogger.Instance);
}
-
- private static NotificationFeedViewModel CreateNotificationFeedViewModel(INotificationService notificationService)
- {
- var mockLoggerFactory = new Mock();
- var mockLogger = new Mock>();
- return new NotificationFeedViewModel(notificationService, mockLoggerFactory.Object, mockLogger.Object);
- }
-}
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Notifications/NotificationFeedViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Notifications/NotificationFeedViewModelTests.cs
index 6573acc2d..2339c3c33 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Notifications/NotificationFeedViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Notifications/NotificationFeedViewModelTests.cs
@@ -174,7 +174,7 @@ public void DismissNotificationCommand_ShouldDismissNotification()
}
///
- /// Verifies that cleans up subscriptions.
+ /// Verifies that cleans up subscriptions.
///
[Fact]
public void Dispose_CleansUpSubscriptions()
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
index f4fc7f283..dc9193923 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Validation/FileSystemValidatorTests.cs
@@ -6,7 +6,7 @@
using Microsoft.Extensions.Logging;
using Moq;
-namespace GenHub.Tests.Features.Validation;
+namespace GenHub.Tests.Core.Features.Validation;
///
/// Unit tests for FileSystemValidator.
@@ -80,17 +80,8 @@ public async Task ValidateFilesAsync_IOException_ReportsIssue()
///
/// Test implementation of for unit testing.
///
- public class TestFileSystemValidator : FileSystemValidator
+ public class TestFileSystemValidator(ILogger logger) : FileSystemValidator(logger, new Mock().Object)
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// Logger instance.
- public TestFileSystemValidator(ILogger logger)
- : base(logger, new Mock().Object)
- {
- }
-
///
/// Exposes base ValidateDirectoriesAsync for testing.
///
@@ -98,8 +89,8 @@ public TestFileSystemValidator(ILogger logger)
/// Directories to check.
/// Cancellation token.
/// List of validation issues.
- public new Task> ValidateDirectoriesAsync(string basePath, IEnumerable requiredDirectories, CancellationToken cancellationToken)
- => FileSystemValidator.ValidateDirectoriesAsync(basePath, requiredDirectories, cancellationToken);
+ public new async Task> ValidateDirectoriesAsync(string basePath, IEnumerable requiredDirectories, CancellationToken cancellationToken)
+ => await base.ValidateDirectoriesAsync(basePath, requiredDirectories, cancellationToken);
///
/// Exposes base ValidateFilesAsync for testing.
@@ -109,7 +100,7 @@ public TestFileSystemValidator(ILogger logger)
/// Cancellation token.
/// Progress reporter.
/// List of validation issues.
- public new Task> ValidateFilesAsync(string basePath, IEnumerable files, CancellationToken cancellationToken, IProgress? progress = null)
- => base.ValidateFilesAsync(basePath, files, cancellationToken, progress);
+ public new async Task> ValidateFilesAsync(string basePath, IEnumerable files, CancellationToken cancellationToken, IProgress? progress = null)
+ => await base.ValidateFilesAsync(basePath, files, cancellationToken, progress);
}
}
\ No newline at end of file
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/BaseActionSetTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/BaseActionSetTests.cs
new file mode 100644
index 000000000..c23cdceb9
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/BaseActionSetTests.cs
@@ -0,0 +1,84 @@
+namespace GenHub.Tests.Windows.Features.ActionSets;
+
+using System.Threading.Tasks;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+///
+/// Tests for the class.
+///
+public class BaseActionSetTests
+{
+ private Mock _loggerMock;
+ private TestActionSet _testActionSet;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BaseActionSetTests()
+ {
+ _loggerMock = new Mock();
+ _testActionSet = new TestActionSet(_loggerMock.Object);
+ }
+
+ ///
+ /// Verifies that ApplyAsync logs the action and calls the internal apply method.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task ApplyAsync_LogsAndCallsInternal()
+ {
+ var installation = new GameInstallation("C:\\Test", GenHub.Core.Models.Enums.GameInstallationType.Unknown);
+
+ var result = await _testActionSet.ApplyAsync(installation);
+
+ Assert.True(result.Success);
+ Assert.True(_testActionSet.ApplyCalled);
+
+ // Verify logging happened (simplistic check)
+ _loggerMock.Verify(
+ x => x.Log(
+ LogLevel.Information,
+ It.IsAny(),
+ It.Is((v, t) => v.ToString() != null && v.ToString()!.Contains("Applying ActionSet")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.AtLeastOnce);
+ }
+
+ private class TestActionSet : BaseActionSet
+ {
+ public bool ApplyCalled { get; private set; }
+
+ public TestActionSet(ILogger logger)
+ : base(logger)
+ {
+ }
+
+ public override string Id => "Test";
+
+ public override string Title => "Test Action Set";
+
+ public override bool IsCoreFix => false;
+
+ public override bool IsCrucialFix => false;
+
+ public override Task IsApplicableAsync(GameInstallation installation) => Task.FromResult(true);
+
+ public override Task IsAppliedAsync(GameInstallation installation) => Task.FromResult(false);
+
+ protected override Task ApplyInternalAsync(GameInstallation installation, System.Threading.CancellationToken ct)
+ {
+ ApplyCalled = true;
+ return Task.FromResult(Success());
+ }
+
+ protected override Task UndoInternalAsync(GameInstallation installation, System.Threading.CancellationToken ct)
+ {
+ return Task.FromResult(Success());
+ }
+ }
+}
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/Fixes/EAAppRegistryFixTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/Fixes/EAAppRegistryFixTests.cs
new file mode 100644
index 000000000..8bd8bd0e2
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Windows/Features/ActionSets/Fixes/EAAppRegistryFixTests.cs
@@ -0,0 +1,126 @@
+namespace GenHub.Tests.Windows.Features.ActionSets.Fixes;
+
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Windows.Features.ActionSets.Fixes;
+using GenHub.Windows.Features.ActionSets.Infrastructure;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+///
+/// Tests for the class.
+///
+public class EAAppRegistryFixTests
+{
+ private readonly Mock _registryMock;
+ private readonly Mock> _loggerMock;
+ private readonly EAAppRegistryFix _fix;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EAAppRegistryFixTests()
+ {
+ _registryMock = new Mock();
+ _registryMock.Setup(r => r.IsRunningAsAdministrator()).Returns(true);
+
+ // Mock Set operations to return true (success)
+ _registryMock.Setup(r => r.SetStringValue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(true);
+ _registryMock.Setup(r => r.SetIntValue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(true);
+
+ _loggerMock = new Mock>();
+ _fix = new EAAppRegistryFix(_registryMock.Object, _loggerMock.Object);
+ }
+
+ ///
+ /// Verifies that IsApplicableAsync returns true when Generals registry keys are missing.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task IsApplicable_ReturnsTrue_WhenGeneralsKeysMissing()
+ {
+ var installation = new GameInstallation("C:\\Games", GameInstallationType.EaApp)
+ {
+ GeneralsPath = "C:\\Games\\Generals",
+ ZeroHourPath = "C:\\Games\\Zero Hour",
+ HasGenerals = true,
+ HasZeroHour = true,
+ };
+
+ // Mock Registry: Any call to GetStringValue for Install Path returns null (missing)
+ _registryMock.Setup(r => r.GetStringValue(It.IsAny(), RegistryConstants.InstallPathValueName, It.IsAny()))
+ .Returns((string?)null);
+
+ var result = await _fix.IsApplicableAsync(installation);
+
+ Assert.True(result);
+ }
+
+ ///
+ /// Verifies that IsApplicableAsync returns true when ergc registry keys are missing.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task IsApplicable_ReturnsTrue_WhenErgcMissing()
+ {
+ var installation = new GameInstallation("C:\\Games", GameInstallationType.EaApp)
+ {
+ GeneralsPath = "C:\\Games\\Generals",
+ ZeroHourPath = "C:\\Games\\Zero Hour",
+ HasGenerals = true,
+ HasZeroHour = true,
+ };
+
+ // Mock returns correct paths
+ _registryMock.Setup(r => r.GetStringValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.InstallPathValueName, It.IsAny()))
+ .Returns(installation.GeneralsPath);
+ _registryMock.Setup(r => r.GetIntValue(RegistryConstants.EAAppGeneralsKeyPath, "Version", It.IsAny()))
+ .Returns(65544); // 1.08
+
+ // Mock zero hour correct
+ _registryMock.Setup(r => r.GetStringValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.InstallPathValueName, It.IsAny()))
+ .Returns(installation.ZeroHourPath);
+ _registryMock.Setup(r => r.GetIntValue(RegistryConstants.EAAppZeroHourKeyPath, "Version", It.IsAny()))
+ .Returns(65540); // 1.04
+
+ // Ergc missing (returns empty or null)
+ _registryMock.Setup(r => r.GetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty, It.IsAny()))
+ .Returns(string.Empty);
+
+ var result = await _fix.IsApplicableAsync(installation);
+
+ Assert.True(result);
+ }
+
+ ///
+ /// Verifies that ApplyAsync sets the correct registry keys.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task Apply_SetsRegistryKeys()
+ {
+ var installation = new GameInstallation("C:\\Games", GameInstallationType.EaApp)
+ {
+ GeneralsPath = "C:\\Games\\Generals",
+ ZeroHourPath = "C:\\Games\\Zero Hour",
+ HasGenerals = true,
+ HasZeroHour = true,
+ };
+
+ var result = await _fix.ApplyAsync(installation);
+
+ Assert.True(result.Success);
+
+ // Verify installs - Verify SET usage
+ _registryMock.Verify(r => r.SetStringValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.InstallPathValueName, installation.GeneralsPath, It.IsAny()), Times.Once);
+ _registryMock.Verify(r => r.SetIntValue(RegistryConstants.EAAppGeneralsKeyPath, "Version", RegistryConstants.GeneralsVersionDWord, It.IsAny()), Times.Once);
+
+ // Verify serials logic - should attempt to write if missing (default mock returns null/empty so logic thinks it's missing)
+ _registryMock.Verify(r => r.SetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty, It.IsAny(), It.IsAny()), Times.AtLeast(1));
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/AppCompatConfigurationsFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/AppCompatConfigurationsFix.cs
new file mode 100644
index 000000000..74f7bd6ab
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/AppCompatConfigurationsFix.cs
@@ -0,0 +1,198 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Windows.Features.ActionSets.Infrastructure;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that applies Windows compatibility flags (Run as Admin, High DPI)
+/// and adds Windows Defender exclusions for game executables.
+///
+public class AppCompatConfigurationsFix(
+ IRegistryService registryService,
+ ILogger logger) : BaseActionSet(logger)
+{
+ private static readonly string[] GeneralsExecutables = ["Generals.exe", "generals.exe", "generalsv.exe"];
+ private static readonly string[] ZeroHourExecutables = ["Generals.exe", "generals.exe", "generalszh.exe", "GeneralsOnlineZH.exe", "GeneralsOnlineZH_30.exe", "GeneralsOnlineZH_60.exe"];
+
+ private readonly IRegistryService _registryService = registryService ?? throw new ArgumentNullException(nameof(registryService));
+ private readonly ILogger _logger = logger;
+
+ ///
+ public override string Id => "AppCompatConfigurationsFix";
+
+ ///
+ public override string Title => "Windows Compatibility Configurations";
+
+ ///
+ public override bool IsCoreFix => true;
+
+ ///
+ public override bool IsCrucialFix => true;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation) => Task.FromResult(true);
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ string expectedFlag = installation.InstallationType == GameInstallationType.Steam
+ ? "~ HIGHDPIAWARE"
+ : "~ RUNASADMIN HIGHDPIAWARE";
+
+ if (installation.HasGenerals)
+ {
+ foreach (var exe in GeneralsExecutables)
+ {
+ var fullPath = Path.Combine(installation.GeneralsPath, exe);
+ if (File.Exists(fullPath))
+ {
+ var current = _registryService.GetStringValue(RegistryConstants.AppCompatLayersKeyPath, fullPath);
+ if (current != expectedFlag) return Task.FromResult(false);
+ }
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ foreach (var exe in ZeroHourExecutables)
+ {
+ var fullPath = Path.Combine(installation.ZeroHourPath, exe);
+ if (File.Exists(fullPath))
+ {
+ var current = _registryService.GetStringValue(RegistryConstants.AppCompatLayersKeyPath, fullPath);
+ if (current != expectedFlag) return Task.FromResult(false);
+ }
+ }
+ }
+
+ return Task.FromResult(true);
+ }
+
+ ///
+ protected override async Task ApplyInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Starting Windows compatibility configuration...");
+
+ string flag = installation.InstallationType == GameInstallationType.Steam
+ ? "~ HIGHDPIAWARE"
+ : "~ RUNASADMIN HIGHDPIAWARE";
+
+ details.Add($"Installation type: {installation.InstallationType}");
+ details.Add($"Compatibility flags: {flag}");
+ details.Add(string.Empty);
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Processing Generals executables: {installation.GeneralsPath}");
+ await ProcessExecutablesAsync(installation.GeneralsPath, GeneralsExecutables, flag, details, ct);
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Processing Zero Hour executables: {installation.ZeroHourPath}");
+ await ProcessExecutablesAsync(installation.ZeroHourPath, ZeroHourExecutables, flag, details, ct);
+ }
+
+ details.Add("✓ Windows compatibility configuration completed successfully");
+ return new ActionSetResult(true, null, details);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to apply AppCompat configurations");
+ details.Add($"✗ Error: {ex.Message}");
+ return new ActionSetResult(false, ex.Message, details);
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Undoing Windows Compatibility Configurations is not supported via GenHub.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+
+ private async Task ProcessExecutablesAsync(string installPath, string[] executables, string flag, List details, CancellationToken ct)
+ {
+ int processedCount = 0;
+ int defenderCount = 0;
+
+ foreach (var exe in executables)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var fullPath = Path.Combine(installPath, exe);
+ if (!File.Exists(fullPath)) continue;
+
+ // 1. Set Registry AppCompat Flag
+ try
+ {
+ _registryService.SetStringValue(RegistryConstants.AppCompatLayersKeyPath, fullPath, flag);
+ details.Add($" ✓ Set compatibility flags for: {exe}");
+ processedCount++;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to set registry flag for {Path}", fullPath);
+ details.Add($" ✗ Failed to set flags for: {exe}");
+ }
+
+ // 2. Add Windows Defender Exclusion
+ var defenderResult = await AddDefenderExclusionAsync(fullPath, ct);
+ if (defenderResult)
+ {
+ details.Add($" ✓ Added Windows Defender exclusion for: {exe}");
+ defenderCount++;
+ }
+ else
+ {
+ details.Add($" ⚠ Could not add Defender exclusion for: {exe}");
+ }
+ }
+
+ details.Add($"✓ Processed {processedCount} executables");
+ details.Add($"✓ Added {defenderCount} Windows Defender exclusions");
+ }
+
+ private async Task AddDefenderExclusionAsync(string path, CancellationToken ct)
+ {
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ Arguments = $"-WindowStyle Hidden -NoProfile -NonInteractive -Command \"Add-MpPreference -ExclusionPath \\\"{path}\\\"\"",
+ CreateNoWindow = true,
+ UseShellExecute = true, // Required for admin prompt if not already admin
+ Verb = "runas",
+ };
+
+ using var process = Process.Start(psi);
+ if (process != null)
+ {
+ await process.WaitForExitAsync(ct);
+ return process.ExitCode == 0;
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to add Defender exclusion for {Path}", path);
+ return false;
+ }
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/BrowserEngineFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/BrowserEngineFix.cs
new file mode 100644
index 000000000..14926f80b
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/BrowserEngineFix.cs
@@ -0,0 +1,173 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix for the BrowserEngine.dll which causes crashes on modern systems.
+///
+public class BrowserEngineFix(ILogger logger) : BaseActionSet(logger)
+{
+ // Use constants from GameClientConstants
+ private const string BrowserEngineDll = GameClientConstants.BrowserEngineDll;
+ private const string BrowserEngineDllBak = GameClientConstants.BrowserEngineDllBak;
+
+ ///
+ public override string Id => "BrowserEngineFix";
+
+ ///
+ public override string Title => "Browser Engine DLL Fix";
+
+ ///
+ public override bool IsCoreFix => true;
+
+ ///
+ public override bool IsCrucialFix => true;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ // Applicable if the file exists in either Generals or Zero Hour path
+ if (installation.HasGenerals && File.Exists(Path.Combine(installation.GeneralsPath, BrowserEngineDll)))
+ {
+ return Task.FromResult(true);
+ }
+
+ if (installation.HasZeroHour && File.Exists(Path.Combine(installation.ZeroHourPath, BrowserEngineDll)))
+ {
+ return Task.FromResult(true);
+ }
+
+ return Task.FromResult(false);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ // Considered applied if the .bak file exists (indicating we renamed it)
+ bool generalsApplied = !installation.HasGenerals || File.Exists(Path.Combine(installation.GeneralsPath, BrowserEngineDllBak));
+ bool zeroHourApplied = !installation.HasZeroHour || File.Exists(Path.Combine(installation.ZeroHourPath, BrowserEngineDllBak));
+
+ return Task.FromResult(generalsApplied && zeroHourApplied);
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Starting BrowserEngine.dll fix...");
+ details.Add("This DLL causes crashes on modern systems and will be disabled");
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Processing Generals: {installation.GeneralsPath}");
+ var result = RenameDll(installation.GeneralsPath, details);
+ if (!result)
+ {
+ details.Add(" ⚠ BrowserEngine.dll not found (may already be fixed)");
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Processing Zero Hour: {installation.ZeroHourPath}");
+ var result = RenameDll(installation.ZeroHourPath, details);
+ if (!result)
+ {
+ details.Add(" ⚠ BrowserEngine.dll not found (may already be fixed)");
+ }
+ }
+
+ details.Add("✓ BrowserEngine.dll fix completed successfully");
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Restoring BrowserEngine.dll...");
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Processing Generals: {installation.GeneralsPath}");
+ RestoreDll(installation.GeneralsPath, details);
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Processing Zero Hour: {installation.ZeroHourPath}");
+ RestoreDll(installation.ZeroHourPath, details);
+ }
+
+ details.Add("✓ BrowserEngine.dll restored");
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ private static bool RenameDll(string path, List details)
+ {
+ var dllPath = Path.Combine(path, BrowserEngineDll);
+ var bakPath = Path.Combine(path, BrowserEngineDllBak);
+
+ if (File.Exists(dllPath))
+ {
+ if (File.Exists(bakPath))
+ {
+ File.Delete(bakPath);
+ details.Add($" • Deleted existing backup: {BrowserEngineDllBak}");
+ }
+
+ File.Move(dllPath, bakPath);
+ details.Add($" ✓ Renamed {BrowserEngineDll} → {BrowserEngineDllBak}");
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void RestoreDll(string path, List details)
+ {
+ var dllPath = Path.Combine(path, BrowserEngineDll);
+ var bakPath = Path.Combine(path, BrowserEngineDllBak);
+
+ if (File.Exists(bakPath))
+ {
+ if (File.Exists(dllPath))
+ {
+ File.Delete(dllPath);
+ }
+
+ File.Move(bakPath, dllPath);
+ details.Add($" ✓ Restored {BrowserEngineDllBak} → {BrowserEngineDll}");
+ }
+ else
+ {
+ details.Add($" ⚠ Backup file not found: {BrowserEngineDllBak}");
+ }
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/CncOnlineLauncherFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/CncOnlineLauncherFix.cs
new file mode 100644
index 000000000..4806f1a8f
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/CncOnlineLauncherFix.cs
@@ -0,0 +1,153 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Windows.Features.ActionSets.Infrastructure;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that creates registry entries for C&C Online (Revora) multiplayer service.
+/// This enables the game to properly detect and connect to C&C Online servers.
+///
+public class CncOnlineLauncherFix(
+ IRegistryService registryService,
+ ILogger logger) : BaseActionSet(logger)
+{
+ private readonly IRegistryService _registryService = registryService;
+ private readonly ILogger _logger = logger;
+
+ ///
+ public override string Id => "CncOnlineLauncherFix";
+
+ ///
+ public override string Title => "C&C Online Launcher Fix";
+
+ ///
+ public override bool IsCoreFix => false;
+
+ ///
+ public override bool IsCrucialFix => false;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ return Task.FromResult(installation.HasGenerals || installation.HasZeroHour);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ try
+ {
+ // Check if C&C Online registry entries exist
+ var cncOnlineInstalled = _registryService.GetStringValue(
+ RegistryConstants.CncOnlineKeyPath,
+ RegistryConstants.InstallPathValueName);
+
+ return Task.FromResult(!string.IsNullOrEmpty(cncOnlineInstalled));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking C&C Online registry status");
+ return Task.FromResult(false);
+ }
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Starting C&C Online registry configuration...");
+
+ // Create C&C Online registry entries for Generals
+ if (installation.HasGenerals)
+ {
+ details.Add($"Configuring C&C Online for Generals at: {installation.GeneralsPath}");
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineGeneralsKeyPath,
+ RegistryConstants.InstallPathValueName,
+ installation.GeneralsPath);
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineGeneralsKeyPath,
+ RegistryConstants.VersionValueName,
+ RegistryConstants.CncOnlineGeneralsVersion);
+
+ details.Add("✓ Created: HKCU\\SOFTWARE\\Revora\\CNCOnline\\Generals");
+ details.Add($" • InstallPath = {installation.GeneralsPath}");
+ details.Add(" • Version = 1.08");
+
+ _logger.LogInformation("Created C&C Online registry entries for Generals");
+ }
+
+ // Create C&C Online registry entries for Zero Hour
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Configuring C&C Online for Zero Hour at: {installation.ZeroHourPath}");
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineZeroHourKeyPath,
+ RegistryConstants.InstallPathValueName,
+ installation.ZeroHourPath);
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineZeroHourKeyPath,
+ RegistryConstants.VersionValueName,
+ RegistryConstants.CncOnlineZeroHourVersion);
+
+ details.Add("✓ Created: HKCU\\SOFTWARE\\Revora\\CNCOnline\\ZeroHour");
+ details.Add($" • InstallPath = {installation.ZeroHourPath}");
+ details.Add(" • Version = 1.04");
+
+ _logger.LogInformation("Created C&C Online registry entries for Zero Hour");
+ }
+
+ // Create main C&C Online entry
+ var basePath = installation.HasGenerals
+ ? installation.GeneralsPath
+ : installation.ZeroHourPath;
+
+ details.Add("Creating main C&C Online registry entry...");
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineKeyPath,
+ RegistryConstants.InstallPathValueName,
+ basePath);
+
+ _registryService.SetStringValue(
+ RegistryConstants.CncOnlineKeyPath,
+ RegistryConstants.VersionValueName,
+ RegistryConstants.CncOnlineVersion);
+
+ details.Add("✓ Created: HKCU\\SOFTWARE\\Revora\\CNCOnline");
+ details.Add($" • InstallPath = {basePath}");
+ details.Add(" • Version = 1.0");
+ details.Add("✓ C&C Online registry configuration completed successfully");
+
+ _logger.LogInformation("C&C Online registry fix applied with {DetailCount} actions", details.Count);
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error applying C&C Online registry fix");
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Undoing C&C Online Registry Fix is not recommended as it may break multiplayer functionality.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/D3D8XDLLCheck.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/D3D8XDLLCheck.cs
new file mode 100644
index 000000000..cbf1b4ee2
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/D3D8XDLLCheck.cs
@@ -0,0 +1,146 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that checks for DirectX 8 DLLs required by the game.
+/// This fix verifies that necessary DirectX 8 runtime files are present
+/// and provides guidance if they are missing.
+///
+public class D3D8XDLLCheck(ILogger logger) : BaseActionSet(logger)
+{
+ // DirectX 8/9 DLLs that Generals and Zero Hour may require (Retail only)
+ private static readonly string[] RequiredDLLs =
+ [
+ "d3d8.dll",
+ "d3d8thk.dll",
+ "d3dx9_43.dll",
+ ];
+
+ private readonly ILogger _logger = logger;
+
+ ///
+ public override string Id => "D3D8XDLLCheck";
+
+ ///
+ public override string Title => "DirectX 8 DLL Check";
+
+ ///
+ public override bool IsCoreFix => false;
+
+ ///
+ public override bool IsCrucialFix => false;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ return Task.FromResult(installation.HasGenerals || installation.HasZeroHour);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ try
+ {
+ // Check if required DirectX DLLs are present in system directories
+ var system32 = Environment.GetFolderPath(Environment.SpecialFolder.System);
+ var sysWow64 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SysWOW64");
+ var gameDir = installation.InstallationPath;
+
+ var allPresent = true;
+ var missingDLLs = new List();
+
+ foreach (var dll in RequiredDLLs)
+ {
+ var inSystem32 = File.Exists(Path.Combine(system32, dll));
+ var inSysWow64 = File.Exists(Path.Combine(sysWow64, dll));
+ var inGameDir = !string.IsNullOrEmpty(gameDir) && File.Exists(Path.Combine(gameDir, dll));
+
+ if (!inSystem32 && !inSysWow64 && !inGameDir)
+ {
+ allPresent = false;
+ missingDLLs.Add(dll);
+ }
+ }
+
+ if (allPresent)
+ {
+ _logger.LogInformation("All required DirectX 8 DLLs are present");
+ }
+ else
+ {
+ _logger.LogWarning("Missing DirectX 8 DLLs: {DLLs}", string.Join(", ", missingDLLs));
+ }
+
+ return Task.FromResult(allPresent);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking DirectX 8 DLLs");
+ return Task.FromResult(false);
+ }
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ try
+ {
+ // This fix is informational - it checks for DLLs and provides guidance
+ // The actual DirectX installation is handled by DirectXRuntimeFix
+ var system32 = Environment.GetFolderPath(Environment.SpecialFolder.System);
+ var sysWow64 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SysWOW64");
+
+ var missingDLLs = new List();
+
+ foreach (var dll in RequiredDLLs)
+ {
+ var inSystem32 = File.Exists(Path.Combine(system32, dll));
+ var inSysWow64 = File.Exists(Path.Combine(sysWow64, dll));
+
+ if (!inSystem32 && !inSysWow64)
+ {
+ missingDLLs.Add(dll);
+ }
+ }
+
+ if (missingDLLs.Count == 0)
+ {
+ _logger.LogInformation("All required DirectX 8 DLLs are present. No action needed.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+
+ _logger.LogWarning("The following DirectX 8 DLLs are missing:");
+ foreach (var dll in missingDLLs)
+ {
+ _logger.LogWarning(" - {DLL}", dll);
+ }
+
+ _logger.LogInformation("To fix this issue:");
+ _logger.LogInformation("1. Run DirectXRuntimeFix to install DirectX 8.1/9.0c runtime");
+ _logger.LogInformation("2. This will install all required DirectX 8 DLLs");
+ _logger.LogInformation("3. Restart your computer after installation");
+
+ return Task.FromResult(new ActionSetResult(true, null, [$"Missing {missingDLLs.Count} DirectX 8 DLLs in system directories. Please run DirectXRuntimeFix."]));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking DirectX 8 DLLs");
+ return Task.FromResult(new ActionSetResult(false, ex.Message));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("D3D8XDLLCheck is informational only. No undo action needed.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DbgHelpFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DbgHelpFix.cs
new file mode 100644
index 000000000..0c13c1927
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DbgHelpFix.cs
@@ -0,0 +1,174 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix for the dbghelp.dll which causes crashes on modern systems.
+///
+public class DbgHelpFix(ILogger logger) : BaseActionSet(logger)
+{
+ // Use constants from GameClientConstants
+ private const string DbgHelpDll = GameClientConstants.DbgHelpDll;
+ private const string DbgHelpDllBak = GameClientConstants.DbgHelpDllBak;
+
+ ///
+ public override string Id => "DbgHelpFix";
+
+ ///
+ public override string Title => "Debug Help DLL Fix";
+
+ ///
+ public override bool IsCoreFix => true;
+
+ ///
+ public override bool IsCrucialFix => true;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ // Applicable if the file exists in either Generals or Zero Hour path
+ // This fix is needed because the old dbghelp.dll causes crashes on modern Windows
+ if (installation.HasGenerals && File.Exists(Path.Combine(installation.GeneralsPath, DbgHelpDll)))
+ {
+ return Task.FromResult(true);
+ }
+
+ if (installation.HasZeroHour && File.Exists(Path.Combine(installation.ZeroHourPath, DbgHelpDll)))
+ {
+ return Task.FromResult(true);
+ }
+
+ return Task.FromResult(false);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ // Considered applied if the DLL is missing (renamed) in all present installations
+ bool generalsOk = !installation.HasGenerals || !File.Exists(Path.Combine(installation.GeneralsPath, DbgHelpDll));
+ bool zeroHourOk = !installation.HasZeroHour || !File.Exists(Path.Combine(installation.ZeroHourPath, DbgHelpDll));
+
+ return Task.FromResult(generalsOk && zeroHourOk);
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Starting DbgHelp.dll fix...");
+ details.Add("This DLL causes crashes on modern Windows and will be disabled");
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Processing Generals: {installation.GeneralsPath}");
+ var result = RenameDll(installation.GeneralsPath, details);
+ if (!result)
+ {
+ details.Add(" ⚠ DbgHelp.dll not found (may already be fixed)");
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Processing Zero Hour: {installation.ZeroHourPath}");
+ var result = RenameDll(installation.ZeroHourPath, details);
+ if (!result)
+ {
+ details.Add(" ⚠ DbgHelp.dll not found (may already be fixed)");
+ }
+ }
+
+ details.Add("✓ DbgHelp.dll fix completed successfully");
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ try
+ {
+ details.Add("Restoring DbgHelp.dll...");
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Processing Generals: {installation.GeneralsPath}");
+ RestoreDll(installation.GeneralsPath, details);
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Processing Zero Hour: {installation.ZeroHourPath}");
+ RestoreDll(installation.ZeroHourPath, details);
+ }
+
+ details.Add("✓ DbgHelp.dll restored");
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ private static bool RenameDll(string path, List details)
+ {
+ var dllPath = Path.Combine(path, DbgHelpDll);
+ var bakPath = Path.Combine(path, DbgHelpDllBak);
+
+ if (File.Exists(dllPath))
+ {
+ if (File.Exists(bakPath))
+ {
+ File.Delete(bakPath);
+ details.Add($" • Deleted existing backup: {DbgHelpDllBak}");
+ }
+
+ File.Move(dllPath, bakPath);
+ details.Add($" ✓ Renamed {DbgHelpDll} → {DbgHelpDllBak}");
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void RestoreDll(string path, List details)
+ {
+ var dllPath = Path.Combine(path, DbgHelpDll);
+ var bakPath = Path.Combine(path, DbgHelpDllBak);
+
+ if (File.Exists(bakPath))
+ {
+ if (File.Exists(dllPath))
+ {
+ File.Delete(dllPath);
+ }
+
+ File.Move(bakPath, dllPath);
+ details.Add($" ✓ Restored {DbgHelpDllBak} → {DbgHelpDll}");
+ }
+ else
+ {
+ details.Add($" ⚠ Backup file not found: {DbgHelpDllBak}");
+ }
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DirectXRuntimeFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DirectXRuntimeFix.cs
new file mode 100644
index 000000000..29856de38
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DirectXRuntimeFix.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Core.Models.Results;
+using Microsoft.Extensions.Logging;
+
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+///
+/// Fix that downloads and installs DirectX 8.1 and 9.0c runtime components required for Generals and Zero Hour.
+///
+public class DirectXRuntimeFix(IHttpClientFactory httpClientFactory, ILogger logger) : BaseActionSet(logger)
+{
+ private readonly ILogger _logger = logger;
+
+ ///
+ public override string Id => "DirectXRuntimeFix";
+
+ ///
+ public override string Title => "DirectX Runtime Fix";
+
+ ///
+ public override bool IsCoreFix => true;
+
+ ///
+ public override bool IsCrucialFix => false; // Network failures shouldn't abort entire sequence
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ // This fix is applicable regardless of installation type as it's a system dependency
+ return Task.FromResult(true);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ try
+ {
+ // GenPatcher check: If D3DX9_43.dll (DX9) and d3d8.dll (DX8 Core) exist, we are good.
+ // Note: Modern dxwebsetup often skips d3dx8.dll (helper), but d3d8.dll is sufficient for the game to launch.
+ var sysWow64Path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SysWOW64");
+ var dx9Dll = Path.Combine(sysWow64Path, "D3DX9_43.dll");
+ var dx8Dll = Path.Combine(sysWow64Path, "d3d8.dll");
+
+ return Task.FromResult(File.Exists(dx9Dll) && File.Exists(dx8Dll));
+ }
+ catch
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ ///
+ protected override async Task ApplyInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ var details = new List();
+ var tempFolder = Path.Combine(Path.GetTempPath(), "GenHub_DirectX");
+ var zipFile = Path.Combine(tempFolder, "dx_runtime.zip");
+ var extractPath = Path.Combine(tempFolder, "Extracted");
+
+ try
+ {
+ details.Add("Starting DirectX Runtime installation...");
+ details.Add($"Download URL: {ExternalUrls.DirectXRuntimeDownloadUrl}");
+
+ if (Directory.Exists(tempFolder))
+ {
+ Directory.Delete(tempFolder, true);
+ }
+
+ Directory.CreateDirectory(extractPath);
+ details.Add($"Temp directory: {tempFolder}");
+
+ details.Add("Downloading DirectX Runtime...");
+
+ using var client = httpClientFactory.CreateClient();
+
+ // Add User-Agent to avoid blocking
+ client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+
+ // Increase timeout for large downloads (DirectX is ~100MB)
+ client.Timeout = TimeSpan.FromMinutes(5);
+
+ var urls = new[]
+ {
+ ExternalUrls.DirectXRuntimeDownloadUrlPrimary,
+ ExternalUrls.DirectXRuntimeDownloadUrlMirror1,
+ };
+ bool downloaded = false;
+
+ var isExe = false;
+ var downloadPath = string.Empty;
+
+ foreach (var url in urls)
+ {
+ try
+ {
+ _logger.LogInformation("Attempting download from {Url}", url);
+
+ var uri = new Uri(url);
+ isExe = uri.AbsolutePath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);
+ downloadPath = isExe ? Path.Combine(tempFolder, "dxsetup.exe") : zipFile;
+
+ var response = await client.GetAsync(url, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var fileSize = response.Content.Headers.ContentLength ?? 0;
+
+ // Validate file size - 200KB for web installer, 1MB for zip
+ var minSize = isExe ? 200 * 1024 : 1024 * 1024;
+
+ if (fileSize < minSize)
+ {
+ _logger.LogWarning("Downloaded file from {Url} is too small ({Size} bytes). Likely blocked.", url, fileSize);
+ continue;
+ }
+
+ details.Add($"✓ Downloaded {fileSize / 1024.0 / 1024.0:F2} MB from {uri.Host}");
+
+ _logger.LogInformation("Reading response content to memory...");
+ var fileBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
+
+ _logger.LogInformation("Writing {Size} bytes to disk...", fileBytes.Length);
+ await File.WriteAllBytesAsync(downloadPath, fileBytes, cancellationToken);
+
+ if (!isExe)
+ {
+ // Validate ZIP integrity
+ try
+ {
+ using var archive = ZipFile.OpenRead(downloadPath);
+ var entryCount = archive.Entries.Count;
+ _logger.LogInformation("Validated zip archive from {Url} ({Count} entries)", url, entryCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning("Downloaded file from {Url} is corrupt: {Error}. Trying next mirror.", url, ex.Message);
+ continue;
+ }
+ }
+
+ downloaded = true;
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning("Failed to download from {Url}: {Error}", url, ex.Message);
+ }
+ }
+
+ if (!downloaded)
+ {
+ throw new HttpRequestException("Failed to download or validate DirectX Runtime from all mirrors.");
+ }
+
+ string setupExe;
+ string arguments = string.Empty;
+
+ if (isExe)
+ {
+ setupExe = downloadPath;
+ arguments = "/Q"; // Silent install for web setup
+ details.Add("Running DirectX Web Setup...");
+ }
+ else
+ {
+ details.Add("Extracting DirectX Runtime...");
+ _logger.LogInformation("Extracting DirectX Runtime...");
+ ZipFile.ExtractToDirectory(zipFile, extractPath);
+
+ var extractedFiles = Directory.GetFiles(extractPath, "*.*", SearchOption.AllDirectories);
+ details.Add($"✓ Extracted {extractedFiles.Length} files");
+
+ setupExe = Path.Combine(extractPath, "DXSETUP.exe");
+ if (!File.Exists(setupExe))
+ {
+ details.Add("✗ DXSETUP.exe not found in package");
+ return new ActionSetResult(false, "DXSETUP.exe not found in downloaded package.", details);
+ }
+ }
+
+ details.Add($"Running DirectX Setup (silent mode)...");
+ details.Add(" ⚠ This may require administrator privileges");
+ _logger.LogInformation("Running DirectX Setup (Silent)...");
+
+ var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = setupExe,
+ Arguments = arguments,
+ UseShellExecute = true,
+ Verb = "runas",
+ });
+
+ if (process == null)
+ {
+ details.Add("✗ Failed to start DirectX setup process");
+ return new ActionSetResult(false, "Failed to start DirectX setup process.", details);
+ }
+
+ await process.WaitForExitAsync(cancellationToken);
+
+ if (process.ExitCode != 0)
+ {
+ _logger.LogWarning("DirectX setup exited with code {ExitCode}", process.ExitCode);
+ details.Add($"⚠ DirectX setup exited with code {process.ExitCode}");
+ details.Add(" Note: Non-zero codes may not indicate failure");
+ }
+ else
+ {
+ details.Add("✓ DirectX setup completed successfully");
+ }
+
+ details.Add("✓ DirectX Runtime installation completed");
+
+ return new ActionSetResult(true, null, details);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error implementing DirectX Runtime Fix");
+ details.Add($"✗ Error: {ex.Message}");
+ return new ActionSetResult(false, ex.Message, details);
+ }
+ finally
+ {
+ try
+ {
+ if (Directory.Exists(tempFolder))
+ {
+ Directory.Delete(tempFolder, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Failed to cleanup temp directory: {TempFolder}", tempFolder);
+ }
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Uninstalling DirectX Runtime is not supported via GenHub.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DisableOriginInGame.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DisableOriginInGame.cs
new file mode 100644
index 000000000..45224121e
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/DisableOriginInGame.cs
@@ -0,0 +1,160 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that disables Origin in-game overlay for Generals and Zero Hour.
+/// The Origin overlay can cause performance issues and conflicts with the game.
+///
+public class DisableOriginInGame(ILogger logger) : BaseActionSet(logger)
+{
+ private readonly ILogger _logger = logger;
+ private readonly string _markerPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GenHub", ActionSetConstants.Paths.SubActionSetMarkers, "DisableOriginInGame.done");
+
+ ///
+ public override string Id => "DisableOriginInGame";
+
+ ///
+ public override string Title => "Disable Origin In-Game Overlay";
+
+ ///
+ public override bool IsCoreFix => false;
+
+ ///
+ public override bool IsCrucialFix => false;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ // Only applicable if Origin is actually installed (something to disable)
+ var originInstalled = IsOriginInstalled();
+ return Task.FromResult(originInstalled && (installation.HasGenerals || installation.HasZeroHour));
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ return Task.FromResult(File.Exists(_markerPath));
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var originInstalled = IsOriginInstalled();
+
+ if (!originInstalled)
+ {
+ _logger.LogInformation("Origin is not installed. No action needed.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+
+ // Check if overlay is already disabled
+ if (IsOriginOverlayDisabled())
+ {
+ _logger.LogInformation("Origin in-game overlay is already disabled.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+
+ // Provide guidance for disabling Origin overlay
+ _logger.LogWarning("Origin in-game overlay is enabled. This may cause performance issues.");
+ _logger.LogInformation("To disable Origin in-game overlay:");
+ _logger.LogInformation("1. Open Origin client");
+ _logger.LogInformation("2. Go to 'Application Settings' (gear icon)");
+ _logger.LogInformation("3. Select 'Origin In-Game'");
+ _logger.LogInformation("4. Uncheck 'Enable Origin In-Game'");
+ _logger.LogInformation("5. Click 'Save'");
+ _logger.LogInformation(string.Empty);
+ _logger.LogInformation("Alternatively, you can disable it per game:");
+ _logger.LogInformation("1. Right-click on Generals or Zero Hour in Origin");
+ _logger.LogInformation("2. Select 'Game Properties'");
+ _logger.LogInformation("3. Uncheck 'Enable Origin In-Game for this game'");
+ _logger.LogInformation("4. Click 'Save'");
+
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_markerPath)!);
+ File.WriteAllText(_markerPath, DateTime.UtcNow.ToString());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to create marker file for DisableOriginInGame");
+ }
+
+ return Task.FromResult(new ActionSetResult(true, "Please manually disable Origin in-game overlay. See logs for details."));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error applying Origin overlay disable fix");
+ return Task.FromResult(new ActionSetResult(false, ex.Message));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Disable Origin In-Game Fix is informational only. No undo action needed.");
+ return Task.FromResult(new ActionSetResult(true));
+ }
+
+ private bool IsOriginInstalled()
+ {
+ try
+ {
+ // Check for Origin in registry
+ using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
+ RegistryConstants.OriginKeyPath,
+ false);
+
+ if (key != null)
+ {
+ return true;
+ }
+
+ // Check for Origin processes
+ var processes = Process.GetProcessesByName("Origin");
+ return processes.Length > 0;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error checking for Origin installation");
+ return false;
+ }
+ }
+
+ private bool IsOriginOverlayDisabled()
+ {
+ try
+ {
+ // Check Origin configuration file
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var originConfigPath = Path.Combine(localAppData, "Origin", "Origin.ini");
+
+ if (!File.Exists(originConfigPath))
+ {
+ return false;
+ }
+
+ var configContent = File.ReadAllText(originConfigPath);
+
+ // Check if overlay is disabled
+ // The setting is typically in the format: [General] OverlayEnabled=0
+ return configContent.Contains("OverlayEnabled=0", StringComparison.OrdinalIgnoreCase) ||
+ configContent.Contains("OverlayEnabled=false", StringComparison.OrdinalIgnoreCase);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error checking Origin overlay configuration");
+ return false;
+ }
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EAAppRegistryFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EAAppRegistryFix.cs
new file mode 100644
index 000000000..ec36b55b2
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EAAppRegistryFix.cs
@@ -0,0 +1,262 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Windows.Features.ActionSets.Infrastructure;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix for EA App registry keys which are often missing or incorrect.
+///
+/// The registry service.
+/// The logger instance.
+public class EAAppRegistryFix(IRegistryService registryService, ILogger logger) : BaseActionSet(logger)
+{
+ private readonly IRegistryService _registryService = registryService ?? throw new ArgumentNullException(nameof(registryService));
+
+ ///
+ public override string Id => "EAAppRegistryFix";
+
+ ///
+ public override string Title => "EA App Registry Fix";
+
+ ///
+ public override bool IsCoreFix => true;
+
+ ///
+ public override bool IsCrucialFix => true;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ // Strictly only for EA App or unknown types that we want to force-fix registry for.
+ if (installation.InstallationType != GameInstallationType.EaApp && installation.InstallationType != GameInstallationType.Unknown)
+ {
+ return Task.FromResult(false);
+ }
+
+ // Applicable if keys are missing or point to wrong location
+ bool fixNeeded = false;
+
+ if (installation.HasGenerals)
+ {
+ var installPath = _registryService.GetStringValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.InstallPathValueName);
+ var version = _registryService.GetIntValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.VersionValueName);
+ var serial = _registryService.GetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty); // Default value name is empty string
+
+ if (!string.Equals(installPath, installation.GeneralsPath, StringComparison.OrdinalIgnoreCase) ||
+ version != RegistryConstants.GeneralsVersionDWord ||
+ string.IsNullOrEmpty(serial))
+ {
+ fixNeeded = true;
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ var installPath = _registryService.GetStringValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.InstallPathValueName);
+ var version = _registryService.GetIntValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.VersionValueName);
+ var serial = _registryService.GetStringValue(RegistryConstants.EAAppZeroHourErgcKeyPath, string.Empty);
+
+ if (!string.Equals(installPath, installation.ZeroHourPath, StringComparison.OrdinalIgnoreCase) ||
+ version != RegistryConstants.ZeroHourVersionDWord ||
+ string.IsNullOrEmpty(serial))
+ {
+ fixNeeded = true;
+ }
+ }
+
+ return Task.FromResult(fixNeeded);
+ }
+
+ ///
+ public override Task IsAppliedAsync(GameInstallation installation)
+ {
+ if (installation.HasGenerals)
+ {
+ var installPath = _registryService.GetStringValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.InstallPathValueName);
+ var version = _registryService.GetIntValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.VersionValueName);
+ var serial = _registryService.GetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty);
+
+ if (!string.Equals(installPath, installation.GeneralsPath, StringComparison.OrdinalIgnoreCase) ||
+ version != RegistryConstants.GeneralsVersionDWord ||
+ string.IsNullOrEmpty(serial))
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ var installPath = _registryService.GetStringValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.InstallPathValueName);
+ var version = _registryService.GetIntValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.VersionValueName);
+ var serial = _registryService.GetStringValue(RegistryConstants.EAAppZeroHourErgcKeyPath, string.Empty);
+
+ if (!string.Equals(installPath, installation.ZeroHourPath, StringComparison.OrdinalIgnoreCase) ||
+ version != RegistryConstants.ZeroHourVersionDWord ||
+ string.IsNullOrEmpty(serial))
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ return Task.FromResult(true);
+ }
+
+ ///
+ protected override Task ApplyInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ var details = new List();
+
+ // Check if running as administrator - required for HKEY_LOCAL_MACHINE writes
+ if (!_registryService.IsRunningAsAdministrator())
+ {
+ details.Add("✗ Administrator privileges required");
+ details.Add(" Registry modifications require elevated permissions");
+ return Task.FromResult(new ActionSetResult(false, "Administrator privileges are required to modify registry keys. Please restart GenHub as administrator.", details));
+ }
+
+ try
+ {
+ details.Add("Starting EA App registry configuration...");
+ bool allSucceeded = true;
+ var failedOperations = new List();
+
+ if (installation.HasGenerals)
+ {
+ details.Add($"Configuring EA App registry for Generals: {installation.GeneralsPath}");
+
+ if (!_registryService.SetStringValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.InstallPathValueName, installation.GeneralsPath))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppGeneralsKeyPath}\\{RegistryConstants.InstallPathValueName}");
+ details.Add($" ✗ Failed to set InstallPath");
+ }
+ else
+ {
+ details.Add($" ✓ InstallPath = {installation.GeneralsPath}");
+ }
+
+ if (!_registryService.SetIntValue(RegistryConstants.EAAppGeneralsKeyPath, RegistryConstants.VersionValueName, RegistryConstants.GeneralsVersionDWord))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppGeneralsKeyPath}\\{RegistryConstants.VersionValueName}");
+ details.Add($" ✗ Failed to set Version");
+ }
+ else
+ {
+ details.Add($" ✓ Version = {RegistryConstants.GeneralsVersionDWord}");
+ }
+
+ var existingSerial = _registryService.GetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty);
+ if (string.IsNullOrEmpty(existingSerial))
+ {
+ const string defaultSerial = "1234567890";
+ if (!_registryService.SetStringValue(RegistryConstants.EAAppGeneralsErgcKeyPath, string.Empty, defaultSerial))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppGeneralsErgcKeyPath}\\(Default)");
+ details.Add($" ✗ Failed to set serial key");
+ }
+ else
+ {
+ details.Add($" ✓ Serial key created: {defaultSerial}");
+ }
+ }
+ else
+ {
+ details.Add($" ✓ Serial key already exists");
+ }
+
+ if (allSucceeded)
+ {
+ details.Add("✓ Generals registry configuration completed");
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ details.Add($"Configuring EA App registry for Zero Hour: {installation.ZeroHourPath}");
+
+ if (!_registryService.SetStringValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.InstallPathValueName, installation.ZeroHourPath))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppZeroHourKeyPath}\\{RegistryConstants.InstallPathValueName}");
+ details.Add($" ✗ Failed to set InstallPath");
+ }
+ else
+ {
+ details.Add($" ✓ InstallPath = {installation.ZeroHourPath}");
+ }
+
+ if (!_registryService.SetIntValue(RegistryConstants.EAAppZeroHourKeyPath, RegistryConstants.VersionValueName, RegistryConstants.ZeroHourVersionDWord))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppZeroHourKeyPath}\\{RegistryConstants.VersionValueName}");
+ details.Add($" ✗ Failed to set Version");
+ }
+ else
+ {
+ details.Add($" ✓ Version = {RegistryConstants.ZeroHourVersionDWord}");
+ }
+
+ var existingSerial = _registryService.GetStringValue(RegistryConstants.EAAppZeroHourErgcKeyPath, string.Empty);
+ if (string.IsNullOrEmpty(existingSerial))
+ {
+ const string defaultSerial = "1234567890";
+ if (!_registryService.SetStringValue(RegistryConstants.EAAppZeroHourErgcKeyPath, string.Empty, defaultSerial))
+ {
+ allSucceeded = false;
+ failedOperations.Add($"{RegistryConstants.EAAppZeroHourErgcKeyPath}\\(Default)");
+ details.Add($" ✗ Failed to set serial key");
+ }
+ else
+ {
+ details.Add($" ✓ Serial key created: {defaultSerial}");
+ }
+ }
+ else
+ {
+ details.Add($" ✓ Serial key already exists");
+ }
+
+ if (allSucceeded)
+ {
+ details.Add("✓ Zero Hour registry configuration completed");
+ }
+ }
+
+ if (!allSucceeded)
+ {
+ details.Add($"✗ Failed to write {failedOperations.Count} registry key(s)");
+ foreach (var op in failedOperations)
+ {
+ details.Add($" • {op}");
+ }
+
+ return Task.FromResult(new ActionSetResult(false, $"Failed to write the following registry keys: {string.Join(", ", failedOperations)}. Ensure you are running as administrator.", details));
+ }
+
+ details.Add("✓ EA App registry configuration completed successfully");
+ return Task.FromResult(new ActionSetResult(true, null, details));
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error: {ex.Message}");
+ return Task.FromResult(new ActionSetResult(false, ex.Message, details));
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken ct)
+ {
+ // Undoing registry fixes is tricky - usually we don't want to revert to a broken state.
+ return Task.FromResult(Success());
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EdgeScrollerFix.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EdgeScrollerFix.cs
new file mode 100644
index 000000000..ac340a8af
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/EdgeScrollerFix.cs
@@ -0,0 +1,185 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Constants;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Interfaces.GameSettings;
+using GenHub.Core.Models.Enums;
+using GenHub.Core.Models.GameInstallations;
+using GenHub.Core.Models.GameSettings;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that improves edge scrolling for modern high-resolution displays.
+/// This fix adjusts edge scrolling sensitivity in Options.ini to ensure
+/// smooth scrolling when mouse cursor reaches the screen edge.
+///
+public class EdgeScrollerFix(ILogger logger, IGameSettingsService gameSettingsService) : BaseActionSet(logger)
+{
+ private readonly ILogger _logger = logger;
+ private readonly IGameSettingsService _gameSettingsService = gameSettingsService;
+
+ ///
+ public override string Id => "EdgeScrollerFix";
+
+ ///
+ public override string Title => "Edge Scrolling Fix";
+
+ ///
+ public override bool IsCoreFix => false;
+
+ ///
+ public override bool IsCrucialFix => false;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ return Task.FromResult(installation.HasGenerals || installation.HasZeroHour);
+ }
+
+ ///
+ public override async Task IsAppliedAsync(GameInstallation installation)
+ {
+ try
+ {
+ if (installation.HasGenerals)
+ {
+ var result = await _gameSettingsService.LoadOptionsAsync(GameType.Generals);
+ if (!result.Success || result.Data == null || !IsEdgeScrollingOptimal(result.Data))
+ {
+ return false;
+ }
+ }
+
+ if (installation.HasZeroHour)
+ {
+ var result = await _gameSettingsService.LoadOptionsAsync(GameType.ZeroHour);
+ if (!result.Success || result.Data == null || !IsEdgeScrollingOptimal(result.Data))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking edge scrolling status");
+ return false;
+ }
+ }
+
+ ///
+ protected override async Task ApplyInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var details = new List();
+
+ if (installation.HasGenerals)
+ {
+ var gameDetails = await ApplyEdgeScrollingFixAsync(GameType.Generals);
+ details.AddRange(gameDetails);
+ }
+
+ if (installation.HasZeroHour)
+ {
+ var gameDetails = await ApplyEdgeScrollingFixAsync(GameType.ZeroHour);
+ details.AddRange(gameDetails);
+ }
+
+ if (details.Count == 0)
+ {
+ details.Add("No games found to apply edge scrolling fix to.");
+ }
+
+ return new ActionSetResult(true, null, details);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error applying edge scrolling fix");
+ return new ActionSetResult(false, ex.Message, [$"Error: {ex.Message}"]);
+ }
+ }
+
+ ///
+ protected override Task UndoInternalAsync(GameInstallation installation, CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Undoing Edge Scrolling Fix is not supported via GenHub.");
+ return Task.FromResult(new ActionSetResult(true, null, ["Undo not supported for Edge Scrolling Fix."]));
+ }
+
+ private static bool IsEdgeScrollingOptimal(IniOptions options)
+ {
+ // Check if edge scrolling settings exist in TheSuperHackers section
+ // If the section exists with ScrollEdgeZone or ScrollEdgeSpeed, consider it applied
+ if (!options.AdditionalSections.TryGetValue("TheSuperHackers", out var tshSection))
+ {
+ return false;
+ }
+
+ // If either setting exists, consider the fix applied
+ return tshSection.ContainsKey("ScrollEdgeZone") || tshSection.ContainsKey("ScrollEdgeSpeed");
+ }
+
+ private async Task> ApplyEdgeScrollingFixAsync(GameType gameType)
+ {
+ var details = new List();
+
+ try
+ {
+ _logger.LogInformation("Applying edge scrolling fix for {GameType}", gameType);
+
+ var result = await _gameSettingsService.LoadOptionsAsync(gameType);
+ if (!result.Success || result.Data == null)
+ {
+ var msg = $"⚠ Could not load Options.ini for {gameType}";
+ details.Add(msg);
+ _logger.LogWarning("Could not load settings for {GameType}", gameType);
+ return details;
+ }
+
+ var options = result.Data;
+ var optionsPath = _gameSettingsService.GetOptionsFilePath(gameType);
+
+ // Apply optimal edge scrolling settings
+ if (!options.AdditionalSections.TryGetValue(ActionSetConstants.IniFiles.TheSuperHackersSection, out var tshSection))
+ {
+ tshSection = [];
+ options.AdditionalSections[ActionSetConstants.IniFiles.TheSuperHackersSection] = tshSection;
+ details.Add($"✓ Created [{ActionSetConstants.IniFiles.TheSuperHackersSection}] section in Options.ini for {gameType}");
+ }
+
+ // Apply scroll settings
+ tshSection[ActionSetConstants.IniFiles.ScrollEdgeZoneKey] = "0";
+ tshSection[ActionSetConstants.IniFiles.ScrollEdgeSpeedKey] = "1.0";
+ tshSection[ActionSetConstants.IniFiles.ScrollEdgeAccelerationKey] = "0.0";
+
+ // Also ensure default scroll factor is good if present
+ if (tshSection.ContainsKey("ScrollFactor"))
+ {
+ tshSection["ScrollFactor"] = "60";
+ details.Add($"✓ Set ScrollFactor=60 for {gameType}");
+ }
+
+ details.Add($"✓ Set {ActionSetConstants.IniFiles.ScrollEdgeZoneKey}=0 for {gameType}");
+ details.Add($"✓ Set {ActionSetConstants.IniFiles.ScrollEdgeSpeedKey}=1.0 for {gameType}");
+ details.Add($"✓ Set {ActionSetConstants.IniFiles.ScrollEdgeAccelerationKey}=0.0 for {gameType}");
+
+ await _gameSettingsService.SaveOptionsAsync(gameType, options);
+
+ details.Add($"✓ Saved Options.ini: {optionsPath}");
+ _logger.LogInformation("Successfully applied edge scrolling fix for {GameType}", gameType);
+ }
+ catch (Exception ex)
+ {
+ details.Add($"✗ Error applying edge scrolling for {gameType}: {ex.Message}");
+ _logger.LogError(ex, "Error applying edge scrolling fix for {GameType}", gameType);
+ }
+
+ return details;
+ }
+}
diff --git a/GenHub/GenHub.Windows/Features/ActionSets/Fixes/ExpandedLANLobbyMenu.cs b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/ExpandedLANLobbyMenu.cs
new file mode 100644
index 000000000..9fdaa7f16
--- /dev/null
+++ b/GenHub/GenHub.Windows/Features/ActionSets/Fixes/ExpandedLANLobbyMenu.cs
@@ -0,0 +1,99 @@
+namespace GenHub.Windows.Features.ActionSets.Fixes;
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using GenHub.Core.Features.ActionSets;
+using GenHub.Core.Models.GameInstallations;
+using Microsoft.Extensions.Logging;
+
+///
+/// Fix that provides guidance for expanded LAN lobby menu.
+/// This fix explains how to access and use LAN features in Generals and Zero Hour.
+///
+public class ExpandedLANLobbyMenu(ILogger logger) : BaseActionSet(logger)
+{
+ private readonly ILogger _logger = logger;
+ private readonly string _markerPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GenHub", "sub_markers", "ExpandedLANLobbyMenu.done");
+
+ ///
+ public override string Id => "ExpandedLANLobbyMenu";
+
+ ///
+ public override string Title => "Expanded LAN Lobby Menu";
+
+ ///
+ public override bool IsCoreFix => false;
+
+ ///
+ public override bool IsCrucialFix => false;
+
+ ///
+ public override Task IsApplicableAsync(GameInstallation installation)
+ {
+ return Task.FromResult(installation.HasGenerals || installation.HasZeroHour);
+ }
+
+ ///