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)