diff --git a/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs b/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs
index 9cf76b1a9..9252a0015 100644
--- a/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs
@@ -44,7 +44,13 @@ protected virtual ILoadoutSynchronizer MakeSynchronizer(IServiceProvider provide
///
public abstract string Name { get; }
-
+
+ ///
+ public abstract SupportType SupportType { get; }
+
+ ///
+ public virtual HashSet Features { get; } = [];
+
///
public abstract GameId GameId { get; }
diff --git a/src/Abstractions/NexusMods.Abstractions.Games/Feature.cs b/src/Abstractions/NexusMods.Abstractions.Games/Feature.cs
new file mode 100644
index 000000000..a109abf03
--- /dev/null
+++ b/src/Abstractions/NexusMods.Abstractions.Games/Feature.cs
@@ -0,0 +1,77 @@
+using JetBrains.Annotations;
+
+namespace NexusMods.Abstractions.Games;
+
+///
+/// Represents a feature a game can support.
+///
+/// Description
+[PublicAPI]
+public readonly record struct Feature(string Description)
+{
+ ///
+ /// Identifier.
+ ///
+ public readonly Guid Id = Guid.NewGuid();
+
+ ///
+ public override int GetHashCode() => Id.GetHashCode();
+
+ ///
+ /// Equality.
+ ///
+ public bool Equals(Feature? other) => other is not null && Id.Equals(other.Value.Id);
+}
+
+///
+/// Status of a feature.
+///
+/// The feature.
+/// Whether the feature is implemented or not.
+[PublicAPI]
+public readonly record struct FeatureStatus(Feature Feature, bool IsImplemented);
+
+///
+/// Status of all game features.
+///
+[PublicAPI]
+public enum GameFeatureStatus
+{
+ ///
+ /// Default value.
+ ///
+ None = 0,
+
+ ///
+ /// The minimum amount of features is implemented.
+ ///
+ Minimal = 1,
+
+ ///
+ /// All features are implemented.
+ ///
+ Full = 2,
+}
+
+///
+/// Extension methods.
+///
+[PublicAPI]
+public static class FeatureExtensions
+{
+ ///
+ ///
+ ///
+ public static GameFeatureStatus ToStatus(this HashSet features)
+ {
+ var implemented = features.Count(status => status.IsImplemented);
+ var total = features.Count;
+ if (implemented == total) return GameFeatureStatus.Full;
+
+ return implemented switch
+ {
+ 0 => GameFeatureStatus.None,
+ _ => GameFeatureStatus.Minimal,
+ };
+ }
+}
diff --git a/src/Abstractions/NexusMods.Abstractions.Games/Features.cs b/src/Abstractions/NexusMods.Abstractions.Games/Features.cs
new file mode 100644
index 000000000..392fcc9c4
--- /dev/null
+++ b/src/Abstractions/NexusMods.Abstractions.Games/Features.cs
@@ -0,0 +1,19 @@
+using JetBrains.Annotations;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace NexusMods.Abstractions.Games;
+
+///
+/// List of features.
+///
+[PublicAPI]
+public static class BaseFeatures
+{
+ public static readonly Feature GameLocatable = new(Description: "The game can be located.");
+
+ public static readonly Feature HasInstallers = new(Description: "The extension provides mod installers.");
+
+ public static readonly Feature HasDiagnostics = new(Description: "The extension provides diagnostics.");
+
+ public static readonly Feature HasLoadOrder = new(Description: "The extension provides load-order support.");
+}
diff --git a/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs b/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs
index 418bcbfe1..9815ec9fe 100644
--- a/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs
@@ -13,6 +13,10 @@ namespace NexusMods.Abstractions.Games;
///
public interface IGame : ILocatableGame
{
+ SupportType SupportType { get; }
+ HashSet Features { get; }
+ GameFeatureStatus FeatureStatus => Features.ToStatus();
+
///
/// Stream factory for the game's icon, must be square but need not be small.
///
diff --git a/src/Abstractions/NexusMods.Abstractions.Games/SupportLevel.cs b/src/Abstractions/NexusMods.Abstractions.Games/SupportLevel.cs
new file mode 100644
index 000000000..255956cee
--- /dev/null
+++ b/src/Abstractions/NexusMods.Abstractions.Games/SupportLevel.cs
@@ -0,0 +1,25 @@
+using JetBrains.Annotations;
+
+namespace NexusMods.Abstractions.Games;
+
+///
+/// Game support type.
+///
+[PublicAPI]
+public enum SupportType
+{
+ ///
+ /// The game is unsupported.
+ ///
+ Unsupported = 0,
+
+ ///
+ /// The game is officially supported and the extension maintained by Nexus Mods.
+ ///
+ Official = 1,
+
+ ///
+ /// The game is supported and the extension is maintained by the community.
+ ///
+ Community = 2,
+}
diff --git a/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs b/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
index b26c249e4..6fc8763a0 100644
--- a/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
+++ b/src/Games/NexusMods.Games.Larian/BaldursGate3/BaldursGate3.cs
@@ -27,6 +27,15 @@ public class BaldursGate3 : AGame, ISteamGame, IGogGame
public IEnumerable SteamIds => [1086940u];
public IEnumerable GogIds => [1456460669];
public override GameId GameId => GameId.From(3474);
+ public override SupportType SupportType => SupportType.Official;
+
+ public override HashSet Features { get; } =
+ [
+ new(BaseFeatures.GameLocatable, IsImplemented: true),
+ new(BaseFeatures.HasInstallers, IsImplemented: true),
+ new(BaseFeatures.HasDiagnostics, IsImplemented: true),
+ new(BaseFeatures.HasLoadOrder, IsImplemented: false),
+ ];
public BaldursGate3(IServiceProvider provider) : base(provider)
{
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs
index 8451f2a00..5baecdf55 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs
@@ -35,7 +35,16 @@ public sealed class MountAndBlade2Bannerlord : AGame, ISteamGame, IGogGame, IEpi
public override string Name => DisplayName;
public override GameId GameId => GameIdStatic;
-
+ public override SupportType SupportType => SupportType.Official;
+
+ public override HashSet Features { get; } =
+ [
+ new(BaseFeatures.GameLocatable, IsImplemented: true),
+ new(BaseFeatures.HasInstallers, IsImplemented: true),
+ new(BaseFeatures.HasDiagnostics, IsImplemented: false),
+ new(BaseFeatures.HasLoadOrder, IsImplemented: false),
+ ];
+
public IEnumerable SteamIds => [261550u];
public IEnumerable GogIds => [1802539526, 1564781494];
public IEnumerable EpicCatalogItemId => ["Chickadee"];
diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Cyberpunk2077Game.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Cyberpunk2077Game.cs
index a543ab183..d6fc6eb97 100644
--- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Cyberpunk2077Game.cs
+++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Cyberpunk2077Game.cs
@@ -34,6 +34,16 @@ protected override ILoadoutSynchronizer MakeSynchronizer(IServiceProvider provid
public override string Name => "Cyberpunk 2077";
public override GameId GameId => GameIdStatic;
+ public override SupportType SupportType => SupportType.Official;
+
+ public override HashSet Features { get; } =
+ [
+ new(BaseFeatures.GameLocatable, IsImplemented: true),
+ new(BaseFeatures.HasInstallers, IsImplemented: true),
+ new(BaseFeatures.HasDiagnostics, IsImplemented: true),
+ new(BaseFeatures.HasLoadOrder, IsImplemented: false),
+ ];
+
public override GamePath GetPrimaryFile(GameStore store) => new(LocationId.Game, "bin/x64/Cyberpunk2077.exe");
protected override IReadOnlyDictionary GetLocations(IFileSystem fileSystem,
GameLocatorResult installation)
diff --git a/src/Games/NexusMods.Games.StardewValley/StardewValley.cs b/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
index 695f2a769..7513d4aee 100644
--- a/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
+++ b/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
@@ -32,6 +32,15 @@ public class StardewValley : AGame, ISteamGame, IGogGame, IXboxGame
public override string Name => "Stardew Valley";
public override GameId GameId => GameId.From(1303);
+ public override SupportType SupportType => SupportType.Official;
+
+ public override HashSet Features { get; } =
+ [
+ new(BaseFeatures.GameLocatable, IsImplemented: true),
+ new(BaseFeatures.HasInstallers, IsImplemented: true),
+ new(BaseFeatures.HasDiagnostics, IsImplemented: true),
+ ];
+
public StardewValley(
IOSInformation osInformation,
IEnumerable gameLocators,
diff --git a/src/Games/NexusMods.Games.StardewValley/StardewValleyLoadoutSynchronizer.cs b/src/Games/NexusMods.Games.StardewValley/StardewValleyLoadoutSynchronizer.cs
index f2936049a..1a9d4669f 100644
--- a/src/Games/NexusMods.Games.StardewValley/StardewValleyLoadoutSynchronizer.cs
+++ b/src/Games/NexusMods.Games.StardewValley/StardewValleyLoadoutSynchronizer.cs
@@ -39,8 +39,6 @@ protected override ValueTask MoveNewFilesToMods(Loadout.ReadOnly loadout, IEnume
foreach (var newFile in newFiles)
{
- GamePath gamePath;
-
if (!IsModFile(newFile.LoadoutItemWithTargetPath.TargetPath, out var modDirectoryName))
{
continue;
diff --git a/tests/NexusMods.StandardGameLocators.TestHelpers/StubbedGames/StubbedGame.cs b/tests/NexusMods.StandardGameLocators.TestHelpers/StubbedGames/StubbedGame.cs
index 48521975a..61d5a2b74 100644
--- a/tests/NexusMods.StandardGameLocators.TestHelpers/StubbedGames/StubbedGame.cs
+++ b/tests/NexusMods.StandardGameLocators.TestHelpers/StubbedGames/StubbedGame.cs
@@ -28,6 +28,8 @@ public class StubbedGame : AGame, IEADesktopGame, IEpicGame, IOriginGame, ISteam
public override string Name => "Stubbed Game";
public override GameId GameId => GameId.From(uint.MaxValue);
+ public override SupportType SupportType => SupportType.Unsupported;
+
private readonly IServiceProvider _serviceProvider;
public StubbedGame(ILogger logger, IEnumerable locators,
IFileSystem fileSystem, IServiceProvider provider) : base(provider)