diff --git a/Directory.Packages.props b/Directory.Packages.props index d4ae4a8f..13ae8f63 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,54 +1,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/GameFinder.sln.DotSettings b/GameFinder.sln.DotSettings index 44794c17..87b28617 100644 --- a/GameFinder.sln.DotSettings +++ b/GameFinder.sln.DotSettings @@ -10,6 +10,7 @@ True True True + True True True True diff --git a/other/GameFinder.Example/Program.cs b/other/GameFinder.Example/Program.cs index dfaa61e5..e762c566 100644 --- a/other/GameFinder.Example/Program.cs +++ b/other/GameFinder.Example/Program.cs @@ -1,30 +1,19 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Runtime.InteropServices; using CommandLine; using GameFinder.Common; using GameFinder.RegistryUtils; -using GameFinder.StoreHandlers.EADesktop; -using GameFinder.StoreHandlers.EADesktop.Crypto; -using GameFinder.StoreHandlers.EADesktop.Crypto.Windows; -using GameFinder.StoreHandlers.EGS; using GameFinder.StoreHandlers.GOG; using GameFinder.StoreHandlers.Origin; -using GameFinder.StoreHandlers.Steam; -using GameFinder.StoreHandlers.Xbox; -using GameFinder.Wine; -using GameFinder.Wine.Bottles; using Microsoft.Extensions.Logging; using NexusMods.Paths; using NLog; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; -using OneOf; using FileSystem = NexusMods.Paths.FileSystem; -using IFileSystem = NexusMods.Paths.IFileSystem; using ILogger = Microsoft.Extensions.Logging.ILogger; [assembly: ExcludeFromCodeCoverage] @@ -32,8 +21,6 @@ namespace GameFinder.Example; public static class Program { - private static NLogLoggerProvider _provider = null!; - public static void Main(string[] args) { var config = new LoggingConfiguration(); @@ -51,6 +38,7 @@ public static void Main(string[] args) CompileRegex = true, ForegroundColor = ConsoleOutputColor.Gray, }, + new ConsoleWordHighlightingRule("TRACE", ConsoleOutputColor.Gray, ConsoleOutputColor.NoChange), new ConsoleWordHighlightingRule("DEBUG", ConsoleOutputColor.Gray, ConsoleOutputColor.NoChange), new ConsoleWordHighlightingRule("INFO", ConsoleOutputColor.Cyan, ConsoleOutputColor.NoChange), new ConsoleWordHighlightingRule("ERROR", ConsoleOutputColor.Red, ConsoleOutputColor.NoChange), @@ -68,166 +56,32 @@ public static void Main(string[] args) config.AddRuleForAllLevels(fileTarget); LogManager.Configuration = config; - _provider = new NLogLoggerProvider(); - - var logger = _provider.CreateLogger(nameof(Program)); + ILoggerFactory loggerFactory = new NLogLoggerFactory(); Parser.Default .ParseArguments(args) - .WithParsed(x => Run(x, logger)); + .WithParsed(x => Run(x, loggerFactory)); } - private static void Run(Options options, ILogger logger) + private static void Run(Options options, ILoggerFactory loggerFactory) { var realFileSystem = FileSystem.Shared; var logFile = realFileSystem.GetKnownPath(KnownPath.CurrentDirectory).Combine("log.log"); if (realFileSystem.FileExists(logFile)) realFileSystem.DeleteFile(logFile); + var logger = loggerFactory.CreateLogger(nameof(Program)); logger.LogInformation("Operating System: {OSDescription}", RuntimeInformation.OSDescription); - if (OperatingSystem.IsWindows()) - { - var windowsRegistry = WindowsRegistry.Shared; - if (options.Steam) RunSteamHandler(realFileSystem, windowsRegistry); - if (options.GOG) RunGOGHandler(windowsRegistry, realFileSystem); - if (options.EGS) RunEGSHandler(windowsRegistry, realFileSystem); - if (options.Origin) RunOriginHandler(realFileSystem); - if (options.Xbox) RunXboxHandler(realFileSystem); - if (options.EADesktop) - { - var hardwareInfoProvider = new HardwareInfoProvider(); - var decryptionKey = Decryption.CreateDecryptionKey(new HardwareInfoProvider()); - var sDecryptionKey = Convert.ToHexString(decryptionKey).ToLower(CultureInfo.InvariantCulture); - logger.LogDebug("EA Decryption Key: {DecryptionKey}", sDecryptionKey); - - RunEADesktopHandler(realFileSystem, hardwareInfoProvider); - } - } - - if (OperatingSystem.IsLinux()) - { - if (options.Steam) RunSteamHandler(realFileSystem, registry: null); - var winePrefixes = new List(); - - if (options.Wine) - { - var prefixManager = new DefaultWinePrefixManager(realFileSystem); - winePrefixes.AddRange(LogWinePrefixes(prefixManager, _provider.CreateLogger("Wine"))); - } - - if (options.Bottles) - { - var prefixManager = new BottlesWinePrefixManager(realFileSystem); - winePrefixes.AddRange(LogWinePrefixes(prefixManager, _provider.CreateLogger("Bottles"))); - } - - foreach (var winePrefix in winePrefixes) - { - var wineFileSystem = winePrefix.CreateOverlayFileSystem(realFileSystem); - var wineRegistry = winePrefix.CreateRegistry(realFileSystem); - - if (options.GOG) RunGOGHandler(wineRegistry, wineFileSystem); - if (options.EGS) RunEGSHandler(wineRegistry, wineFileSystem); - if (options.Origin) RunOriginHandler(wineFileSystem); - if (options.Xbox) RunXboxHandler(wineFileSystem); - } - } - - if (OperatingSystem.IsMacOS()) - { - if (options.Steam) - RunSteamHandler(realFileSystem, null); - } - } - - private static void RunGOGHandler(IRegistry registry, IFileSystem fileSystem) - { - var logger = _provider.CreateLogger(nameof(GOGHandler)); - var handler = new GOGHandler(registry, fileSystem); - LogGamesAndErrors(handler.FindAllGames(), logger); - } + // TODO: Linux and macOS + if (!OperatingSystem.IsWindows()) return; - private static void RunEGSHandler(IRegistry registry, IFileSystem fileSystem) - { - var logger = _provider.CreateLogger(nameof(EGSHandler)); - var handler = new EGSHandler(registry, fileSystem); - LogGamesAndErrors(handler.FindAllGames(), logger); - } + var gameFinder = GameFinderBuilder.Create( + loggerFactory, + new OriginHandler(loggerFactory, realFileSystem), + new GOGHandler(loggerFactory, realFileSystem, new WindowsRegistry()) + ); - private static void RunOriginHandler(IFileSystem fileSystem) - { - var logger = _provider.CreateLogger(nameof(OriginHandler)); - var handler = new OriginHandler(fileSystem); - LogGamesAndErrors(handler.FindAllGames(), logger); - } - - private static void RunEADesktopHandler( - IFileSystem fileSystem, - IHardwareInfoProvider hardwareInfoProvider) - { - var logger = _provider.CreateLogger(nameof(EADesktopHandler)); - var handler = new EADesktopHandler(fileSystem, hardwareInfoProvider); - LogGamesAndErrors(handler.FindAllGames(), logger); - } - - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:RequiresUnreferencedCodeAttribute", - Justification = "Required types are preserved using TrimmerRootDescriptor file.")] - private static void RunXboxHandler(IFileSystem fileSystem) - { - var logger = _provider.CreateLogger(nameof(XboxHandler)); - var handler = new XboxHandler(fileSystem); - LogGamesAndErrors(handler.FindAllGames(), logger); - } - - private static void RunSteamHandler(IFileSystem fileSystem, IRegistry? registry) - { - var logger = _provider.CreateLogger(nameof(SteamHandler)); - var handler = new SteamHandler(fileSystem, registry); - LogGamesAndErrors(handler.FindAllGames(), logger, game => - { - if (!OperatingSystem.IsLinux()) return; - var protonPrefix = game.GetProtonPrefix(); - if (protonPrefix is null) return; - logger.LogInformation("Proton Directory for this game: {}", protonPrefix.ProtonDirectory.GetFullPath()); - }); - } - - private static List LogWinePrefixes(IWinePrefixManager prefixManager, ILogger logger) - where TWinePrefix : AWinePrefix - { - var res = new List(); - - foreach (var result in prefixManager.FindPrefixes()) - { - result.Switch(prefix => - { - logger.LogInformation("Found wine prefix at {PrefixConfigurationDirectory}", prefix.ConfigurationDirectory); - res.Add(prefix); - }, error => - { - logger.LogError("{Error}", error); - }); - } - - return res; - } - - private static void LogGamesAndErrors(IEnumerable> results, ILogger logger, Action? action = null) - where TGame : class - { - foreach (var result in results) - { - result.Switch(game => - { - logger.LogInformation("Found {Game}", game); - action?.Invoke(game); - }, error => - { - logger.LogError("{Error}", error); - }); - } + var foundGames = gameFinder.FindAllGames(); } } diff --git a/src/GameFinder.Common/AHandler.cs b/src/GameFinder.Common/AHandler.cs deleted file mode 100644 index 594acc66..00000000 --- a/src/GameFinder.Common/AHandler.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using JetBrains.Annotations; -using OneOf; - -namespace GameFinder.Common; - -/// -/// Base class for store handlers. -/// -/// -[PublicAPI] -public abstract class AHandler -{ - /// - /// Finds all instances installed with this store. - /// - /// - /// - [MustUseReturnValue] - [System.Diagnostics.Contracts.Pure] - public abstract IEnumerable> FindAllInterfaceGames(); -} - -/// -/// Generic base class for store handlers. -/// -/// -/// -/// -[PublicAPI] -public abstract class AHandler : AHandler - where TGame : class, IGame - where TId : notnull -{ - /// - /// Method that accepts a and returns the - /// of it. This is useful for constructing - /// key-based data types like . - /// - public abstract Func IdSelector { get; } - - /// - /// Custom equality comparer for . This is useful - /// for constructing key-based data types like . - /// - public abstract IEqualityComparer? IdEqualityComparer { get; } - - /// - [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")] - public override IEnumerable> FindAllInterfaceGames() - { - foreach (var res in FindAllGames()) - { - yield return res.MapT0(x => (IGame)x); - } - } - - /// - /// Finds all games installed with this store. - /// - /// - [MustUseReturnValue] - [System.Diagnostics.Contracts.Pure] - public abstract IEnumerable> FindAllGames(); - - /// - /// Calls and converts the result into a dictionary where - /// the key is the id of the game. - /// - /// - /// - [MustUseReturnValue] - [System.Diagnostics.Contracts.Pure] - public IReadOnlyDictionary FindAllGamesById(out ErrorMessage[] errors) - { - var (games, allErrors) = FindAllGames().SplitResults(); - errors = allErrors; - - return games.CustomToDictionary(IdSelector, game => game, IdEqualityComparer ?? EqualityComparer.Default); - } - - /// - /// Wrapper around if you just need to find one game. - /// - /// - /// - /// - [MustUseReturnValue] - [System.Diagnostics.Contracts.Pure] - public TGame? FindOneGameById(TId id, out ErrorMessage[] errors) - { - var allGames = FindAllGamesById(out errors); - return allGames.TryGetValue(id, out var game) ? game : null; - } -} diff --git a/src/GameFinder.Common/ErrorMessage.cs b/src/GameFinder.Common/ErrorMessage.cs deleted file mode 100644 index 3abbdc3e..00000000 --- a/src/GameFinder.Common/ErrorMessage.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Diagnostics; -using JetBrains.Annotations; - -namespace GameFinder.Common; - -/// -/// Represents a generic error. -/// -[PublicAPI] -[DebuggerDisplay("{Message}")] -public readonly struct ErrorMessage -{ - /// - /// The error message. - /// - public readonly string Message; - - /// - /// Constructor taking a message. - /// - /// - public ErrorMessage(string message) - { - Message = message; - } - - /// - /// Constructor taking an exception. - /// - /// - public ErrorMessage(Exception e) - { - Message = e.ToString(); - } - - /// - /// Constructor taking an exception and a message. - /// - /// - /// - public ErrorMessage(Exception e, string message) - { - Message = $"{message}:\n{e}"; - } - - /// - public override string ToString() => Message; - - /// - /// Converts to a . - /// - public static explicit operator string(ErrorMessage error) => error.Message; - - /// - /// Creates a new from a . - /// - /// - /// - public static implicit operator ErrorMessage(string message) => new(message); - - /// - public override bool Equals(object? obj) - { - return obj switch - { - null => false, - string s => string.Equals(Message, s, StringComparison.InvariantCulture), - ErrorMessage errorMessage => string.Equals(Message, errorMessage.Message, StringComparison.InvariantCulture), - _ => false - }; - } - - /// - public override int GetHashCode() => Message.GetHashCode(StringComparison.InvariantCulture); -} diff --git a/src/GameFinder.Common/Extensions.cs b/src/GameFinder.Common/Extensions.cs deleted file mode 100644 index 9ffdcc41..00000000 --- a/src/GameFinder.Common/Extensions.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using JetBrains.Annotations; -using OneOf; - -namespace GameFinder.Common; - -/// -/// Utility extensions for GameFinder. -/// -[PublicAPI] -public static class Extensions -{ - /// - /// Fully enumerates and splits the results into - /// two separate arrays. - /// - /// - /// - /// - public static (TGame[] games, ErrorMessage[] errors) SplitResults( - [InstantHandle] this IEnumerable> results) - where TGame : class, IGame - { - var allResults = results.ToArray(); - - var games = allResults - .Where(x => x.IsT0) - .Select(x => x.AsT0) - .ToArray(); - - var errors = allResults - .Where(x => x.IsT1) - .Select(x => x.AsT1) - .ToArray(); - - return (games, errors); - } - - /// - /// Custom - /// function that skips duplicate keys. - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static IReadOnlyDictionary CustomToDictionary( - [InstantHandle] this IEnumerable source, Func keySelector, - Func elementSelector, IEqualityComparer? comparer = null) - where TKey : notnull - { - var dictionary = new Dictionary(comparer); - - foreach (var element in source) - { - var key = keySelector(element); - if (dictionary.ContainsKey(key)) continue; - - dictionary.Add(key, elementSelector(element)); - } - - return dictionary; - } - - /// - /// Returns true if the result is of type . - /// - /// - /// - /// - public static bool IsGame(this OneOf result) - where TGame : class, IGame - { - return result.IsT0; - } - - /// - /// Returns true if the result is of type . - /// - /// - /// - /// - public static bool IsError(this OneOf result) - { - return result.IsT1; - } - - /// - /// Returns the part of the result. This can throw if - /// the result is not of type . Use - /// instead. - /// - /// - /// - /// - /// - /// Thrown when the result is not of type . - /// - public static TGame AsGame(this OneOf result) - where TGame : class, IGame - { - return result.AsT0; - } - - /// - /// Returns the part of the result. This can - /// throw if the result is not of type . Use - /// instead. - /// - /// - /// - /// - /// - /// Thrown when the result is not of type . - /// - public static ErrorMessage AsError(this OneOf result) - { - return result.AsT1; - } - - /// - /// Returns the part of the result using the try-get - /// pattern. - /// - /// - /// - /// - /// - public static bool TryGetGame(this OneOf result, - [MaybeNullWhen(false)] out TGame game) - where TGame : class, IGame - { - game = null; - if (!result.IsGame()) return false; - - game = result.AsGame(); - return true; - } - - /// - /// Returns the part of the result using the - /// try-get pattern. - /// - /// - /// - /// - /// - public static bool TryGetError(this OneOf result, out ErrorMessage error) - { - error = default; - if (!result.IsError()) return false; - - error = result.AsError(); - return true; - } -} diff --git a/src/GameFinder.Common/GameFinder.Common.csproj b/src/GameFinder.Common/GameFinder.Common.csproj index 6e3a8677..e916632d 100644 --- a/src/GameFinder.Common/GameFinder.Common.csproj +++ b/src/GameFinder.Common/GameFinder.Common.csproj @@ -5,8 +5,19 @@ + - - + + + + + IGame.cs + + + IGameFinder.cs + + + IGame.cs + diff --git a/src/GameFinder.Common/GameFinder.cs b/src/GameFinder.Common/GameFinder.cs new file mode 100644 index 00000000..ef524cc4 --- /dev/null +++ b/src/GameFinder.Common/GameFinder.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace GameFinder.Common; + +/// +/// Builder for creating instances of . +/// +[PublicAPI] +public static class GameFinderBuilder +{ + /// + /// Creates an instance of . + /// + public static IGameFinder Create( + ILoggerFactory loggerFactory, + params IHandler[] handlers) + { + return new GameFinder(loggerFactory.CreateLogger(), handlers); + } +} + +internal class GameFinder : IGameFinder +{ + private readonly ILogger _logger; + private readonly IHandler[] _handlers; + + public GameFinder(ILogger logger, IHandler[] handlers) + { + _logger = logger; + _handlers = handlers; + } + + private IReadOnlyList? UseHandler(IHandler handler) + { + try + { + LogMessages.UsingHandler(_logger, handler.GetType()); + var foundGames = handler.Search(); + LogMessages.HandlerFoundGames(_logger, handler.GetType(), foundGames.Count); + + return foundGames; + } + catch (Exception e) + { + LogMessages.ExceptionWhileUsingHandler(_logger, e, handler.GetType()); + return null; + } + } + + private IReadOnlyList? UseHandler(IHandler handler) where TGame : IGame + { + try + { + LogMessages.UsingHandler(_logger, handler.GetType()); + var foundGames = handler.Search(); + LogMessages.HandlerFoundGames(_logger, handler.GetType(), foundGames.Count); + + return foundGames; + } + catch (Exception e) + { + LogMessages.ExceptionWhileUsingHandler(_logger, e, handler.GetType()); + return null; + } + } + + public IReadOnlyList FindAllGames() + { + LogMessages.FindingAllGames(_logger, _handlers.Length); + var games = new List(); + + foreach (var handler in _handlers) + { + var foundGames = UseHandler(handler); + if (foundGames is null) continue; + games.AddRange(foundGames); + } + + LogMessages.FoundGames(_logger, games.Count); + return games; + } + + public IReadOnlyList FindAllGames() where TGame : IGame + { + LogMessages.FindingAllGames(_logger, _handlers.Length); + var games = new List(); + + foreach (var handler in _handlers) + { + if (handler is not IHandler gameHandler) continue; + + var foundGames = UseHandler(gameHandler); + if (foundGames is null) continue; + games.AddRange(foundGames); + } + + LogMessages.FoundGames(_logger, games.Count); + return games; + } + + public bool TryFindGameWithId(IId id, [NotNullWhen(true)] out IGame? game) + { + LogMessages.FindingGameWithId(_logger, id, _handlers.Length); + + foreach (var handler in _handlers) + { + var foundGames = UseHandler(handler); + if (foundGames is null) continue; + + foreach (var foundGame in foundGames) + { + if (!foundGame.Id.Equals(id)) continue; + + LogMessages.FoundGameWithId(_logger, id, foundGame); + game = foundGame; + return true; + } + } + + LogMessages.UnableToFindGameWithId(_logger, id); + game = default; + return false; + } + + public bool TryFindGameWithId(TId id, [NotNullWhen(true)] out TGame? game) + where TGame : IGame + where TId : IId + { + LogMessages.FindingGameWithId(_logger, id, _handlers.Length); + + foreach (var handler in _handlers) + { + if (handler is not IHandler gameHandler) continue; + + var foundGames = UseHandler(gameHandler); + if (foundGames is null) continue; + + foreach (var foundGame in foundGames) + { + if (!foundGame.Id.Equals(id)) continue; + + LogMessages.FoundGameWithId(_logger, id, foundGame); + game = foundGame; + return true; + } + } + + LogMessages.UnableToFindGameWithId(_logger, id); + game = default; + return false; + } + + public bool TryFindGameWithManyIds(IId[] ids, [NotNullWhen(true)] out IGame? game) + { + LogMessages.FindingGameWithIds(_logger, ids, _handlers.Length); + + foreach (var handler in _handlers) + { + var foundGames = UseHandler(handler); + if (foundGames is null) continue; + + foreach (var foundGame in foundGames) + { + if (Array.IndexOf(ids, foundGame.Id) == -1) continue; + + LogMessages.FoundGameWithId(_logger, foundGame.Id, foundGame); + game = foundGame; + return true; + } + } + + LogMessages.UnableToFindGameWithIds(_logger, ids); + game = default; + return false; + } + + public bool TryFindGameWithManyIds(TId[] ids, [NotNullWhen(true)] out TGame? game) + where TGame : IGame + where TId : IId + { + var logIds = ids.Cast().ToArray(); + LogMessages.FindingGameWithIds(_logger, logIds, _handlers.Length); + + foreach (var handler in _handlers) + { + if (handler is not IHandler gameHandler) continue; + + var foundGames = UseHandler(gameHandler); + if (foundGames is null) continue; + + foreach (var foundGame in foundGames) + { + if (Array.IndexOf(ids, foundGame.Id) == -1) continue; + + LogMessages.FoundGameWithId(_logger, foundGame.Id, foundGame); + game = foundGame; + return true; + } + } + + LogMessages.UnableToFindGameWithIds(_logger, logIds); + game = default; + return false; + } +} diff --git a/src/GameFinder.Common/IGame.cs b/src/GameFinder.Common/IGame.cs index 2921dd27..7e1806dd 100644 --- a/src/GameFinder.Common/IGame.cs +++ b/src/GameFinder.Common/IGame.cs @@ -1,9 +1,43 @@ using JetBrains.Annotations; +using NexusMods.Paths; namespace GameFinder.Common; /// -/// Interface for games. +/// Represents a game found by GameFinder. /// +/// +/// [PublicAPI] -public interface IGame { } +public interface IGame +{ + /// + /// Gets the ID of the game. + /// + /// + IId Id { get; } + + /// + /// Gets the path to the game. + /// + /// + /// This is not guaranteed to point to a directory. Whether this + /// points to a file or a directory is up to the implementation. + /// + AbsolutePath Path { get; } +} + +/// +/// Represents a game with a concrete ID type. +/// +public interface IGame : IGame + where TId : IId +{ + /// + IId IGame.Id => Id; + + /// + /// Gets the ID of the game. + /// + new TId Id { get; } +} diff --git a/src/GameFinder.Common/IGameFinder.cs b/src/GameFinder.Common/IGameFinder.cs new file mode 100644 index 00000000..3d9858c1 --- /dev/null +++ b/src/GameFinder.Common/IGameFinder.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace GameFinder.Common; + +/// +/// Finds games. +/// +[PublicAPI] +public interface IGameFinder +{ + /// + /// Uses all registered handlers to find all installed games. + /// + /// + [MustUseReturnValue] + IReadOnlyList FindAllGames(); + + /// + /// Uses all registered handlers that implement with + /// to find all installed games of type . + /// + [MustUseReturnValue] + IReadOnlyList FindAllGames() + where TGame : IGame; + + /// + /// Tries to find a single game with the provided ID. + /// + /// + bool TryFindGameWithId(IId id, [NotNullWhen(true)] out IGame? game); + + /// + /// Tries to find a single game of type with the provided ID of type . + /// + /// + /// Similar to , this method only uses registered handlers that implement + /// with . + /// + /// + bool TryFindGameWithId(TId id, [NotNullWhen(true)] out TGame? game) + where TGame : IGame + where TId : IId; + + /// + /// Tries to find a single game which ID is contained in . + /// + bool TryFindGameWithManyIds(IId[] ids, [NotNullWhen(true)] out IGame? game); + + /// + /// Tries to find a single game of type which ID of type + /// is contained in . + /// + bool TryFindGameWithManyIds(TId[] ids, [NotNullWhen(true)] out TGame? game) + where TGame : IGame + where TId : IId; +} diff --git a/src/GameFinder.Common/IGameName.cs b/src/GameFinder.Common/IGameName.cs new file mode 100644 index 00000000..3da56b21 --- /dev/null +++ b/src/GameFinder.Common/IGameName.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; + +namespace GameFinder.Common; + +/// +/// Represents a game with a name. +/// +/// +[PublicAPI] +public interface IGameName +{ + /// + /// Gets the name of the game. + /// + /// + /// This can be used as a display string. + /// + string Name { get; } +} diff --git a/src/GameFinder.Common/IHandler.cs b/src/GameFinder.Common/IHandler.cs new file mode 100644 index 00000000..cc47d476 --- /dev/null +++ b/src/GameFinder.Common/IHandler.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace GameFinder.Common; + +/// +/// Represents a handler that provides functionality for searching +/// for installed games. +/// +[PublicAPI] +public interface IHandler +{ + /// + /// Searches for installed games. + /// + [MustUseReturnValue] + IReadOnlyList Search(); +} + +/// +/// Represents a generic handler. +/// +[PublicAPI] +public interface IHandler : IHandler + where TGame : IGame +{ + /// + IReadOnlyList IHandler.Search() + { + return (IReadOnlyList)Search(); + } + + /// + /// Searches for installed games. + /// + [MustUseReturnValue] + new IReadOnlyList Search(); +} diff --git a/src/GameFinder.Common/IId.cs b/src/GameFinder.Common/IId.cs new file mode 100644 index 00000000..428f5187 --- /dev/null +++ b/src/GameFinder.Common/IId.cs @@ -0,0 +1,8 @@ +using System; + +namespace GameFinder.Common; + +/// +/// Represents an ID. +/// +public interface IId : IEquatable { } diff --git a/src/GameFinder.Common/LogMessages.cs b/src/GameFinder.Common/LogMessages.cs new file mode 100644 index 00000000..87261793 --- /dev/null +++ b/src/GameFinder.Common/LogMessages.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace GameFinder.Common; + +internal static partial class LogMessages +{ + [LoggerMessage( + EventId = 0, EventName = nameof(FindingAllGames), + Level = LogLevel.Trace, + Message = "Finding all installed games using `{numHandlers}` Handler(s)" + )] + public static partial void FindingAllGames( + ILogger logger, + int numHandlers + ); + + [LoggerMessage( + EventId = 1, EventName = nameof(UsingHandler), + Level = LogLevel.Trace, + Message = "Using Handler `{handlerType}`" + )] + public static partial void UsingHandler( + ILogger logger, + Type handlerType + ); + + [LoggerMessage( + EventId = 2, EventName = nameof(HandlerFoundGames), + Level = LogLevel.Information, + Message = "Handler `{handlerType}` found `{numFoundGames}` game(s)" + )] + public static partial void HandlerFoundGames( + ILogger logger, + Type handlerType, + int numFoundGames + ); + + [LoggerMessage( + EventId = 3, EventName = nameof(ExceptionWhileUsingHandler), + Level = LogLevel.Warning, + Message = "Handler `{handlerType}` threw an exception while searching for games" + )] + public static partial void ExceptionWhileUsingHandler( + ILogger logger, + Exception e, + Type handlerType + ); + + [LoggerMessage( + EventId = 4, EventName = nameof(FoundGames), + Level = LogLevel.Information, + Message = "Found a total of `{numGames}` game(s)" + )] + public static partial void FoundGames( + ILogger logger, + int numGames + ); + + [LoggerMessage( + EventId = 5, EventName = nameof(FindingGameWithId), + Level = LogLevel.Trace, + Message = "Finding a game with ID `{gameId}` using `{numHandlers}` Handler(s)" + )] + public static partial void FindingGameWithId( + ILogger logger, + IId gameId, + int numHandlers + ); + + [LoggerMessage( + EventId = 6, EventName = nameof(FoundGameWithId), + Level = LogLevel.Information, + Message = "Found a game with ID `{gameId}`: `{game}`" + )] + public static partial void FoundGameWithId( + ILogger logger, + IId gameId, + IGame game + ); + + [LoggerMessage( + EventId = 7, EventName = nameof(UnableToFindGameWithId), + Level = LogLevel.Warning, + Message = "Didn't find a game with ID `{gameId}`" + )] + public static partial void UnableToFindGameWithId( + ILogger logger, + IId gameId + ); + + [LoggerMessage( + EventId = 8, EventName = nameof(FindingGameWithIds), + Level = LogLevel.Trace, + Message = "Finding a game with any ID matching `{ids}` using `{numHandlers}` Handler(s)" + )] + public static partial void FindingGameWithIds( + ILogger logger, + IId[] ids, + int numHandlers + ); + + [LoggerMessage( + EventId = 9, EventName = nameof(UnableToFindGameWithIds), + Level = LogLevel.Warning, + Message = "Didn't find a game with IDs `{ids}`" + )] + public static partial void UnableToFindGameWithIds( + ILogger logger, + IId[] ids + ); +} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Decryption.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/Decryption.cs deleted file mode 100644 index ae5bb0f2..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Decryption.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Security.Cryptography; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto; - -[SuppressMessage("ReSharper", "InconsistentNaming")] -internal static class Decryption -{ - private const string AllUsersGenericId = "allUsersGenericId"; - private const string IS = "IS"; - - private static readonly byte[] preComputedIV = - { - 0x84, 0xef, 0xc4, 0xb8, 0x36, 0x11, 0x9c, 0x20, 0x41, 0x93, 0x98, 0xc3, 0xf3, - 0xf2, 0xbc, 0xef, - }; - - public static byte[] CreateDecryptionKey(IHardwareInfoProvider hardwareInfoProvider) - { - var hardwareString = HardwareInformation.GenerateHardwareString(hardwareInfoProvider); - - var hardwareHash = Hashing.CalculateSHA1Hash(hardwareString); - var hashInput = AllUsersGenericId + IS + hardwareHash; - var key = Hashing.CalculateSHA3_256Hash(hashInput); - - return key; - } - - public static byte[] CreateDecryptionIV() - { - // NOTE: they calculate a 256-bit hash, but only use the first 16 bytes for AES - - // const string hashInput = AllUsersGenericId + IS; - // var iv = new byte[16]; - // - // var hash = Hashing.CalculateSHA3_256Hash(hashInput); - // var span = hash.AsSpan(); - // var slice = span[..16]; - // slice.CopyTo(iv.AsSpan()); - // - // return iv; - return preComputedIV; - } - - public static string DecryptFile(byte[] fileContents, byte[] key, byte[] iv) - { - // skips the first 64 bytes, because they contain a hash we don't need - using var cipherTextStream = new MemoryStream(fileContents, 64, fileContents.Length - 64, writable: false); - - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Key = key; - aes.IV = iv; - - using var decryptor = aes.CreateDecryptor(key, iv); - using var cryptoStream = new CryptoStream(cipherTextStream, decryptor, CryptoStreamMode.Read); - using var decryptionStream = new StreamReader(cryptoStream); - var plainText = decryptionStream.ReadToEnd(); - - return plainText; - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInfoProviderException.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInfoProviderException.cs deleted file mode 100644 index 57d51f60..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInfoProviderException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto; - -/// -/// Represents an Exception thrown by . -/// -[PublicAPI] -public class HardwareInfoProviderException : Exception -{ - private readonly string _msg; - private readonly Exception? _inner; - - internal HardwareInfoProviderException(string msg, Exception? inner) : base(msg, inner) - { - _msg = msg; - _inner = inner; - } - - /// - public override string ToString() - { - return _inner is null ? _msg : $"{_msg}\n{_inner}"; - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInformation.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInformation.cs deleted file mode 100644 index f97961e0..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/HardwareInformation.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto; - -internal static class HardwareInformation -{ - public static string GenerateHardwareString(IHardwareInfoProvider hardwareInfoProvider) - { - var sb = new StringBuilder(); - - var baseBoardManufacturer = hardwareInfoProvider.GetBaseBoardManufacturer(); - var baseBoardSerialNumber = hardwareInfoProvider.GetBaseBoardSerialNumber(); - var biosManufacturer = hardwareInfoProvider.GetBIOSManufacturer(); - var biosSerialNumber = hardwareInfoProvider.GetBIOSSerialNumber(); - var volumeSerialNumber = hardwareInfoProvider.GetVolumeSerialNumber(); - var videoControllerDeviceId = hardwareInfoProvider.GetVideoControllerDeviceId(); - var processorManufacturer = hardwareInfoProvider.GetProcessorManufacturer(); - var processorId = hardwareInfoProvider.GetProcessorId(); - var processorName = hardwareInfoProvider.GetProcessorName(); - - sb.Append(baseBoardManufacturer); - sb.Append(';'); - sb.Append(baseBoardSerialNumber); - sb.Append(';'); - sb.Append(biosManufacturer); - sb.Append(';'); - sb.Append(biosSerialNumber); - sb.Append(';'); - sb.Append(volumeSerialNumber); - sb.Append(';'); - sb.Append(videoControllerDeviceId); - sb.Append(';'); - sb.Append(processorManufacturer); - sb.Append(';'); - sb.Append(processorId); - sb.Append(';'); - sb.Append(processorName); - sb.Append(';'); - - return sb.ToString(); - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Hashing.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/Hashing.cs deleted file mode 100644 index 01f18b70..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Hashing.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using SHA3.Net; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto; - -[SuppressMessage("ReSharper", "InconsistentNaming")] -internal static class Hashing -{ - public static string CalculateSHA1Hash(string input) - { - var inputSpan = input.AsSpan(); - - var byteCount = Encoding.ASCII.GetByteCount(inputSpan); - var span = byteCount < 1024 ? stackalloc byte[byteCount] : new byte[byteCount]; - - Encoding.ASCII.GetBytes(inputSpan, span); - return CalculateSHA1Hash(span); - } - - private static string CalculateSHA1Hash(ReadOnlySpan input) - { - Span buffer = stackalloc byte[20]; - SHA1.HashData(input, buffer); - return Convert.ToHexString(buffer).ToLower(CultureInfo.InvariantCulture); - } - - public static byte[] CalculateSHA3_256Hash(string input) - { - var byteCount = Encoding.ASCII.GetByteCount(input); - var buffer = new byte[byteCount]; - Encoding.ASCII.GetBytes(input.AsSpan(), buffer.AsSpan()); - return CalculateSHA3_256Hash(buffer); - } - - private static byte[] CalculateSHA3_256Hash(byte[] input) - { - // TODO: find or create a lib that works with spans - using var algorithm = Sha3.Sha3256(); - var hash = algorithm.ComputeHash(input); - return hash; - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/IHardwareInfoProvider.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/IHardwareInfoProvider.cs deleted file mode 100644 index ef33f976..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/IHardwareInfoProvider.cs +++ /dev/null @@ -1,73 +0,0 @@ -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto; - -/// -/// Represents a Hardware Info Provider. -/// -[PublicAPI] -public interface IHardwareInfoProvider -{ - /// - /// Returns the Serial Number of the Volume that contains the Windows folder. - /// - /// - /// - string GetVolumeSerialNumber(); - - /// - /// Returns the Manufacturer of the Motherboard. - /// - /// - /// - string GetBaseBoardManufacturer(); - - /// - /// Returns the Serial Number of the Motherboard. - /// - /// - /// - string GetBaseBoardSerialNumber(); - - /// - /// Returns the Manufacturer of the BIOS. - /// - /// - /// - string GetBIOSManufacturer(); - - /// - /// Returns of Serial Number of the BIOS. - /// - /// - /// - string GetBIOSSerialNumber(); - - /// - /// Returns the PNPDeviceId of the GPU. - /// - /// - /// - string GetVideoControllerDeviceId(); - - /// - /// Returns the Manufacturer of the CPU. - /// - /// - /// - string GetProcessorManufacturer(); - - /// - /// Returns the ProcessorId of the CPU. - /// - /// - /// - string GetProcessorId(); - - /// - /// Returns the name of the CPU. - /// - /// - /// - string GetProcessorName(); -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/HardwareInfoProvider.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/HardwareInfoProvider.cs deleted file mode 100644 index ea6d41ed..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/HardwareInfoProvider.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Runtime.Versioning; -using JetBrains.Annotations; -using static GameFinder.StoreHandlers.EADesktop.Crypto.Windows.WMIHelper; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto.Windows; - -/// -/// Implementation of that uses WMI and GetVolumeInformationW. -/// -[PublicAPI] -[SupportedOSPlatform("windows")] -[ExcludeFromCodeCoverage(Justification = "Only available on Windows.")] -public sealed class HardwareInfoProvider : IHardwareInfoProvider -{ - /// - public string GetVolumeSerialNumber() - { - var ok = Native.GetVolumeInformationW( - "C:\\", - null!, - 0, - out var volumeSerialNumber, - out _, - out _, - null!, - 0); - - if (ok) return volumeSerialNumber.ToString("X", CultureInfo.InvariantCulture); - throw new HardwareInfoProviderException($"{nameof(Native.GetVolumeInformationW)} returned false", null); - } - - /// - public string GetBaseBoardManufacturer() - { - return GetWMIProperty(Win32BaseBoardClass, ManufacturerPropertyName); - } - - /// - public string GetBaseBoardSerialNumber() - { - return GetWMIProperty(Win32BaseBoardClass, SerialNumberPropertyName); - } - - /// - public string GetBIOSManufacturer() - { - return GetWMIProperty(Win32BIOSClass, ManufacturerPropertyName); - } - - /// - public string GetBIOSSerialNumber() - { - return GetWMIProperty(Win32BIOSClass, SerialNumberPropertyName); - } - - /// - public string GetVideoControllerDeviceId() - { - return GetWMIProperty(Win32VideoControllerClass, PNPDeviceIDPropertyName); - } - - /// - public string GetProcessorManufacturer() - { - return GetWMIProperty(Win32ProcessorClass, ManufacturerPropertyName); - } - - /// - public string GetProcessorId() - { - return GetWMIProperty(Win32ProcessorClass, ProcessorIDPropertyName); - } - - /// - public string GetProcessorName() - { - return GetWMIProperty(Win32ProcessorClass, NamePropertyName); - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/Native.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/Native.cs deleted file mode 100644 index ec0afb81..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/Native.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto.Windows; - -[SupportedOSPlatform("windows")] -[ExcludeFromCodeCoverage(Justification = "Uses DllImport.")] -internal static class Native -{ - /// - /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationw - /// - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - public static extern bool GetVolumeInformationW( - string rootPathName, // [in, optional] LPCWSTR lpRootPathName - StringBuilder? volumeNameBuffer, // [out, optional] LPWSTR lpVolumeNameBuffer - int volumeNameSize, // [in] DWORD nVolumeNameSize, - out uint volumeSerialNumber, // [out, optional] LPDWORD lpVolumeSerialNumber - out uint maximumComponentLength, // [out, optional] LPDWORD lpMaximumComponentLength - out uint fileSystemFlags, // [out, optional] LPDWORD lpFileSystemFlags - StringBuilder? fileSystemNameBuffer,// [out, optional] LPWSTR lpFileSystemNameBuffer - int nFileSystemNameSize // [in] DWORD nFileSystemNameSize - ); -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/WMIHelper.cs b/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/WMIHelper.cs deleted file mode 100644 index 5e90232b..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/Crypto/Windows/WMIHelper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Management; -using System.Runtime.Versioning; - -namespace GameFinder.StoreHandlers.EADesktop.Crypto.Windows; - -[SupportedOSPlatform("windows")] -[SuppressMessage("ReSharper", "InconsistentNaming")] -[ExcludeFromCodeCoverage(Justification = "Only available on Windows.")] -internal static class WMIHelper -{ - // private const string ComputerName = "localhost"; - // private const string Namespace = @"ROOT\CIMV2"; - // private const string QueryDialect = "WQL"; - - public const string Win32BaseBoardClass = "Win32_BaseBoard"; - public const string Win32BIOSClass = "Win32_BIOS"; - public const string Win32VideoControllerClass = "Win32_VideoController"; - public const string Win32ProcessorClass = "Win32_Processor"; - - public const string ManufacturerPropertyName = "Manufacturer"; - public const string SerialNumberPropertyName = "SerialNumber"; - public const string PNPDeviceIDPropertyName = "PNPDeviceId"; - public const string NamePropertyName = "Name"; - public const string ProcessorIDPropertyName = "ProcessorId"; - - public static string GetWMIProperty(string className, string propertyName) - { - try - { - var query = $"SELECT {propertyName} FROM {className}"; - var selectQuery = new SelectQuery(query); - var searcher = new ManagementObjectSearcher(selectQuery); - - using var results = searcher.Get(); - - var arr = new ManagementBaseObject[1]; - results.CopyTo(arr, 0); - - var baseObject = arr[0]; - - var propertyData = baseObject.Properties[propertyName]; - if (propertyData.Type == CimType.String) return (string)propertyData.Value; - - throw new Exception($"Property from query is not of type {nameof(CimType.String)} but {propertyData.Type}"); - - } - catch (Exception e) - { - throw new HardwareInfoProviderException($"Exception while getting property {propertyName} from class {className}", e); - } - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/EADesktopGame.cs b/src/GameFinder.StoreHandlers.EADesktop/EADesktopGame.cs deleted file mode 100644 index db463c05..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/EADesktopGame.cs +++ /dev/null @@ -1,27 +0,0 @@ -using GameFinder.Common; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.EADesktop; - -/// -/// Represents a game installed with the EA Desktop app. -/// -/// Id of the game. -/// Slug name of the game. -/// Absolute path to the game folder. -[PublicAPI] -public record EADesktopGame(EADesktopGameId EADesktopGameId, string BaseSlug, AbsolutePath BaseInstallPath) : IGame -{ - /// - /// Returns the absolute path to the installerdata.xml file inside the __Installer folder - /// of the game folder. - /// - /// - public AbsolutePath GetInstallerDataFile() - { - return BaseInstallPath - .Combine("__Installer") - .Combine("installerdata.xml"); - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/EADesktopGameId.cs b/src/GameFinder.StoreHandlers.EADesktop/EADesktopGameId.cs deleted file mode 100644 index 350998a5..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/EADesktopGameId.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.EADesktop; - -/// -/// Represents an id for games installed with EA Desktop. -/// -[ValueObject] -public readonly partial struct EADesktopGameId : IAugmentWith -{ - /// - public static IEqualityComparer InnerValueDefaultEqualityComparer { get; } = StringComparer.OrdinalIgnoreCase; -} - -/// -[PublicAPI] -public class EADesktopGameIdComparer : IEqualityComparer -{ - private static EADesktopGameIdComparer? _default; - - /// - /// Default equality comparer that uses . - /// - public static EADesktopGameIdComparer Default => _default ??= new(); - - private readonly StringComparison _stringComparison; - - /// - /// Default constructor that uses . - /// - public EADesktopGameIdComparer() : this(StringComparison.OrdinalIgnoreCase) { } - - /// - /// Constructor. - /// - /// - public EADesktopGameIdComparer(StringComparison stringComparison) - { - _stringComparison = stringComparison; - } - - /// - public bool Equals(EADesktopGameId x, EADesktopGameId y) => string.Equals(x.Value, y.Value, _stringComparison); - - /// - public int GetHashCode(EADesktopGameId obj) => obj.Value.GetHashCode(_stringComparison); -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/EADesktopHandler.cs b/src/GameFinder.StoreHandlers.EADesktop/EADesktopHandler.cs deleted file mode 100644 index 6cb47f64..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/EADesktopHandler.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using GameFinder.Common; -using GameFinder.StoreHandlers.EADesktop.Crypto; -using JetBrains.Annotations; -using NexusMods.Paths; -using OneOf; - -namespace GameFinder.StoreHandlers.EADesktop; - -/// -/// Handler for finding games installed with EA Desktop. -/// -[PublicAPI] -public class EADesktopHandler : AHandler -{ - internal const string AllUsersFolderName = "530c11479fe252fc5aabc24935b9776d4900eb3ba58fdc271e0d6229413ad40e"; - internal const string InstallInfoFileName = "IS"; - - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true, - NumberHandling = JsonNumberHandling.Strict, - TypeInfoResolver = SourceGenerationContext.Default, - }; - - private readonly IFileSystem _fileSystem; - private readonly IHardwareInfoProvider _hardwareInfoProvider; - - /// - /// The supported schema version of this handler. You can change the schema policy with - /// . - /// - public const int SupportedSchemaVersion = 21; - - /// - /// Policy to use when the schema version does not match . - /// The default behavior is . - /// - public SchemaPolicy SchemaPolicy { get; set; } = SchemaPolicy.Warn; - - /// - /// Constructor. - /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. - /// - /// - /// The implementation of to use. Currently only - /// - /// is available and is Windows-only. - /// - public EADesktopHandler(IFileSystem fileSystem, IHardwareInfoProvider hardwareInfoProvider) - { - _fileSystem = fileSystem; - _hardwareInfoProvider = hardwareInfoProvider; - } - - /// - public override IEqualityComparer IdEqualityComparer => EADesktopGameIdComparer.Default; - - /// - public override Func IdSelector => game => game.EADesktopGameId; - - /// - public override IEnumerable> FindAllGames() - { - var dataFolder = GetDataFolder(_fileSystem); - if (!_fileSystem.DirectoryExists(dataFolder)) - { - yield return new ErrorMessage($"Data folder {dataFolder} does not exist!"); - yield break; - } - - var installInfoFile = GetInstallInfoFile(dataFolder); - if (!_fileSystem.FileExists(installInfoFile)) - { - yield return new ErrorMessage($"File does not exist: {installInfoFile.GetFullPath()}"); - yield break; - } - - var decryptionResult = DecryptInstallInfoFile(_fileSystem, installInfoFile, _hardwareInfoProvider); - if (decryptionResult.TryGetError(out var error)) - { - yield return error; - yield break; - } - - var plaintext = decryptionResult.AsT0; - foreach (var result in ParseInstallInfoFile(plaintext, installInfoFile, SchemaPolicy)) - { - yield return result; - } - } - - internal static AbsolutePath GetDataFolder(IFileSystem fileSystem) - { - return fileSystem - .GetKnownPath(KnownPath.CommonApplicationDataDirectory) - .Combine("EA Desktop"); - } - - internal static AbsolutePath GetInstallInfoFile(AbsolutePath dataFolder) - { - return dataFolder - .Combine(AllUsersFolderName) - .Combine(InstallInfoFileName); - } - - internal static OneOf DecryptInstallInfoFile(IFileSystem fileSystem, AbsolutePath installInfoFile, IHardwareInfoProvider hardwareInfoProvider) - { - try - { - using var stream = fileSystem.ReadFile(installInfoFile); - var cipherText = new byte[stream.Length]; - var unused = stream.Read(cipherText); - - var key = Decryption.CreateDecryptionKey(hardwareInfoProvider); - - var iv = Decryption.CreateDecryptionIV(); - var plainText = Decryption.DecryptFile(cipherText, key, iv); - return plainText; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Exception while decrypting file {installInfoFile.GetFullPath()}"); - } - } - - internal IEnumerable> ParseInstallInfoFile(string plaintext, AbsolutePath installInfoFile, SchemaPolicy schemaPolicy) - { - try - { - return ParseInstallInfoFileInner(plaintext, installInfoFile, schemaPolicy); - } - catch (Exception e) - { - return new OneOf[] - { - new ErrorMessage(e, $"Exception while parsing InstallInfoFile {installInfoFile.GetFullPath()}"), - }; - } - } - - - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code", - Justification = $"{nameof(JsonSerializerOptions)} uses {nameof(SourceGenerationContext)} for type information.")] - private IEnumerable> ParseInstallInfoFileInner(string plaintext, AbsolutePath installInfoFile, SchemaPolicy schemaPolicy) - { - var installInfoFileContents = JsonSerializer.Deserialize(plaintext, JsonSerializerOptions); - - if (installInfoFileContents is null) - { - yield return new ErrorMessage($"Unable to deserialize InstallInfoFile {installInfoFile.GetFullPath()}"); - yield break; - } - - var schemaVersionNullable = installInfoFileContents.Schema?.Version; - if (!schemaVersionNullable.HasValue) - { - yield return new ErrorMessage($"InstallInfoFile {installInfoFile.GetFullPath()} does not have a schema version!"); - yield break; - } - - var schemaVersion = schemaVersionNullable.Value; - var (schemaMessage, isSchemaError) = CreateSchemaVersionMessage(schemaPolicy, schemaVersion, installInfoFile); - if (schemaMessage is not null) - { - yield return new ErrorMessage(schemaMessage); - if (isSchemaError) yield break; - } - - var installInfos = installInfoFileContents.InstallInfos; - if (installInfos is null || installInfos.Count == 0) - { - yield return new ErrorMessage($"InstallInfoFile {installInfoFile.GetFullPath()} does not have any infos!"); - yield break; - } - - for (var i = 0; i < installInfos.Count; i++) - { - yield return InstallInfoToGame(_fileSystem, installInfos[i], i, installInfoFile); - } - } - - internal static (string? message, bool isError) CreateSchemaVersionMessage( - SchemaPolicy schemaPolicy, int schemaVersion, AbsolutePath installInfoFilePath) - { - if (schemaVersion == SupportedSchemaVersion) return (null, false); - - return schemaPolicy switch - { - SchemaPolicy.Warn => ( - $"InstallInfoFile {installInfoFilePath} has a schema version " + - $"{schemaVersion.ToString(CultureInfo.InvariantCulture)} but this library only supports schema version " + - $"{SupportedSchemaVersion.ToString(CultureInfo.InvariantCulture)}. " + - $"This message is a WARNING because the consumer of this library has set {nameof(SchemaPolicy)} to {nameof(SchemaPolicy.Warn)}", - false), - SchemaPolicy.Error => ( - $"InstallInfoFile {installInfoFilePath} has a schema version " + - $"{schemaVersion.ToString(CultureInfo.InvariantCulture)} but this library only supports schema version " + - $"{SupportedSchemaVersion.ToString(CultureInfo.InvariantCulture)}. " + - $"This is an ERROR because the consumer of this library has set {nameof(SchemaPolicy)} to {nameof(SchemaPolicy.Error)}", - true), - SchemaPolicy.Ignore => (null, false), - _ => throw new ArgumentOutOfRangeException(nameof(schemaPolicy), schemaPolicy, message: null), - }; - } - - internal static OneOf InstallInfoToGame(IFileSystem fileSystem, InstallInfo installInfo, int i, AbsolutePath installInfoFilePath) - { - var num = i.ToString(CultureInfo.InvariantCulture); - - if (string.IsNullOrEmpty(installInfo.SoftwareId)) - { - return new ErrorMessage($"InstallInfo #{num} does not have the value \"softwareId\""); - } - - var softwareId = installInfo.SoftwareId; - - if (string.IsNullOrEmpty(installInfo.BaseSlug)) - { - return new ErrorMessage($"InstallInfo #{num} for {softwareId} does not have the value \"baseSlug\""); - } - - var baseSlug = installInfo.BaseSlug; - - if (string.IsNullOrEmpty(installInfo.BaseInstallPath)) - { - return new ErrorMessage($"InstallInfo #{num} for {softwareId} ({baseSlug}) does not have the value \"baseInstallPath\""); - } - - var baseInstallPath = installInfo.BaseInstallPath; - var game = new EADesktopGame( - EADesktopGameId.From(softwareId), - baseSlug, - fileSystem.FromUnsanitizedFullPath(baseInstallPath) - ); - - return game; - } -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/InstallInfoFile.cs b/src/GameFinder.StoreHandlers.EADesktop/InstallInfoFile.cs deleted file mode 100644 index d5f0ab48..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/InstallInfoFile.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.EADesktop; - -[UsedImplicitly] -internal record InstallInfoFile( - List? InstallInfos, - Schema? Schema); - -[UsedImplicitly] -internal record InstallInfo( - string? BaseInstallPath, - string? BaseSlug, - string? InstallCheck, - [property: JsonPropertyName("softwareId")] - string? SoftwareId); - -[UsedImplicitly] -internal record Schema(int Version); diff --git a/src/GameFinder.StoreHandlers.EADesktop/SchemaPolicy.cs b/src/GameFinder.StoreHandlers.EADesktop/SchemaPolicy.cs deleted file mode 100644 index 29d10559..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/SchemaPolicy.cs +++ /dev/null @@ -1,29 +0,0 @@ -using GameFinder.Common; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.EADesktop; - -/// -/// Policy to employ when the schema version doesn't match the supported schema version. -/// See for more information. -/// -[PublicAPI] -public enum SchemaPolicy -{ - /// - /// Completely ignores the new schema version. - /// - Ignore, - - /// - /// Creates a warning about the new schema version. Note that this is represented as - /// an error using . This does not abort the - /// parsing. - /// - Warn, - - /// - /// Creates an error and aborts the parsing. - /// - Error, -} diff --git a/src/GameFinder.StoreHandlers.EADesktop/SourceGenerationContext.cs b/src/GameFinder.StoreHandlers.EADesktop/SourceGenerationContext.cs deleted file mode 100644 index 033186b5..00000000 --- a/src/GameFinder.StoreHandlers.EADesktop/SourceGenerationContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GameFinder.StoreHandlers.EADesktop; - -[JsonSourceGenerationOptions(WriteIndented = false, GenerationMode = JsonSourceGenerationMode.Default)] -[JsonSerializable(typeof(InstallInfoFile))] -internal partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/src/GameFinder.StoreHandlers.EGS/EGSGame.cs b/src/GameFinder.StoreHandlers.EGS/EGSGame.cs deleted file mode 100644 index 726f7e6b..00000000 --- a/src/GameFinder.StoreHandlers.EGS/EGSGame.cs +++ /dev/null @@ -1,14 +0,0 @@ -using GameFinder.Common; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.EGS; - -/// -/// Represents a game installed with the Epic Games Store. -/// -/// -/// -/// -[PublicAPI] -public record EGSGame(EGSGameId CatalogItemId, string DisplayName, AbsolutePath InstallLocation) : IGame; diff --git a/src/GameFinder.StoreHandlers.EGS/EGSGameId.cs b/src/GameFinder.StoreHandlers.EGS/EGSGameId.cs deleted file mode 100644 index e51b1024..00000000 --- a/src/GameFinder.StoreHandlers.EGS/EGSGameId.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.EGS; - -/// -/// Represents an id for games installed with the Epic Games Store. -/// -[ValueObject] -public readonly partial struct EGSGameId : IAugmentWith -{ - /// - public static IEqualityComparer InnerValueDefaultEqualityComparer { get; } = StringComparer.OrdinalIgnoreCase; -} - -/// -[PublicAPI] -public class EGSGameIdComparer : IEqualityComparer -{ - private static EGSGameIdComparer? _default; - - /// - /// Default equality comparer that uses . - /// - public static EGSGameIdComparer Default => _default ??= new(); - - private readonly StringComparison _stringComparison; - - /// - /// Default constructor that uses . - /// - public EGSGameIdComparer() : this(StringComparison.OrdinalIgnoreCase) { } - - /// - /// Constructor. - /// - /// - public EGSGameIdComparer(StringComparison stringComparison) - { - _stringComparison = stringComparison; - } - - /// - public bool Equals(EGSGameId x, EGSGameId y) => string.Equals(x.Value, y.Value, _stringComparison); - - /// - public int GetHashCode(EGSGameId obj) => obj.Value.GetHashCode(_stringComparison); -} diff --git a/src/GameFinder.StoreHandlers.EGS/EGSHandler.cs b/src/GameFinder.StoreHandlers.EGS/EGSHandler.cs deleted file mode 100644 index cc41c06b..00000000 --- a/src/GameFinder.StoreHandlers.EGS/EGSHandler.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using GameFinder.Common; -using GameFinder.RegistryUtils; -using JetBrains.Annotations; -using NexusMods.Paths; -using OneOf; - -namespace GameFinder.StoreHandlers.EGS; - -[UsedImplicitly] -internal record ManifestFile(string CatalogItemId, string DisplayName, string InstallLocation); - -/// -/// Handler for finding games installed with the Epic Games Store. -/// -[PublicAPI] -public class EGSHandler : AHandler -{ - internal const string RegKey = @"Software\Epic Games\EOS"; - internal const string ModSdkMetadataDir = "ModSdkMetadataDir"; - - private readonly IRegistry _registry; - private readonly IFileSystem _fileSystem; - - /// - /// Constructor. - /// - /// - /// The implementation of to use. For a shared instance - /// use on Windows. For tests either use - /// , a custom implementation or just a mock - /// of the interface. See the README for more information if you want to use - /// Wine. - /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. See the README for more information - /// if you want to use Wine. - /// - public EGSHandler(IRegistry registry, IFileSystem fileSystem) - { - _registry = registry; - _fileSystem = fileSystem; - } - - /// - public override IEqualityComparer IdEqualityComparer => EGSGameIdComparer.Default; - - /// - public override Func IdSelector => game => game.CatalogItemId; - - /// - public override IEnumerable> FindAllGames() - { - var manifestDir = GetManifestDir(); - if (!_fileSystem.DirectoryExists(manifestDir)) - { - yield return new ErrorMessage($"The manifest directory {manifestDir.GetFullPath()} does not exist!"); - yield break; - } - - var itemFiles = _fileSystem - .EnumerateFiles(manifestDir, "*.item") - .ToArray(); - - if (itemFiles.Length == 0) - { - yield return new ErrorMessage($"The manifest directory {manifestDir.GetFullPath()} does not contain any .item files"); - yield break; - } - - foreach (var itemFile in itemFiles) - { - yield return DeserializeGame(itemFile); - } - } - - private OneOf DeserializeGame(AbsolutePath itemFile) - { - using var stream = _fileSystem.ReadFile(itemFile); - - try - { - var manifest = JsonSerializer.Deserialize(stream, SourceGenerationContext.Default.ManifestFile); - - if (manifest is null) - { - return new ErrorMessage($"Unable to deserialize file {itemFile.GetFullPath()}"); - } - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (manifest.CatalogItemId is null) - { - return new ErrorMessage($"Manifest {itemFile.GetFullPath()} does not have a value \"CatalogItemId\""); - } - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (manifest.DisplayName is null) - { - return new ErrorMessage($"Manifest {itemFile.GetFullPath()} does not have a value \"DisplayName\""); - } - - if (string.IsNullOrEmpty(manifest.InstallLocation)) - { - return new ErrorMessage($"Manifest {itemFile.GetFullPath()} does not have a value \"InstallLocation\""); - } - - var game = new EGSGame( - EGSGameId.From(manifest.CatalogItemId), - manifest.DisplayName, - _fileSystem.FromUnsanitizedFullPath(manifest.InstallLocation) - ); - - return game; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Unable to deserialize file {itemFile.GetFullPath()}"); - } - } - - private AbsolutePath GetManifestDir() - { - return TryGetManifestDirFromRegistry(out var manifestDir) - ? manifestDir - : GetDefaultManifestsPath(_fileSystem); - } - - internal static AbsolutePath GetDefaultManifestsPath(IFileSystem fileSystem) - { - return fileSystem - .GetKnownPath(KnownPath.CommonApplicationDataDirectory) - .Combine("Epic/EpicGamesLauncher/Data/Manifests"); - } - - private bool TryGetManifestDirFromRegistry(out AbsolutePath manifestDir) - { - manifestDir = default; - - try - { - var currentUser = _registry.OpenBaseKey(RegistryHive.CurrentUser); - using var regKey = currentUser.OpenSubKey(RegKey); - - if (regKey is null || !regKey.TryGetString("ModSdkMetadataDir", - out var registryMetadataDir)) return false; - - manifestDir = _fileSystem.FromUnsanitizedFullPath(registryMetadataDir); - return true; - - } - catch (Exception) - { - return false; - } - } -} diff --git a/src/GameFinder.StoreHandlers.EGS/SourceGenerationContext.cs b/src/GameFinder.StoreHandlers.EGS/SourceGenerationContext.cs deleted file mode 100644 index 557a4101..00000000 --- a/src/GameFinder.StoreHandlers.EGS/SourceGenerationContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Text.Json.Serialization; - -namespace GameFinder.StoreHandlers.EGS; - -[JsonSourceGenerationOptions(WriteIndented = false, GenerationMode = JsonSourceGenerationMode.Default)] -[JsonSerializable(typeof(ManifestFile))] -internal partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/src/GameFinder.StoreHandlers.GOG/GOGGame.cs b/src/GameFinder.StoreHandlers.GOG/GOGGame.cs index c9718ec1..bbbbcba9 100644 --- a/src/GameFinder.StoreHandlers.GOG/GOGGame.cs +++ b/src/GameFinder.StoreHandlers.GOG/GOGGame.cs @@ -7,8 +7,20 @@ namespace GameFinder.StoreHandlers.GOG; /// /// Represents a game installed with GOG Galaxy. /// -/// -/// -/// [PublicAPI] -public record GOGGame(GOGGameId Id, string Name, AbsolutePath Path) : IGame; +public record GOGGame : IGame, IGameName +{ + /// + /// Gets the ID of this game. + /// + /// + /// This corresponds to the "Product ID" field found on https://www.gogdb.org + /// + public required GOGGameId Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required AbsolutePath Path { get; init; } +} diff --git a/src/GameFinder.StoreHandlers.GOG/GOGGameId.cs b/src/GameFinder.StoreHandlers.GOG/GOGGameId.cs index 30c27d8e..7daea18a 100644 --- a/src/GameFinder.StoreHandlers.GOG/GOGGameId.cs +++ b/src/GameFinder.StoreHandlers.GOG/GOGGameId.cs @@ -1,9 +1,16 @@ +using GameFinder.Common; +using JetBrains.Annotations; using TransparentValueObjects; namespace GameFinder.StoreHandlers.GOG; /// -/// Represents an id for games installed with GOG Galaxy. +/// Represents an ID for games installed with GOG Galaxy. /// -[ValueObject] -public readonly partial struct GOGGameId { } +[PublicAPI] +[ValueObject] +public readonly partial struct GOGGameId : IId +{ + /// + public bool Equals(IId? other) => other is GOGGameId same && Equals(same); +} diff --git a/src/GameFinder.StoreHandlers.GOG/GOGHandler.cs b/src/GameFinder.StoreHandlers.GOG/GOGHandler.cs index e2275cdf..ef0d1fe8 100644 --- a/src/GameFinder.StoreHandlers.GOG/GOGHandler.cs +++ b/src/GameFinder.StoreHandlers.GOG/GOGHandler.cs @@ -1,133 +1,123 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using GameFinder.Common; using GameFinder.RegistryUtils; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using NexusMods.Paths; -using OneOf; namespace GameFinder.StoreHandlers.GOG; /// /// Handler for finding games installed with GOG Galaxy. /// +/// +/// This is the base class of which is probably +/// what you're looking for instead. This abstract class is only useful if you +/// want to extend the base functionality. +/// +/// [PublicAPI] -public class GOGHandler : AHandler +public abstract class GOGHandler : IHandler + where TGame : class, IGame { - internal const string GOGRegKey = @"Software\GOG.com\Games"; + /// + /// Registry sub-key of GOG Galaxy. + /// + /// + /// The base key for this is , + /// also make sure to use the 32-bit registry view. + /// + public const string GOGRegKey = @"Software\GOG.com\Games"; - private readonly IRegistry _registry; - private readonly IFileSystem _fileSystem; + /// + /// Logger. + /// + protected readonly ILogger Logger; + + /// + /// Filesystem. + /// + protected readonly IFileSystem FileSystem; + + /// + /// Registry. + /// + protected readonly IRegistry Registry; /// /// Constructor. /// - /// - /// The implementation of to use. For a shared instance - /// use on Windows. For tests either use - /// , a custom implementation or just a mock - /// of the interface. See the README for more information if you want to use - /// Wine. - /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. See the README for more information - /// if you want to use Wine. - /// - public GOGHandler(IRegistry registry, IFileSystem fileSystem) + protected GOGHandler( + ILoggerFactory loggerFactory, + IFileSystem fileSystem, + IRegistry registry) { - _registry = registry; - _fileSystem = fileSystem; + Logger = loggerFactory.CreateLogger>(); + FileSystem = fileSystem; + Registry = registry; } /// - public override Func IdSelector => game => game.Id; - - /// - public override IEqualityComparer? IdEqualityComparer => null; - - /// - public override IEnumerable> FindAllGames() + [Pure] + public IReadOnlyList Search() { - try - { - var localMachine = _registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + var baseKey = Registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); - using var gogKey = localMachine.OpenSubKey(GOGRegKey); - if (gogKey is null) - { - return new OneOf[] - { - new ErrorMessage($"Unable to open HKEY_LOCAL_MACHINE\\{GOGRegKey}"), - }; - } - - var subKeyNames = gogKey.GetSubKeyNames().ToArray(); - if (subKeyNames.Length == 0) - { - return new OneOf[] - { - new ErrorMessage($"Registry key {gogKey.GetName()} has no sub-keys"), - }; - } - - return subKeyNames - .Select(subKeyName => ParseSubKey(gogKey, subKeyName)) - .ToArray(); - } - catch (Exception e) + using var gogKey = baseKey.OpenSubKey(GOGRegKey); + if (gogKey is null) { - return new OneOf[] - { - new ErrorMessage(e, "Exception looking for GOG games"), - }; + LogMessages.UnableToOpenGOGSubKey(Logger, baseKey.GetName(), GOGRegKey); + return Array.Empty(); } - } - private OneOf ParseSubKey(IRegistryKey gogKey, string subKeyName) - { - try + var subKeyNames = gogKey.GetSubKeyNames().ToArray(); + LogMessages.FoundSubKeyNames(Logger, gogKey.GetName(), subKeyNames.Length, subKeyNames); + + var res = new List(capacity: subKeyNames.Length); + foreach (var subKeyName in subKeyNames) { - using var subKey = gogKey.OpenSubKey(subKeyName); - if (subKey is null) + try { - return new ErrorMessage($"Unable to open {gogKey}\\{subKeyName}"); - } + var game = ParseSubKey(gogKey, subKeyName); + if (game is null) continue; - if (!subKey.TryGetString("gameID", out var sId)) - { - return new ErrorMessage($"{subKey.GetName()} doesn't have a string value \"gameID\""); + LogMessages.ParsedRegistryKey(Logger, subKeyName, game); + res.Add(game); } - - if (!long.TryParse(sId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) + catch (Exception e) { - return new ErrorMessage($"The value \"gameID\" of {subKey.GetName()} is not a number: \"{sId}\""); + LogMessages.ExceptionWhileParsingRegistryKey(Logger, e, subKeyName); } + } - if (!subKey.TryGetString("gameName", out var name)) - { - return new ErrorMessage($"{subKey.GetName()} doesn't have a string value \"gameName\""); - } + return res; + } - if (!subKey.TryGetString("path", out var path)) - { - return new ErrorMessage($"{subKey.GetName()} doesn't have a string value \"path\""); - } + /// + /// Parses the given and key name into . + /// + protected abstract TGame? ParseSubKey(IRegistryKey gogKey, string subKeyName); +} - var game = new GOGGame( - GOGGameId.From(id), - name, - _fileSystem.FromUnsanitizedFullPath(path) - ); +/// +/// Handler for finding games installed with GOG Galaxy. +/// +[PublicAPI] +public class GOGHandler : GOGHandler +{ + /// + /// Constructor. + /// + public GOGHandler( + ILoggerFactory loggerFactory, + IFileSystem fileSystem, + IRegistry registry) : base(loggerFactory, fileSystem, registry) { } - return game; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Exception while parsing registry key {gogKey}\\{subKeyName}"); - } + /// + protected override GOGGame? ParseSubKey(IRegistryKey gogKey, string subKeyName) + { + return RegistryParser.ParseSubKey(Logger, FileSystem, gogKey, subKeyName); } } diff --git a/src/GameFinder.StoreHandlers.GOG/LogMessages.cs b/src/GameFinder.StoreHandlers.GOG/LogMessages.cs new file mode 100644 index 00000000..ef20438c --- /dev/null +++ b/src/GameFinder.StoreHandlers.GOG/LogMessages.cs @@ -0,0 +1,131 @@ +using System; +using GameFinder.Common; +using Microsoft.Extensions.Logging; + +namespace GameFinder.StoreHandlers.GOG; + +internal static partial class LogMessages +{ + [LoggerMessage( + EventId = 0, EventName = nameof(UnableToOpenGOGSubKey), + Level = LogLevel.Information, + Message = "Unable to open the GOG Registry Key `{baseKeyName}\\\\{gogKeyName}`." + + "This is no cause for concern if GOG Galaxy isn't installed or no games have been installed with GOG Galaxy." + )] + public static partial void UnableToOpenGOGSubKey( + ILogger logger, + string baseKeyName, + string gogKeyName + ); + + [LoggerMessage( + EventId = 1, EventName = nameof(FoundSubKeyNames), + Level = LogLevel.Trace, + Message = "Found {count} sub key name(s) for key `{key}`: `{names}`" + )] + public static partial void FoundSubKeyNames( + ILogger logger, + string key, + int count, + string[] names + ); + + [LoggerMessage( + EventId = 2, EventName = nameof(UnableToOpenSubKey), + Level = LogLevel.Warning, + Message = "Unable to open the Registry Key `{gogKeyName}\\\\{subKeyName}`" + )] + public static partial void UnableToOpenSubKey( + ILogger logger, + string gogKeyName, + string subKeyName + ); + + [LoggerMessage( + EventId = 3, EventName = nameof(NoValueForKey), + Level = LogLevel.Warning, + Message = "Found no value with name `{valueName}` in `{keyName}`" + )] + public static partial void NoValueForKey( + ILogger logger, + string valueName, + string keyName + ); + + [LoggerMessage( + EventId = 4, EventName = nameof(ValueForKey), + Level = LogLevel.Trace, + Message = "Found value with name `{valueName}` in `{keyName}`: `{value}`" + )] + public static partial void ValueForKey( + ILogger logger, + string valueName, + string keyName, + string value + ); + + [LoggerMessage( + EventId = 5, EventName = nameof(FailedToParse), + Level = LogLevel.Warning, + Message = "Failed to parse value `{value}` with name `{valueName}` in `{keyName}` as `{typeName}`" + )] + public static partial void FailedToParse( + ILogger logger, + string keyName, + string value, + string valueName, + string typeName + ); + + [LoggerMessage( + EventId = 6, EventName = nameof(NoId), + Level = LogLevel.Warning, + Message = "Unable to find any IDs in `{keyName}`" + )] + public static partial void NoId( + ILogger logger, + string keyName + ); + + [LoggerMessage( + EventId = 7, EventName = nameof(NoGameName), + Level = LogLevel.Warning, + Message = "Unable to find any game names in `{keyName}`" + )] + public static partial void NoGameName( + ILogger logger, + string keyName + ); + + [LoggerMessage( + EventId = 8, EventName = nameof(NoPath), + Level = LogLevel.Warning, + Message = "Unable to find any paths in `{keyName}`" + )] + public static partial void NoPath( + ILogger logger, + string keyName + ); + + [LoggerMessage( + EventId = 9, EventName = nameof(ParsedRegistryKey), + Level = LogLevel.Information, + Message = "Parsed Registry Key `{keyName}` to `{game}`" + )] + public static partial void ParsedRegistryKey( + ILogger logger, + string keyName, + IGame game + ); + + [LoggerMessage( + EventId = 10, EventName = nameof(ExceptionWhileParsingRegistryKey), + Level = LogLevel.Warning, + Message = "Exception while parsing Registry Key `{keyName}`" + )] + public static partial void ExceptionWhileParsingRegistryKey( + ILogger logger, + Exception e, + string keyName + ); +} diff --git a/src/GameFinder.StoreHandlers.GOG/RegistryParser.cs b/src/GameFinder.StoreHandlers.GOG/RegistryParser.cs new file mode 100644 index 00000000..6db68891 --- /dev/null +++ b/src/GameFinder.StoreHandlers.GOG/RegistryParser.cs @@ -0,0 +1,180 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using GameFinder.RegistryUtils; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.GOG; + +/// +/// Parsing methods. +/// +[PublicAPI] +public static class RegistryParser +{ + /// + /// Parses the given sub key in into . + /// + /// Used for logging. + /// + /// Used to convert the unsanitized installation path found in the Registry + /// to a sanitized . + /// + /// The main GOG registry key. + /// The name of the sub key. + /// + /// if the parsing failed, otherwise a valid . + /// + /// + /// This method can throw exceptions and should be surrounded with a try/catch. + /// + public static GOGGame? ParseSubKey( + ILogger logger, + IFileSystem fileSystem, + IRegistryKey gogKey, + string subKeyName) + { + using var subKey = gogKey.OpenSubKey(subKeyName); + if (subKey is null) + { + LogMessages.UnableToOpenSubKey(logger, gogKey.GetName(), subKeyName); + return null; + } + + if (!TryGetId(logger, subKey, out var id)) return null; + if (!TryGetGameName(logger, subKey, out var gameName)) return null; + if (!TryGetPath(logger, fileSystem, subKey, out var path)) return null; + + var game = new GOGGame + { + Id = id, + Name = gameName, + Path = path, + }; + + return game; + } + + /// + /// Tries to find a valid path in . + /// + public static bool TryGetPath( + ILogger logger, + IFileSystem fileSystem, + IRegistryKey subKey, + out AbsolutePath path) + { + const string valuePath = "path"; + const string valueExe = "exe"; + const string valueWorkingDir = "workingDir"; + + if (TryGetString(logger, subKey, valuePath, out var sPath)) + { + path = fileSystem.FromUnsanitizedFullPath(sPath); + return true; + } + + if (TryGetString(logger, subKey, valueExe, out var sExe)) + { + var exePath = fileSystem.FromUnsanitizedFullPath(sExe); + path = exePath.Parent; + return true; + } + + if (TryGetString(logger, subKey, valueWorkingDir, out var sWorkingDir)) + { + path = fileSystem.FromUnsanitizedFullPath(sWorkingDir); + return true; + } + + LogMessages.NoPath(logger, subKey.GetName()); + path = default; + return false; + } + + /// + /// Tries to find a valid game name in . + /// + public static bool TryGetGameName( + ILogger logger, + IRegistryKey subKey, + [NotNullWhen(true)] out string? gameName) + { + const string valueGameName = "gameName"; + + if (!TryGetString(logger, subKey, valueGameName, out gameName)) + { + LogMessages.NoGameName(logger, subKey.GetName()); + return false; + } + + return true; + } + + /// + /// Tries to find a valid ID in . + /// + public static bool TryGetId( + ILogger logger, + IRegistryKey subKey, + out GOGGameId id) + { + const string valueGameID = "gameID"; + const string valueProductID = "productID"; + + id = default; + if (TryParse(logger, subKey, valueGameID, out var gameId)) + { + id = GOGGameId.From(gameId); + return true; + } + + if (TryParse(logger, subKey, valueProductID, out var productId)) + { + id = GOGGameId.From(productId); + return true; + } + + LogMessages.NoId(logger, subKey.GetName()); + return false; + } + + /// + /// Tries to get a string from the given registry key and parses it into + /// the given integer type. + /// + public static bool TryParse( + ILogger logger, + IRegistryKey subKey, + string valueName, + out T value) where T : struct, INumberBase + { + value = default; + if (!TryGetString(logger, subKey, valueName, out var sValue)) return false; + if (T.TryParse(sValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) return true; + + LogMessages.FailedToParse(logger, subKey.GetName(), sValue, valueName, typeof(T).Name); + return false; + } + + /// + /// Tries to get a string from the given registry key. + /// + public static bool TryGetString( + ILogger logger, + IRegistryKey subKey, + string valueName, + [NotNullWhen(true)] out string? value) + { + if (!subKey.TryGetString(valueName, out value)) + { + LogMessages.NoValueForKey(logger, valueName, subKey.GetName()); + return false; + } + + LogMessages.ValueForKey(logger, valueName, subKey.GetName(), value); + return true; + } +} diff --git a/src/GameFinder.StoreHandlers.Origin/LogMessages.cs b/src/GameFinder.StoreHandlers.Origin/LogMessages.cs new file mode 100644 index 00000000..64c89b7e --- /dev/null +++ b/src/GameFinder.StoreHandlers.Origin/LogMessages.cs @@ -0,0 +1,121 @@ +using System; +using GameFinder.Common; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.Origin; + +internal static partial class LogMessages +{ + [LoggerMessage( + EventId = 0, EventName = nameof(MissingManifestDirectory), + Level = LogLevel.Information, + Message = "The directory where Origin stores its Manifest files `{manifestDirectory}` does not exist. " + + "This is no cause for concern if Origin isn't installed or no games have been installed with Origin." + )] + public static partial void MissingManifestDirectory( + ILogger logger, + AbsolutePath manifestDirectory + ); + + [LoggerMessage( + EventId = 1, EventName = nameof(NoManifestFiles), + Level = LogLevel.Information, + Message = "The directory where Origin stores its Manifest files `{manifestDirectory}` does not contain any Manifest files. " + + "This is no cause for concern if Origin isn't installed or no games have been installed with Origin." + )] + public static partial void NoManifestFiles( + ILogger logger, + AbsolutePath manifestDirectory + ); + + [LoggerMessage( + EventId = 2, EventName = nameof(ParsingManifestFile), + Level = LogLevel.Debug, + Message = "Parsing manifest file `{manifestFile}`" + )] + public static partial void ParsingManifestFile( + ILogger logger, + AbsolutePath manifestFile + ); + + [LoggerMessage( + EventId = 3, EventName = nameof(NoValuesForKey), + Level = LogLevel.Warning, + Message = "Manifest file `{manifestFile}` does not contain any values for key `{key}`" + )] + public static partial void NoValuesForKey( + ILogger logger, + AbsolutePath manifestFile, + string key + ); + + [LoggerMessage( + EventId = 4, EventName = nameof(ValuesForKey), + Level = LogLevel.Trace, + Message = "Found {count} value(s) for key `{key}` in Manifest file `{manifestFile}`: `{values}`" + )] + public static partial void ValuesForKey( + ILogger logger, + AbsolutePath manifestFile, + string key, + int count, + string[] values + ); + + [LoggerMessage( + EventId = 5, EventName = nameof(SkipSteamGame), + Level = LogLevel.Information, + Message = "Skipping Manifest file `{manifestFile}` because it indicates that the game comes from Steam: `{steamId}`" + )] + public static partial void SkipSteamGame( + ILogger logger, + AbsolutePath manifestFile, + string steamId + ); + + [LoggerMessage( + EventId = 6, EventName = nameof(FoundValueForKey), + Level = LogLevel.Trace, + Message = "Found value `{value}` for key `{key}` in Manifest file `{manifestFile}`" + )] + public static partial void FoundValueForKey( + ILogger logger, + AbsolutePath manifestFile, + string key, + string value + ); + + [LoggerMessage( + EventId = 7, EventName = nameof(ExceptionWhileParsingManifest), + Level = LogLevel.Warning, + Message = "Exception while parsing Manifest file `{manifestFile}`" + )] + public static partial void ExceptionWhileParsingManifest( + ILogger logger, + Exception e, + AbsolutePath manifestFile + ); + + [LoggerMessage( + EventId = 8, EventName = nameof(ParsedManifestFile), + Level = LogLevel.Information, + Message = "Parsed Manifest file `{manifestFile}` to `{game}`" + )] + public static partial void ParsedManifestFile( + ILogger logger, + AbsolutePath manifestFile, + IGame game + ); + + [LoggerMessage( + EventId = 9, EventName = nameof(ExceptionWhileReadingManifest), + Level = LogLevel.Warning, + Message = "Exception while reading Manifest file `{manifestFile}`" + )] + public static partial void ExceptionWhileReadingManifest( + ILogger logger, + Exception e, + AbsolutePath manifestFile + ); +} diff --git a/src/GameFinder.StoreHandlers.Origin/ManifestLocator.cs b/src/GameFinder.StoreHandlers.Origin/ManifestLocator.cs new file mode 100644 index 00000000..7d5a6e14 --- /dev/null +++ b/src/GameFinder.StoreHandlers.Origin/ManifestLocator.cs @@ -0,0 +1,20 @@ +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.Origin; + +/// +/// Locator methods. +/// +public static class ManifestLocator +{ + /// + /// Returns the path to the default manifest directory. + /// + public static AbsolutePath GetManifestDirectory(IFileSystem fileSystem) + { + return fileSystem + .GetKnownPath(KnownPath.CommonApplicationDataDirectory) + .Combine("Origin") + .Combine("LocalContent"); + } +} diff --git a/src/GameFinder.StoreHandlers.Origin/ManifestParser.cs b/src/GameFinder.StoreHandlers.Origin/ManifestParser.cs new file mode 100644 index 00000000..f998a60c --- /dev/null +++ b/src/GameFinder.StoreHandlers.Origin/ManifestParser.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Text; +using System.Web; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.Origin; + +/// +/// Parsing methods. +/// +public static class ManifestParser +{ + /// + /// Parses the provided Manifest file into a value. + /// + /// Used for logging. + /// + /// Used to convert the unsanitized installation path found in the Manifest + /// to a sanitized . + /// + /// The contents of the Manifest file. + /// The path to the Manifest file, used for logging. + /// + /// if the parsing failed, otherwise a valid . + /// + /// + /// This method can throw exceptions and should be surrounded with a try/catch. + /// + public static OriginGame? ParseManifestFile( + ILogger logger, + IFileSystem fileSystem, + string contents, + AbsolutePath manifestFile) + { + const string keyId = "id"; + const string keyInstallPath = "dipInstallPath"; + + // NOTE(erri120): NameValueCollection is case-insensitive by default, + // which is exactly what we need to parse Manifest files because the + // casing is inconsistent. + var query = HttpUtility.ParseQueryString(contents, Encoding.UTF8); + + // NOTE(erri120): The Manifest can contain duplicate key-value entries, + // so we have to use GetValues to get all of them. + var idValues = query.GetValues(keyId); + if (idValues is null || idValues.Length == 0) + { + LogMessages.NoValuesForKey(logger, manifestFile, keyId); + return null; + } + + LogMessages.ValuesForKey(logger, manifestFile, keyId, idValues.Length, idValues); + + idValues = idValues + .Where(value => !string.IsNullOrWhiteSpace(value)) + .OrderByDescending(value => value.Length) + .ToArray(); + + if (idValues.Length == 0) + { + LogMessages.NoValuesForKey(logger, manifestFile, keyId); + return null; + } + + // NOTE(erri120): Origin will append "@steam" to the ID if the game was + // bought and installed via Steam. Origin will also launch even if you + // run the game through Steam. We skip these games since they will be + // picked up by the Steam StoreHandler instead. + var steamId = idValues.FirstOrDefault(value => value.EndsWith("@steam", StringComparison.OrdinalIgnoreCase)); + if (steamId is not null) + { + LogMessages.SkipSteamGame(logger, manifestFile, steamId); + return null; + } + + var id = idValues[0]; + LogMessages.FoundValueForKey(logger, manifestFile, keyId, id); + + var installPathValues = query.GetValues(keyInstallPath); + if (installPathValues is null || idValues.Length == 0) + { + LogMessages.NoValuesForKey(logger, manifestFile, keyInstallPath); + return null; + } + + LogMessages.ValuesForKey(logger, manifestFile, keyInstallPath, installPathValues.Length, installPathValues); + + installPathValues = installPathValues + .Where(value => !string.IsNullOrWhiteSpace(value)) + .OrderByDescending(value => value.Length) + .ToArray(); + + if (installPathValues.Length == 0) + { + LogMessages.NoValuesForKey(logger, manifestFile, keyInstallPath); + return null; + } + + var installPath = installPathValues[0]; + LogMessages.FoundValueForKey(logger, manifestFile, keyInstallPath, installPath); + + var game = new OriginGame + { + Id = OriginGameId.From(id), + Path = fileSystem.FromUnsanitizedFullPath(installPath), + }; + + return game; + } +} diff --git a/src/GameFinder.StoreHandlers.Origin/OriginGame.cs b/src/GameFinder.StoreHandlers.Origin/OriginGame.cs index 88a12ac8..34dec615 100644 --- a/src/GameFinder.StoreHandlers.Origin/OriginGame.cs +++ b/src/GameFinder.StoreHandlers.Origin/OriginGame.cs @@ -7,7 +7,12 @@ namespace GameFinder.StoreHandlers.Origin; /// /// Represents a game installed with Origin. /// -/// -/// [PublicAPI] -public record OriginGame(OriginGameId Id, AbsolutePath InstallPath) : IGame; +public record OriginGame : IGame +{ + /// + public required OriginGameId Id { get; init; } + + /// + public required AbsolutePath Path { get; init; } +} diff --git a/src/GameFinder.StoreHandlers.Origin/OriginGameId.cs b/src/GameFinder.StoreHandlers.Origin/OriginGameId.cs index fa26a735..0da3f557 100644 --- a/src/GameFinder.StoreHandlers.Origin/OriginGameId.cs +++ b/src/GameFinder.StoreHandlers.Origin/OriginGameId.cs @@ -1,50 +1,21 @@ using System; using System.Collections.Generic; +using GameFinder.Common; using JetBrains.Annotations; using TransparentValueObjects; namespace GameFinder.StoreHandlers.Origin; /// -/// Represents an id for games installed with Origin. +/// Represents an ID for games installed with Origin. /// +[PublicAPI] [ValueObject] -public readonly partial struct OriginGameId : IAugmentWith +public readonly partial struct OriginGameId : IId, IAugmentWith { /// public static IEqualityComparer InnerValueDefaultEqualityComparer { get; } = StringComparer.OrdinalIgnoreCase; -} - -/// -[PublicAPI] -public class OriginGameIdComparer : IEqualityComparer -{ - private static OriginGameIdComparer? _default; - - /// - /// Default equality comparer that uses . - /// - public static OriginGameIdComparer Default => _default ??= new(); - - private readonly StringComparison _stringComparison; - - /// - /// Default constructor that uses . - /// - public OriginGameIdComparer() : this(StringComparison.OrdinalIgnoreCase) { } - - /// - /// Constructor. - /// - /// - public OriginGameIdComparer(StringComparison stringComparison) - { - _stringComparison = stringComparison; - } - - /// - public bool Equals(OriginGameId x, OriginGameId y) => string.Equals(x.Value, y.Value, _stringComparison); /// - public int GetHashCode(OriginGameId obj) => obj.Value.GetHashCode(_stringComparison); + public bool Equals(IId? other) => other is OriginGameId same && Equals(same); } diff --git a/src/GameFinder.StoreHandlers.Origin/OriginHandler.cs b/src/GameFinder.StoreHandlers.Origin/OriginHandler.cs index 9d583eb9..72bc0bbc 100644 --- a/src/GameFinder.StoreHandlers.Origin/OriginHandler.cs +++ b/src/GameFinder.StoreHandlers.Origin/OriginHandler.cs @@ -1,129 +1,125 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; -using System.Web; using GameFinder.Common; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using NexusMods.Paths; -using OneOf; namespace GameFinder.StoreHandlers.Origin; /// -/// Handler for finding games install with Origin. +/// Handler for finding games installed with Origin. /// +/// +/// This is the base class of which is probably +/// what you're looking for instead. This abstract class is only useful if you +/// want to extend the base functionality. +/// +/// [PublicAPI] -public class OriginHandler : AHandler +public abstract class OriginHandler : IHandler + where TGame : class, IGame { - private readonly IFileSystem _fileSystem; + /// + /// Logger. + /// + protected readonly ILogger Logger; /// - /// Constructor. + /// Filesystem. /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. See the README for more information - /// if you want to use Wine. - /// - public OriginHandler(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } + protected readonly IFileSystem FileSystem; - internal static AbsolutePath GetManifestDir(IFileSystem fileSystem) + /// + /// Constructor. + /// + protected OriginHandler( + ILoggerFactory loggerFactory, + IFileSystem fileSystem) { - return fileSystem.GetKnownPath(KnownPath.CommonApplicationDataDirectory) - .Combine("Origin") - .Combine("LocalContent"); + Logger = loggerFactory.CreateLogger>(); + FileSystem = fileSystem; } /// - public override Func IdSelector => game => game.Id; - - /// - public override IEqualityComparer IdEqualityComparer => OriginGameIdComparer.Default; - - /// - public override IEnumerable> FindAllGames() + [Pure] + public IReadOnlyList Search() { - var manifestDir = GetManifestDir(_fileSystem); - - if (!_fileSystem.DirectoryExists(manifestDir)) + var manifestDir = ManifestLocator.GetManifestDirectory(FileSystem); + if (!FileSystem.DirectoryExists(manifestDir)) { - yield return new ErrorMessage($"Manifest folder {manifestDir} does not exist!"); - yield break; + LogMessages.MissingManifestDirectory(Logger, manifestDir); + return Array.Empty(); } - var mfstFiles = _fileSystem.EnumerateFiles(manifestDir, "*.mfst").ToList(); - if (mfstFiles.Count == 0) + var manifestFiles = FileSystem.EnumerateFiles(manifestDir, "*.mfst").ToArray(); + if (manifestFiles.Length == 0) { - yield return new ErrorMessage($"Manifest folder {manifestDir} does not contain any .mfst files"); - yield break; + LogMessages.NoManifestFiles(Logger, manifestDir); + return Array.Empty(); } - foreach (var mfstFile in mfstFiles) + var games = new List(capacity: manifestFiles.Length); + + foreach (var manifestFile in manifestFiles) { - var result = ParseMfstFile(mfstFile); + LogMessages.ParsingManifestFile(Logger, manifestFile); - // ignore steam games - if (result.IsT2) continue; + string contents; - if (result.IsT1) + try { - yield return result.AsT1; - continue; + using var stream = FileSystem.ReadFile(manifestFile); + using var reader = new StreamReader(stream, Encoding.UTF8); + contents = reader.ReadToEnd(); } - - yield return result.AsT0; - } - } - - [SuppressMessage("ReSharper", "IdentifierTypo")] - private OneOf ParseMfstFile(AbsolutePath filePath) - { - try - { - using var stream = _fileSystem.ReadFile(filePath); - using var reader = new StreamReader(stream, Encoding.UTF8); - var contents = reader.ReadToEnd(); - - var query = HttpUtility.ParseQueryString(contents, Encoding.UTF8); - - // using GetValues because some manifest have duplicate key-value entries for whatever reason - var ids = query.GetValues("id"); - if (ids is null || ids.Length == 0) + catch (Exception e) { - return new ErrorMessage($"Manifest {filePath} does not have a value \"id\""); + LogMessages.ExceptionWhileReadingManifest(Logger, e, manifestFile); + continue; } - var id = ids[0]; - if (id.EndsWith("@steam", StringComparison.OrdinalIgnoreCase)) - return true; + try + { + var game = ParseManifestFile(contents, manifestFile); + if (game is null) continue; - var installPaths = query.GetValues("dipInstallPath"); - if (installPaths is null || installPaths.Length == 0) + LogMessages.ParsedManifestFile(Logger, manifestFile, game); + games.Add(game); + } + catch (Exception e) { - return new ErrorMessage($"Manifest {filePath} does not have a value \"dipInstallPath\""); + LogMessages.ExceptionWhileParsingManifest(Logger, e, manifestFile); } + } - var path = installPaths - .OrderByDescending(x => x.Length) - .First(); + return games; + } - var game = new OriginGame( - OriginGameId.From(id), - _fileSystem.FromUnsanitizedFullPath(path) - ); + /// + /// Parses the given Manifest file into . + /// + public abstract TGame? ParseManifestFile(string contents, AbsolutePath manifestFile); +} - return game; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Exception while parsing {filePath}"); - } +/// +/// Handler for finding games installed with Origin. +/// +[PublicAPI] +public class OriginHandler : OriginHandler +{ + /// + /// Constructor. + /// + public OriginHandler(ILoggerFactory loggerFactory, IFileSystem fileSystem) : base(loggerFactory, fileSystem) { } + + /// + [Pure] + public override OriginGame? ParseManifestFile(string contents, AbsolutePath manifestFile) + { + return ManifestParser.ParseManifestFile(Logger, FileSystem, contents, manifestFile); } } diff --git a/src/GameFinder.StoreHandlers.Steam/EnumerableExtensions.cs b/src/GameFinder.StoreHandlers.Steam/EnumerableExtensions.cs deleted file mode 100644 index a7a4a8c7..00000000 --- a/src/GameFinder.StoreHandlers.Steam/EnumerableExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam; - -internal static class EnumerableExtensions -{ - public static Size Sum(this IEnumerable enumerable) - { - return enumerable.Aggregate(Size.Zero, (total, current) => total + current); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/GameFinder.StoreHandlers.Steam.csproj.DotSettings b/src/GameFinder.StoreHandlers.Steam/GameFinder.StoreHandlers.Steam.csproj.DotSettings deleted file mode 100644 index c98be33c..00000000 --- a/src/GameFinder.StoreHandlers.Steam/GameFinder.StoreHandlers.Steam.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - True - True \ No newline at end of file diff --git a/src/GameFinder.StoreHandlers.Steam/Models/AppManifest.cs b/src/GameFinder.StoreHandlers.Steam/Models/AppManifest.cs deleted file mode 100644 index 86f7a634..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/AppManifest.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Text; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using JetBrains.Annotations; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a parsed app manifest file. -/// -/// -/// Manifest files appmanifest_*.acf use Valve's custom -/// KeyValue format. -/// -[PublicAPI] -public sealed record AppManifest -{ - /// - /// Gets the to the appmanifest_*.acf file - /// that was parsed to produce this . - /// - /// E:/SteamLibrary/steamapps/appmanifest_262060.acf - /// - public required AbsolutePath ManifestPath { get; init; } - - #region Parsed Values - - /// - /// Gets the unique identifier of the app. - /// - public required AppId AppId { get; init; } - - /// - /// Gets the this app is part of. This is pretty irrelevant. - /// - public SteamUniverse Universe { get; init; } - - /// - /// Gets name of the app. - /// - public required string Name { get; init; } - - /// - /// Gets the current state of the app. - /// - public required StateFlags StateFlags { get; init; } - - /// - /// Gets the to the installation directory of the app. - /// - public required AbsolutePath InstallationDirectory { get; init; } - - /// - /// Gets the time when the app was last updated. - /// - /// - /// This value is saved as a unix timestamp in the *.acf file. - /// - public DateTimeOffset LastUpdated { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets the size of the app on disk. - /// - /// - /// This value is only set when installing or updating the app. If the - /// user adds or removes files from the , Steam - /// won't update this value automatically. This value will be - /// while the app is being staged. - /// - /// - public Size SizeOnDisk { get; init; } = Size.Zero; - - /// - /// Gets the size of the app during staging. - /// - /// - /// This value will be after the app has been - /// completely downloaded and installed. - /// - /// - public Size StagingSize { get; init; } = Size.Zero; - - /// - /// Gets the unique identifier of the current build of the app. - /// - /// - /// This value represents the current "patch" of the app and is - /// a global identifier that can be used to retrieve the current - /// update notes using SteamDB. - /// - /// - /// - public BuildId BuildId { get; init; } = BuildId.DefaultValue; - - /// - /// Gets the last owner of this app. - /// - /// - /// This is usually the last account that installed and launched the app. This - /// can be used to get the user date for the current app. - /// - public SteamId LastOwner { get; init; } = SteamId.Empty; - - /// - /// Unknown. - /// - /// - /// The meaning of this value is unknown. - /// - public uint UpdateResult { get; init; } - - /// - /// Gets the amount of bytes to download. - /// - /// This value will be when there is no update available. - /// - public Size BytesToDownload { get; init; } = Size.Zero; - - /// - /// Gets the amount of bytes downloaded. - /// - /// This value will be when there is no update available. - /// - public Size BytesDownloaded { get; init; } = Size.Zero; - - /// - /// Gets the amount of bytes to stage. - /// - /// This value will be when there is no update available. - /// - public Size BytesToStage { get; init; } = Size.Zero; - - /// - /// Gets the amount of bytes staged. - /// - /// This value will be when there is no update available. - /// - public Size BytesStaged { get; init; } = Size.Zero; - - /// - /// Gets the target build ID of the update. - /// - /// - /// This value will be 0, if there is no update available. - /// - /// - /// - public BuildId TargetBuildId { get; init; } = BuildId.DefaultValue; - - /// - /// Gets the automatic update behavior for this app. - /// - public AutoUpdateBehavior AutoUpdateBehavior { get; init; } - - /// - /// Gets the background download behavior for this app. - /// - public BackgroundDownloadBehavior BackgroundDownloadBehavior { get; init; } - - /// - /// Gets the time when the app is scheduled to be updated. - /// - /// - /// The *.acf file saves this value as a unix timestamp and the value will be - /// 0 or if there is no update scheduled. - /// - public DateTimeOffset ScheduledAutoUpdate { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Whether Steam will do a full validation after the next update. - /// - public bool FullValidateAfterNextUpdate { get; init; } - - /// - /// Gets all locally installed depots. - /// - public IReadOnlyDictionary InstalledDepots { get; init; } = ImmutableDictionary.Empty; - - /// - /// Gets all scripts that run after installation. - /// - public IReadOnlyDictionary InstallScripts { get; init; } = ImmutableDictionary.Empty; - - /// - /// Gets all locally installed shared depots. - /// - /// - /// Shared depots are depots from another app and are commonly used for the Steamworks Common Redistributables. - /// - public IReadOnlyDictionary SharedDepots { get; init; } = ImmutableDictionary.Empty; - - /// - /// Gets the local user config. - /// - /// - /// This can contains keys like language or BetaKey. - /// - /// - public IReadOnlyDictionary UserConfig { get; init; } = ImmutableDictionary.Empty; - - /// - /// Gets the local mounted config. - /// - /// - /// The meaning of these values are unknown. You'd think they have something to do with - /// but at the time of writing, I couldn't make out a clear connection since these values aren't being updated at all. - /// - /// - public IReadOnlyDictionary MountedConfig { get; init; } = ImmutableDictionary.Empty; - - #endregion - - #region Helpers - - private static readonly RelativePath CommonDirectoryName = "common".ToRelativePath(); - private static readonly RelativePath ShaderCacheDirectoryName = "shadercache".ToRelativePath(); - private static readonly RelativePath WorkshopDirectoryName = "workshop".ToRelativePath(); - private static readonly RelativePath CompatabilityDataDirectoryName = "compatdata".ToRelativePath(); - - /// - /// Parses the file at again and returns a new - /// instance of . - /// - [Pure] - [System.Diagnostics.Contracts.Pure] - [MustUseReturnValue] - public Result Reload() - { - return AppManifestParser.ParseManifestFile(ManifestPath); - } - - /// - /// Gets the path to the appworkshop_*.acf file. - /// - /// E:/SteamLibrary/steamapps/workshop/appworkshop_262060.acf - public AbsolutePath GetWorkshopManifestFilePath() => ManifestPath.Parent - .Combine(WorkshopDirectoryName) - .Combine($"appworkshop_{AppId.Value.ToString(CultureInfo.InvariantCulture)}.acf"); - - /// - /// Gets all locally installed DLCs. - /// - public IReadOnlyDictionary GetInstalledDLCs() => InstalledDepots - .Where(kv => kv.Value.DLCAppId != AppId.DefaultValue) - .ToDictionary(kv => kv.Value.DLCAppId, kv => kv.Value); - - /// - /// Gets the URL to the Update Notes for the current on SteamDB. - /// - public string GetCurrentUpdateNotesUrl() => BuildId.GetSteamDbUpdateNotesUrl(); - - /// - /// Gets the URL to the Update Notes for the next update using on SteamDB. - /// - /// - /// This value will be null, if is . - /// - public string? GetNextUpdateNotesUrl() => TargetBuildId == BuildId.DefaultValue ? null : TargetBuildId.GetSteamDbUpdateNotesUrl(); - - /// - /// Gets the user-data path for the current app using and - /// . - /// - /// - /// Path to the Steam installation directory. Example: - /// C:/Program Files/Steam - /// - /// C:/Program Files/Steam/userdata/149956546/262060 - /// - public AbsolutePath GetUserDataDirectoryPath(AbsolutePath steamDirectory) - { - return SteamLocationFinder.GetUserDataDirectoryPath(steamDirectory, LastOwner) - .Combine(AppId.Value.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Gets the path to the shader-cache directory. - /// - /// E:/SteamLibrary/common/steamapps/shadercache/262060 - public AbsolutePath GetShaderCacheDirectoryPath() => ManifestPath.Parent - .Combine(ShaderCacheDirectoryName) - .Combine(AppId.Value.ToString(CultureInfo.InvariantCulture)); - - /// - /// Gets the path to the compatability data directory used by Proton. - /// - /// /mnt/ssd/SteamLibrary/common/steamapps/compatdata/262060 - public AbsolutePath GetCompatabilityDataDirectoryPath() => ManifestPath.Parent - .Combine(CompatabilityDataDirectoryName) - .Combine(AppId.Value.ToString(CultureInfo.InvariantCulture)); - - #endregion - - #region Overwrites - - /// - public bool Equals(AppManifest? other) - { - if (other is null) return false; - if (AppId != other.AppId) return false; - if (Universe != other.Universe) return false; - if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) return false; - if (StateFlags != other.StateFlags) return false; - if (InstallationDirectory != other.InstallationDirectory) return false; - if (LastUpdated != other.LastUpdated) return false; - if (SizeOnDisk != other.SizeOnDisk) return false; - if (StagingSize != other.StagingSize) return false; - if (BuildId != other.BuildId) return false; - if (LastOwner != other.LastOwner) return false; - if (UpdateResult != other.UpdateResult) return false; - if (BytesToDownload != other.BytesToDownload) return false; - if (BytesDownloaded != other.BytesDownloaded) return false; - if (BytesToStage != other.BytesToStage) return false; - if (BytesStaged != other.BytesStaged) return false; - if (TargetBuildId != other.TargetBuildId) return false; - if (AutoUpdateBehavior != other.AutoUpdateBehavior) return false; - if (BackgroundDownloadBehavior != other.BackgroundDownloadBehavior) return false; - if (ScheduledAutoUpdate != other.ScheduledAutoUpdate) return false; - if (FullValidateAfterNextUpdate != other.FullValidateAfterNextUpdate) return false; - if (!InstalledDepots.SequenceEqual(other.InstalledDepots)) return false; - if (!InstallScripts.SequenceEqual(other.InstallScripts)) return false; - if (!SharedDepots.SequenceEqual(other.SharedDepots)) return false; - if (!UserConfig.SequenceEqual(other.UserConfig)) return false; - if (!MountedConfig.SequenceEqual(other.MountedConfig)) return false; - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(ManifestPath); - hashCode.Add(AppId); - hashCode.Add((int)Universe); - hashCode.Add(Name); - hashCode.Add((int)StateFlags); - hashCode.Add(InstallationDirectory); - hashCode.Add(LastUpdated); - hashCode.Add(SizeOnDisk); - hashCode.Add(StagingSize); - hashCode.Add(BuildId); - hashCode.Add(LastOwner); - hashCode.Add(UpdateResult); - hashCode.Add(BytesToDownload); - hashCode.Add(BytesDownloaded); - hashCode.Add(BytesToStage); - hashCode.Add(BytesStaged); - hashCode.Add(TargetBuildId); - hashCode.Add((int)AutoUpdateBehavior); - hashCode.Add((int)BackgroundDownloadBehavior); - hashCode.Add(ScheduledAutoUpdate); - hashCode.Add(FullValidateAfterNextUpdate); - hashCode.Add(InstalledDepots); - hashCode.Add(InstallScripts); - hashCode.Add(SharedDepots); - hashCode.Add(UserConfig); - hashCode.Add(MountedConfig); - return hashCode.ToHashCode(); - } - - /// - public override string ToString() - { - var sb = new StringBuilder(); - - sb.Append("{ "); - sb.Append($"{nameof(AppId)} = {AppId}, "); - sb.Append($"{nameof(Name)} = {Name}, "); - sb.Append($"{nameof(InstallationDirectory)} = {InstallationDirectory}"); - sb.Append(" }"); - - return sb.ToString(); - } - - #endregion -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/AutoUpdateBehavior.cs b/src/GameFinder.StoreHandlers.Steam/Models/AutoUpdateBehavior.cs deleted file mode 100644 index 509654dd..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/AutoUpdateBehavior.cs +++ /dev/null @@ -1,33 +0,0 @@ -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Automatic update behavior. -/// -/// -/// This data was sourced from Steam itself. You can manually change the update -/// settings and verify the values in the *.acf file. -/// -[PublicAPI] -public enum AutoUpdateBehavior : byte -{ - /// - /// Always keep the app updated. The app and its updates will be - /// automatically acquired as soon as they are available. - /// - AlwaysUpdated = 0, - - /// - /// Only update the app when it's launched. Updated content will - /// be acquired only when launching the app. - /// - UpdateOnLaunch = 1, - - /// - /// High Priority - Always auto-update the app before others. The app - /// and its updates will be automatically acquired as soon as they - /// are available. Steam will prioritize the app over other downloads. - /// - HighPriority = 2, -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/BackgroundDownloadBehavior.cs b/src/GameFinder.StoreHandlers.Steam/Models/BackgroundDownloadBehavior.cs deleted file mode 100644 index 6ac3c5a8..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/BackgroundDownloadBehavior.cs +++ /dev/null @@ -1,32 +0,0 @@ -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Background download behavior. -/// -/// -/// This data was sourced from Steam itself. You can manually change the update -/// settings and verify the values in the *.acf file. -/// -[PublicAPI] -public enum BackgroundDownloadBehavior : byte -{ - /// - /// Follows the global Steam download settings. The default value - /// allows downloads while the app is running. - /// - FollowGlobalSteamSettings = 0, - - /// - /// Always allow background downloads while the app is running. - /// This overwrites the global settings. - /// - AlwaysAllow = 1, - - /// - /// Never allow background downloads while the app is running. - /// This overwrites the global settings. - /// - NeverAllow = 2, -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/Constants.cs b/src/GameFinder.StoreHandlers.Steam/Models/Constants.cs deleted file mode 100644 index 276e4605..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/Constants.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GameFinder.StoreHandlers.Steam.Models; - -internal static class Constants -{ - public const string SteamDbBaseUrl = "https://steamdb.info"; - public const string SteamStoreBaseUrl = "https://store.steampowered.com"; - public const string SteamCommunityBaseUrl = "https://steamcommunity.com"; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/InstalledDepot.cs b/src/GameFinder.StoreHandlers.Steam/Models/InstalledDepot.cs deleted file mode 100644 index cc1747f0..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/InstalledDepot.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Globalization; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a locally installed depot. -/// -[PublicAPI] -public record InstalledDepot -{ - /// - /// Gets the unique identifier of the depot. - /// - /// 445700 - public required DepotId DepotId { get; init; } - - /// - /// Gets the unique identifier of the current manifest of the depot. - /// - /// 560769545274183569 - public required ManifestId ManifestId { get; init; } - - /// - /// Gets the size of the depot on disk. - /// - public required Size SizeOnDisk { get; init; } = Size.Zero; - - /// - /// Gets the optionally unique identifier of the DLC that is associated with this depot. - /// - /// - /// This value can be if the depot is not associated with a DLC. - /// - public AppId DLCAppId { get; init; } = AppId.DefaultValue; - - /// - /// Gets the URL to the SteamDB page for the depot. - /// - public string GetSteamDbUrl() => $"{Constants.SteamDbBaseUrl}/depot/{DepotId.Value.ToString(CultureInfo.InvariantCulture)}"; - - /// - /// Gets the URL to the SteamDB page for the Changeset of the current version of this depot, based on the . - /// - public string GetSteamDbChangeSetUrl() => ManifestId.GetSteamDbChangesetUrl(DepotId); -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/LibraryFolder.cs b/src/GameFinder.StoreHandlers.Steam/Models/LibraryFolder.cs deleted file mode 100644 index 33a54a56..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/LibraryFolder.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a single library folder. -/// -[PublicAPI] -public sealed record LibraryFolder -{ - /// - /// Gets the absolute path to the library folder. - /// - public required AbsolutePath Path { get; init; } - - /// - /// Gets the label of the library folder. - /// - /// This value can be . - public string Label { get; init; } = string.Empty; - - /// - /// Gets the total size of the disk that contains this library. - /// - /// - /// Since you usually only have one library per disk (eg: C:/SteamLibrary, - /// E:/SteamLibrary and M:/SteamLibrary), this value can give you - /// an idea of how big the drive is. Do note that this value can also be - /// in some situations, for example, on Linux - /// if Steam doesn't fully understand the file system. - /// - public Size TotalDiskSize { get; init; } = Size.Zero; - - /// - /// Gets all installed apps inside the library folders and their sizes. - /// - /// - public IReadOnlyDictionary AppSizes { get; init; } = ImmutableDictionary.Empty; - - #region Helpers - - /// - /// Calculates the total size of all installed apps. - /// - /// - public Size GetTotalSizeOfInstalledApps() => AppSizes.Values.Sum(); - - /// - /// Calculates a free space estimate on the disk using and . - /// - public Size GetFreeSpaceEstimate() => TotalDiskSize - GetTotalSizeOfInstalledApps(); - - private static readonly RelativePath SteamAppsDirectoryName = "steamapps".ToRelativePath(); - - /// - /// Returns an enumerable for every appmanifest_*.acf file path in the current library. - /// - public IEnumerable EnumerateAppManifestFilePaths() - { - return Path.Combine(SteamAppsDirectoryName).EnumerateFiles("*.acf", recursive: false); - } - - #endregion - - #region Overrides - - /// - public bool Equals(LibraryFolder? other) - { - if (other is null) return false; - if (!Path.Equals(other.Path)) return false; - if (!Label.Equals(other.Label, StringComparison.Ordinal)) return false; - if (!TotalDiskSize.Equals(other.TotalDiskSize)) return false; - if (!AppSizes.SequenceEqual(other.AppSizes)) return false; - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(Path); - hashCode.Add(Label); - hashCode.Add(TotalDiskSize); - hashCode.Add(AppSizes); - return hashCode.ToHashCode(); - } - - /// - public override string ToString() => Path.GetFullPath(); - - #endregion -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/LibraryFoldersManifest.cs b/src/GameFinder.StoreHandlers.Steam/Models/LibraryFoldersManifest.cs deleted file mode 100644 index d92c872e..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/LibraryFoldersManifest.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Services; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a parsed libraryfolders.vdf file. -/// -[PublicAPI] -public sealed record LibraryFoldersManifest : IReadOnlyList -{ - /// - /// Gets the absolute path to the parsed manifest file. - /// - /// /home/gabe_newell/.local/share/Steam/config/libraryfolders.vdf - public required AbsolutePath ManifestPath { get; init; } - - /// - /// Gets all library folders. - /// - public required IReadOnlyList LibraryFolders { get; init; } - - /// - /// Parses the file at again and returns a new - /// instance of . - /// - [Pure] - [System.Diagnostics.Contracts.Pure] - [MustUseReturnValue] - public Result Reload() - { - return LibraryFoldersManifestParser.ParseManifestFile(ManifestPath); - } - - #region Overrides - - /// - public bool Equals(LibraryFoldersManifest? other) - { - if (other is null) return false; - if (!LibraryFolders.SequenceEqual(other.LibraryFolders)) return false; - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(LibraryFolders); - return hashCode.ToHashCode(); - } - - /// - public IEnumerator GetEnumerator() => LibraryFolders.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - public int Count => LibraryFolders.Count; - - /// - public LibraryFolder this[int index] => LibraryFolders[index]; - - #endregion -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/LocalAppData.cs b/src/GameFinder.StoreHandlers.Steam/Models/LocalAppData.cs deleted file mode 100644 index a919b751..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/LocalAppData.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Local user data for a specific game. -/// -/// -[PublicAPI] -public sealed record LocalAppData -{ - /// - /// Gets the unique identifier of the app that is associated with this data. - /// - public required AppId AppId { get; init; } - - /// - /// Gets the last played date or . - /// - public DateTimeOffset LastPlayed { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets the playtime or . - /// - public TimeSpan Playtime { get; init; } = TimeSpan.Zero; - - /// - /// Gets the custom launch options for the game or . - /// - public string LaunchOptions { get; init; } = string.Empty; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/LocalUserConfig.cs b/src/GameFinder.StoreHandlers.Steam/Models/LocalUserConfig.cs deleted file mode 100644 index 12ed4440..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/LocalUserConfig.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a parsed local user config. -/// -/// -/// userdata/{steamId}/config/localconfig.vdf -/// -[PublicAPI] -public sealed record LocalUserConfig -{ - /// - /// Gets the absolute path to the parsed config file. - /// - public required AbsolutePath ConfigPath { get; init; } - - /// - /// Gets the user that is associated with this config. - /// - public required SteamId User { get; init; } - - /// - /// Gets all local user data for almost every game the user owns. - /// - /// - /// It doesn't look like every game the user owns is listed in the config. Games that - /// the user hasn't played yet aren't listed, for example. - /// - public required IReadOnlyDictionary LocalAppData { get; init; } - - /// - /// Gets the absolute path to the directory where uncompressed screenshots are being saved to. - /// - public AbsolutePath? InGameOverlayScreenshotSaveUncompressedPath { get; init; } - - /// - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append("{ "); - sb.Append($"ConfigPath = {ConfigPath}, "); - sb.Append($"User = {User}"); - sb.Append(" }"); - return sb.ToString(); - } - - /// - public bool Equals(LocalUserConfig? other) - { - if (other is null) return false; - if (User != other.User) return false; - if (!LocalAppData.SequenceEqual(other.LocalAppData)) return false; - if (InGameOverlayScreenshotSaveUncompressedPath != other.InGameOverlayScreenshotSaveUncompressedPath) return false; - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(User); - hashCode.Add(LocalAppData); - hashCode.Add(InGameOverlayScreenshotSaveUncompressedPath); - return hashCode.ToHashCode(); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/StateFlags.cs b/src/GameFinder.StoreHandlers.Steam/Models/StateFlags.cs deleted file mode 100644 index 91b9e314..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/StateFlags.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Describes the various state an app can be in. -/// -/// -/// These values are sourced from https://github.com/lutris/lutris/blob/master/docs/steam.rst. -/// -[PublicAPI] -[Flags] -public enum StateFlags : uint -{ - /// - /// Invalid. - /// - Invalid = 0, - - /// - /// Uninstalled. - /// - Uninstalled = 1, - - /// - /// Update Required. - /// - UpdateRequired = 2 << 0, - - /// - /// Fully Installed. - /// - FullyInstalled = 2 << 1, - - /// - /// Encrypted. - /// - Encrypted = 2 << 2, - - /// - /// Locked. - /// - Locked = 2 << 3, - - /// - /// Files Missing. - /// - FilesMissing = 2 << 4, - - /// - /// App Running. - /// - AppRunning = 2 << 5, - - /// - /// Files Corrupt. - /// - FilesCorrupt = 2 << 6, - - /// - /// Update Running. - /// - UpdateRunning = 2 << 7, - - /// - /// Update Paused. - /// - UpdatePaused = 2 << 8, - - /// - /// Update Started. - /// - UpdateStarted = 2 << 9, - - /// - /// Uninstalling. - /// - Uninstalling = 2 << 10, - - /// - /// Backup Running. - /// - BackupRunning = 2 << 11, - - /// - /// Unknown 1. - /// - Unknown1 = 2 << 12, - - /// - /// Unknown 2. - /// - Unknown2 = 2 << 13, - - /// - /// Unknown 3. - /// - Unknown3 = 2 << 14, - - /// - /// Reconfiguring. - /// - Reconfiguring = 2 << 15, - - /// - /// Validating. - /// - Validating = 2 << 16, - - /// - /// Adding Files. - /// - AddingFiles = 2 << 17, - - /// - /// Pre-allocating. - /// - Preallocating = 2 << 18, - - /// - /// Downloading. - /// - Downloading = 2 << 19, - - /// - /// Staging. - /// - Staging = 2 << 20, - - /// - /// Committing. - /// - Committing = 2 << 21, - - /// - /// Update Stopping. - /// - UpdateStopping = 2 << 22, -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/SteamAccountType.cs b/src/GameFinder.StoreHandlers.Steam/Models/SteamAccountType.cs deleted file mode 100644 index 926ba79c..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/SteamAccountType.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Known account types for a Steam account. -/// -/// -/// This data was sourced from https://developer.valvesoftware.com/wiki/SteamID. -/// -[SuppressMessage("ReSharper", "CommentTypo")] -[SuppressMessage("ReSharper", "IdentifierTypo")] -[PublicAPI] -public enum SteamAccountType : byte -{ - /// - /// Invalid. This can't be used and has the letter I or i. - /// - Invalid = 0, - - /// - /// Single user account. Has the letter U. - /// - Individual = 1, - - /// - /// Multiseat (e.g. cybercafe) account. Has the letter M. - /// - Multiseat = 2, - - /// - /// Game server account. Has the letter G. - /// - GameServer = 3, - - /// - /// Anonymous game server account. Has the letter A. - /// - AnonGameServer = 4, - - /// - /// A pending user account which credentials are not yet verified by Steam. - /// This can't be used and has the letter P. - /// - Pending = 5, - - /// - /// Content server. Usage is unknown and it has the letter C. - /// - ContentServer = 6, - - /// - /// Group. Has the letter g. - /// - Clan = 7, - - /// - /// Chat. Has the letters T, L or c. - /// - Chat = 8, - - /// - /// Local PSN account on PS3 or Live account on 360. Can't be used and has no letter. - /// - P2PSuperSeeder = 9, - - /// - /// Anonymous user. Has the letter a. - /// - AnonUser = 10, -} - -/// -/// Extension methods for . -/// -[PublicAPI] -public static class SteamAccountTypeExtensions -{ - /// - /// Gets the letter assigned to the provided account type. - /// - /// - /// Returns ? if the account type is unknown or doesn't have - /// a letter associated with it. - /// - /// - /// - public static char GetLetter(this SteamAccountType accountType) => accountType switch - { - SteamAccountType.Invalid => 'I', - SteamAccountType.Individual => 'U', - SteamAccountType.Multiseat => 'M', - SteamAccountType.GameServer => 'G', - SteamAccountType.AnonGameServer => 'A', - SteamAccountType.Pending => 'P', - SteamAccountType.ContentServer => 'C', - SteamAccountType.Clan => 'g', - SteamAccountType.Chat => 'T', - SteamAccountType.AnonUser => 'a', - _ => '?' - }; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/SteamId.cs b/src/GameFinder.StoreHandlers.Steam/Models/SteamId.cs deleted file mode 100644 index 5a970417..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/SteamId.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Globalization; -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Unique identifier used to identify a Steam account. -/// -/// -/// See https://developer.valvesoftware.com/wiki/SteamID for more information. -/// -[PublicAPI] -public readonly struct SteamId : IEquatable, IComparable -{ - /// - /// Represents an empty ID of an invalid user. - /// - public static readonly SteamId Empty = new(0); - - /// - /// Compressed binary representation of the id. - /// - /// 76561198110222274 - public readonly ulong RawId; - - /// - /// Constructor using the compressed binary representation of the id. - /// - /// The raw 64-bit unique identifier. - public SteamId(ulong rawId) - { - RawId = rawId; - } - - /// - /// Factory method for consistency. - /// - /// - /// - public static SteamId From(ulong rawId) => new(rawId); - - /// - /// Creates a new using an account Id. - /// - public static SteamId FromAccountId( - uint accountId, - SteamUniverse universe = SteamUniverse.Public, - SteamAccountType accountType = SteamAccountType.Individual) - { - var rawId = (ulong)accountId; - - var universeMask = (ulong)universe << 56; - var accountTypeMask = (ulong)accountType << 52; - - rawId |= universeMask; - rawId |= accountTypeMask; - - return From(rawId); - } - - /// - /// Gets the universe of the account. - /// - public SteamUniverse Universe => (SteamUniverse)(int)(RawId >> 56); - - /// - /// Gets the account type. - /// - public SteamAccountType AccountType => (SteamAccountType)((byte)(RawId >> 52) & 0xF); - - /// - /// Gets the account identifier. - /// - /// - /// This identifier can be used to get the current user data in the Steam installation directory. - /// It's also used by . - /// - /// 149956546 - public uint AccountId => (uint)(RawId << 32 >> 32); - - /// - /// Gets the account number. - /// - /// - /// This is only useful for . - /// - /// 74978273 - public uint AccountNumber => (uint)(RawId << 32 >> 33); - - /// - /// Gets the textually representation in the Steam2 ID format. - /// - /// STEAM_1:0:74978273 - /// - public string Steam2Id => $"STEAM_{((byte)Universe).ToString(CultureInfo.InvariantCulture)}:{((byte)(RawId << 63 >> 63)).ToString(CultureInfo.InvariantCulture)}:{AccountNumber.ToString(CultureInfo.InvariantCulture)}"; - - /// - /// Gets the textually representation in the Steam3 ID format. - /// - /// [U:1:149956546] - /// - public string Steam3Id => $"[{AccountType.GetLetter()}:1:{AccountId.ToString(CultureInfo.InvariantCulture)}]"; - - /// - /// Gets the URL to the community profile page of the account using . - /// - /// https://steamcommunity.com/profiles/76561198110222274 - public string GetProfileUrl() => $"{Constants.SteamCommunityBaseUrl}/profiles/{RawId}"; - - /// - /// Gets the URL to the community profile page of the account using the . - /// - /// https://steamcommunity.com/profiles/[U:1:149956546] - public string GetSteam3IdProfileUrl() => $"{Constants.SteamCommunityBaseUrl}/profiles/{Steam3Id}"; - - /// - public override string ToString() => Steam3Id; - - /// - public bool Equals(SteamId other) => RawId == other.RawId; - - /// - public int CompareTo(SteamId other) => RawId.CompareTo(other.RawId); - - /// - public override bool Equals(object? obj) - { - return obj is SteamId other && Equals(other); - } - - /// - public override int GetHashCode() => RawId.GetHashCode(); - - /// - /// Equality operator. - /// - public static bool operator ==(SteamId a, SteamId b) => a.Equals(b); - - /// - /// Inequality operator. - /// - public static bool operator !=(SteamId a, SteamId b) => !a.Equals(b); -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/SteamUniverse.cs b/src/GameFinder.StoreHandlers.Steam/Models/SteamUniverse.cs deleted file mode 100644 index 485868bd..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/SteamUniverse.cs +++ /dev/null @@ -1,39 +0,0 @@ -using JetBrains.Annotations; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Universes available for Steam Accounts. -/// -/// -/// The values for these enums were sourced from https://partner.steamgames.com/doc/api/steam_api#EUniverse -/// and https://developer.valvesoftware.com/wiki/SteamID#Universes_Available_for_Steam_Accounts. -/// -[PublicAPI] -public enum SteamUniverse : uint -{ - /// - /// Invalid. - /// - Invalid = 0, - - /// - /// The standard public universe. - /// - Public = 1, - - /// - /// Beta universe used inside Valve. - /// - Beta = 2, - - /// - /// Internal universe used inside Valve. - /// - Internal = 3, - - /// - /// Dev universe used inside Valve. - /// - Dev = 4, -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/AppId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/AppId.cs deleted file mode 100644 index 51184b82..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/AppId.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Globalization; -using System.Web; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a 32-bit unsigned integer unique identifier of an app. -/// -/// 262060 -[PublicAPI] -[ValueObject] -public readonly partial struct AppId : IAugmentWith -{ - /// - public static AppId DefaultValue { get; } = From(0); - - /// - /// Gets the URL to the SteamDB page of the app associated with this id. - /// - /// https://steamdb.info/app/262060 - public string GetSteamDbUrl() => $"{Constants.SteamDbBaseUrl}/app/{Value.ToString(CultureInfo.InvariantCulture)}"; - - /// - /// Gets the URL to the Steam Store page of the app associated with this id and with additional UTM parameters. - /// - /// - /// Setting the UTM source parameter helps developers identify what links to their app. - /// See https://partner.steamgames.com/doc/marketing/utm_analytics for more details. - /// - /// The current source. This should be the name of your app. - /// - /// - /// https://store.steampowered.com/app/262060 or - /// https://store.steampowered.com/app/262060/?utm_source=MyApp - /// - public string GetSteamStoreUrl(string? source = null) - { - var url = $"{Constants.SteamStoreBaseUrl}/app/{Value.ToString(CultureInfo.InvariantCulture)}"; - return source is null ? url : $"{url}/?utm_source={HttpUtility.UrlEncode(source)}"; - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/BuildId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/BuildId.cs deleted file mode 100644 index a5dc96ad..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/BuildId.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Globalization; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a 32-bit unsigned integer unique identifier for a build. -/// -/// 9545898 -[PublicAPI] -[ValueObject] -public readonly partial struct BuildId : IAugmentWith -{ - /// - public static BuildId DefaultValue { get; } = From(0); - - /// - /// Gets the URL to the SteamDB Update Notes for the build associated with this id. - /// - /// https://steamdb.info/patchnotes/9545898 - public string GetSteamDbUpdateNotesUrl() => $"{Constants.SteamDbBaseUrl}/patchnotes/{Value.ToString(CultureInfo.InvariantCulture)}"; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/DepotId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/DepotId.cs deleted file mode 100644 index 4585287d..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/DepotId.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Globalization; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a 32-bit unsigned integer unique identifier for a depot. -/// -/// 262061 -[PublicAPI] -[ValueObject] -public readonly partial struct DepotId : IAugmentWith -{ - /// - public static DepotId DefaultValue { get; } = From(0); - - /// - /// Gets the URL to the SteamDB page of this depot. - /// - /// https://steamdb.info/depot/262061 - public string GetSteamDbUrl() => $"{Constants.SteamDbBaseUrl}/depot/{Value.ToString(CultureInfo.InvariantCulture)}"; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/ManifestId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/ManifestId.cs deleted file mode 100644 index 14e4f336..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/ManifestId.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a unique identifier for a manifest of a depot change. -/// -/// -/// I've seen conflicting info about this data type online. On one hand, you -/// have some random comment that suggests these values are actually -/// just strings (https://github.com/SteamDatabase/ValveKeyValue/pull/47#issuecomment-984605893), -/// and on another you have the Steam API, which uses uint64 for manifest IDs -/// (https://steamapi.xpaw.me/#IContentServerDirectoryService/GetDepotPatchInfo). -/// -/// 5542773349944116172 -[PublicAPI] -[ValueObject] -public readonly partial struct ManifestId : IAugmentWith -{ - /// - public static ManifestId DefaultValue { get; } = From(0); - - /// - /// Gets the URL to the SteamDB page for the Changeset of the manifest associated with this id. - /// - /// ID of the depot this manifest ID is a part of. - /// - /// https://steamdb.info/depot/262061/history/?changeid=M:5542773349944116172 - [SuppressMessage("ReSharper", "StringLiteralTypo")] - public string GetSteamDbChangesetUrl(DepotId depotId) - { - return $"{depotId.GetSteamDbUrl()}/history/?changeid=M:{Value}"; - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopItemId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopItemId.cs deleted file mode 100644 index 26edd6a1..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopItemId.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a 64-bit unsigned integer unique identifier of a workshop item. -/// -/// -/// Steam's APIs call this a "published file ID", see https://steamapi.xpaw.me/#IPublishedFileService/GetDetails -/// for reference, but calling this a "file identifier" only leads to confusion. This isn't the identifier -/// for a single file, this is the identifier for a Workshop Item, which can have multiple versions and multiple files. -/// -/// 942405260 -[PublicAPI] -[ValueObject] -public readonly partial struct WorkshopItemId : IAugmentWith -{ - /// - public static WorkshopItemId DefaultValue { get; } = From(0); - - /// - /// Gets the URL to the Steam Workshop page of this item. - /// - /// https://steamcommunity.com/sharedfiles/filedetails/?id=942405260 - [SuppressMessage("ReSharper", "StringLiteralTypo")] - public string GetSteamWorkshopUrl() => $"{Constants.SteamCommunityBaseUrl}/sharedfiles/filedetails/?id={Value.ToString(CultureInfo.InvariantCulture)}"; -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopManifestId.cs b/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopManifestId.cs deleted file mode 100644 index 2d4659a4..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/ValueTypes/WorkshopManifestId.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using TransparentValueObjects; - -namespace GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -/// -/// Represents a unique identifier for a manifest of a workshop item change. -/// -/// -/// Not to be confused with . -/// -/// 1625768140039815850 -/// -[PublicAPI] -[ValueObject] -public readonly partial struct WorkshopManifestId : IAugmentWith -{ - /// - public static WorkshopManifestId DefaultValue { get; } = From(0); -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/WorkshopItemDetails.cs b/src/GameFinder.StoreHandlers.Steam/Models/WorkshopItemDetails.cs deleted file mode 100644 index 4ca3fd0a..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/WorkshopItemDetails.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents an installed workshop item. -/// -/// -[PublicAPI] -public sealed record WorkshopItemDetails -{ - /// - /// Gets the unique identifier associated with this item. - /// - public required WorkshopItemId ItemId { get; init; } - - /// - /// Gets the size of the item on disk. - /// - public required Size SizeOnDisk { get; init; } - - /// - /// Gets the unique identifier of the change tracking manifest. - /// - public required WorkshopManifestId ManifestId { get; init; } - - /// - /// Gets the time when the item was last updated. - /// - /// - /// If this value is missing, - /// will be used as the default value. - /// - public DateTimeOffset LastUpdated { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets the time when the item was last "touched". - /// - /// - /// This value will be set to if the current - /// workshop item has been downloaded, but not applied yet. - /// - public DateTimeOffset LastTouched { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets the associated with the account that subscribed to this workshop item. - /// - /// - /// The value saved in the file is a raw 64-bit unsigned integer, but a 32-bit - /// unsigned integer that only represents the . - /// Due to this, is used. However, this - /// implies that the account universe is - /// and the account type is . - /// - /// 76561193815254978 - public SteamId SubscribedBy { get; init; } - - /// - /// Gets the URL to the Steam Workshop page of this item. - /// - /// https://steamcommunity.com/sharedfiles/filedetails/?id=942405260 - [SuppressMessage("ReSharper", "StringLiteralTypo")] - public string SteamWorkshopUrl => ItemId.GetSteamWorkshopUrl(); -} diff --git a/src/GameFinder.StoreHandlers.Steam/Models/WorkshopManifest.cs b/src/GameFinder.StoreHandlers.Steam/Models/WorkshopManifest.cs deleted file mode 100644 index 1f6eb8f7..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Models/WorkshopManifest.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Models; - -/// -/// Represents a parsed workshop manifest file. -/// -/// -/// Workshop manifest files appworkshop_*.acf use Valve's custom -/// KeyValue format. -/// -[PublicAPI] -public sealed record WorkshopManifest -{ - /// - /// Gets the to the appworkshop_*.acf file - /// that was parsed to produce this . - /// - /// E:/SteamLibrary/steamapps/workshop/appworkshop_262060.acf - [SuppressMessage("ReSharper", "CommentTypo")] - public required AbsolutePath ManifestPath { get; init; } - - #region Parsed Values - - /// - /// Gets the unique identifier of the app, this manifest is relevant to. - /// - public required AppId AppId { get; init; } - - /// - /// Gets the combined size of disk of all installed workshop items. - /// - public Size SizeOnDisk { get; init; } = Size.Zero; - - /// - /// Gets whether or not Steam needs to update the workshop items. - /// - public bool NeedsUpdate { get; init; } - - /// - /// Gets whether or not Steam needs to download some workshop items. - /// - public bool NeedsDownload { get; init; } - - /// - /// Gets the time when the workshop items were last updated. - /// - public DateTimeOffset LastUpdated { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets the time when the app was last started. - /// - /// - /// This value can be compared to to check whether - /// the latest update of the workshop item has been applied yet. - /// - public DateTimeOffset LastAppStart { get; init; } = DateTimeOffset.UnixEpoch; - - /// - /// Gets all installed workshop items. - /// - public IReadOnlyDictionary InstalledWorkshopItems { get; init; } = ImmutableDictionary.Empty; - - #endregion - - #region Helpers - - /// - /// Parses the file at again and returns a new - /// instance of . - /// - [Pure] - [System.Diagnostics.Contracts.Pure] - [MustUseReturnValue] - public Result Reload() - { - return WorkshopManifestParser.ParseManifestFile(ManifestPath); - } - - private static readonly RelativePath ContentDirectoryName = "content"; - private static readonly RelativePath DownloadsDirectoryName = "downloads"; - - /// - /// Gets the absolute path to the content directory. - /// - /// E:/SteamLibrary/steamapps/workshop/content/262060 - public AbsolutePath GetContentDirectoryPath() => ManifestPath.Parent - .Combine(ContentDirectoryName) - .Combine(AppId.Value.ToString(CultureInfo.InvariantCulture)); - - /// - /// Gets the absolute path to the downloads directory. - /// - /// E:/SteamLibrary/steamapps/workshop/downloads/262060 - public AbsolutePath GetDownloadsDirectoryPath() => ManifestPath.Parent - .Combine(DownloadsDirectoryName) - .Combine(AppId.Value.ToString(CultureInfo.InvariantCulture)); - - #endregion - - #region Overwrites - - /// - public bool Equals(WorkshopManifest? other) - { - if (other is null) return false; - if (AppId != other.AppId) return false; - if (SizeOnDisk != other.SizeOnDisk) return false; - if (NeedsUpdate != other.NeedsUpdate) return false; - if (NeedsDownload != other.NeedsDownload) return false; - if (LastUpdated != other.LastUpdated) return false; - if (LastAppStart != other.LastAppStart) return false; - if (!InstalledWorkshopItems.SequenceEqual(other.InstalledWorkshopItems)) return false; - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(AppId); - hashCode.Add(SizeOnDisk); - hashCode.Add(NeedsUpdate); - hashCode.Add(NeedsDownload); - hashCode.Add(LastUpdated); - hashCode.Add(LastAppStart); - hashCode.Add(InstalledWorkshopItems); - return hashCode.ToHashCode(); - } - - #endregion -} diff --git a/src/GameFinder.StoreHandlers.Steam/ProtonWinePrefix.cs b/src/GameFinder.StoreHandlers.Steam/ProtonWinePrefix.cs deleted file mode 100644 index ce18a82e..00000000 --- a/src/GameFinder.StoreHandlers.Steam/ProtonWinePrefix.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using GameFinder.Wine; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam; - -/// -/// Represents a Wine prefix managed by Valve's Proton library. -/// -[PublicAPI] -public record ProtonWinePrefix : AWinePrefix -{ - /// - /// This is the parent directory of . - /// This directory mostly contains metadata files created by Proton, the actual Wine - /// prefix is the sub directory pfx, which you can access using - /// . - /// - public required AbsolutePath ProtonDirectory { get; init; } - - /// - [SuppressMessage("ReSharper", "StringLiteralTypo")] - protected override string GetUserName() - { - return "steamuser"; - } - - /// - /// Returns the absolute path to the config_info file. - /// - /// - public AbsolutePath GetConfigInfoFile() - { - return ProtonDirectory.Combine("config_info"); - } - - /// - /// Returns the absolute path to the launch_command file. - /// - /// - public AbsolutePath GetLaunchCommandFile() - { - return ProtonDirectory.Combine("launch_command"); - } - - /// - /// Returns the absolute path to the version file. - /// - /// - public AbsolutePath GetVersionFile() - { - return ProtonDirectory.Combine("version"); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/AppManifestParser.cs b/src/GameFinder.StoreHandlers.Steam/Services/Parsers/AppManifestParser.cs deleted file mode 100644 index 18892e54..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/AppManifestParser.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using NexusMods.Paths.Utilities; -using ValveKeyValue; -using static GameFinder.StoreHandlers.Steam.Services.ParserHelpers; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Parser for appmanifest_*.acf files. -/// -/// -[PublicAPI] -public static class AppManifestParser -{ - /// - /// Parses the appmanifest_*.acf file at the given path. - /// - public static Result ParseManifestFile(AbsolutePath manifestPath) - { - if (!manifestPath.FileExists) - { - return Result.Fail(new Error("Manifest file doesn't exist!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - try - { - using var stream = manifestPath.Read(); - - var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - var appState = kv.Deserialize(stream, KVSerializerOptions.DefaultOptions); - - if (appState is null) - { - return Result.Fail( - new Error($"{nameof(KVSerializer)} returned null trying to parse the manifest file!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - if (!appState.Name.Equals("AppState", StringComparison.Ordinal)) - { - return Result.Fail( - new Error("Manifest file is potentially broken because the name doesn't match!") - .WithMetadata("Path", manifestPath.GetFullPath()) - .WithMetadata("ExpectedName", "AppState") - .WithMetadata("ActualName", appState.Name) - ); - } - - // NOTE (@erri120 on 2023-06-02): - // The ValveKeyValue package by SteamDB (https://github.com/SteamDatabase/ValveKeyValue) - // is currently "broken" and has multiple issues regarding parsing values - // of type "uint", "ulong" and "string". - // see the following links for more information: - // - https://github.com/SteamDatabase/ValveKeyValue/pull/47 - // - https://github.com/SteamDatabase/ValveKeyValue/issues/53 - // - https://github.com/SteamDatabase/ValveKeyValue/issues/73 - // Until those issues are resolved or I find another library, parsing is going to be broken. - - var appIdResult = ParseRequiredChildObject(appState, "appid", ParseAppId); - var universeResult = ParseOptionalChildObject(appState, "Universe", ParseUInt32, default).Map(x => (SteamUniverse)x); - var nameResult = ParseRequiredChildObject(appState, "name", ParseString); - var stateFlagsResult = ParseRequiredChildObject(appState, "StateFlags", ParseUInt32).Map(x => (StateFlags)x); - var installationDirectoryNameResult = ParseInstallationDirectory(appState, manifestPath.FileSystem, manifestPath); - var lastUpdatedResult = ParseOptionalChildObject(appState, "LastUpdated", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var sizeOnDiskResult = ParseOptionalChildObject(appState, "SizeOnDisk", ParseSize, Size.Zero); - var stagingSizeResult = ParseOptionalChildObject(appState, "StagingSize", ParseSize, Size.Zero); - var buildIdResult = ParseOptionalChildObject(appState, "buildid", ParseBuildId, BuildId.DefaultValue); - var lastOwnerResult = ParseOptionalChildObject(appState, "LastOwner", ParseSteamId, SteamId.Empty); - var updateResult = ParseOptionalChildObject(appState, "UpdateResult", ParseUInt32, default); - var bytesToDownloadResult = ParseOptionalChildObject(appState, "BytesToDownload", ParseSize, Size.Zero); - var bytesDownloadedResult = ParseOptionalChildObject(appState, "BytesDownloaded", ParseSize, Size.Zero); - var bytesToStageResult = ParseOptionalChildObject(appState, "BytesToStage", ParseSize, Size.Zero); - var bytesStagedResult = ParseOptionalChildObject(appState, "BytesStaged", ParseSize, Size.Zero); - var targetBuildIdResult = ParseOptionalChildObject(appState, "TargetBuildID", ParseBuildId, BuildId.DefaultValue); - var autoUpdateBehaviorResult = ParseOptionalChildObject(appState, "AutoUpdateBehavior", ParseByte, default).Map(x => (AutoUpdateBehavior)x); - var backgroundDownloadBehaviorResult = ParseOptionalChildObject(appState, "AllowOtherDownloadsWhileRunning", ParseByte, default).Map(x => (BackgroundDownloadBehavior)x); - var scheduledAutoUpdateResult = ParseOptionalChildObject(appState, "ScheduledAutoUpdate", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var fullValidateAfterNextUpdateResult = ParseOptionalChildObject(appState, "FullValidateAfterNextUpdate", ParseBool, default); - - var installedDepotsResult = ParseInstalledDepots(appState); - var installScriptsResult = ParseBasicDictionary( - appState, - "InstallScripts", - key => DepotId.From(uint.Parse(key)), - x => ParseRelativePath(x, manifestPath.FileSystem)); - - var sharedDepotsResult = ParseBasicDictionary( - appState, - "SharedDepots", - key => DepotId.From(uint.Parse(key)), - ParseAppId); - - var userConfigResult = ParseBasicDictionary( - appState, - "UserConfig", - key => key, - ParseString, - StringComparer.OrdinalIgnoreCase); - - var mountedConfigResult = ParseBasicDictionary( - appState, - "MountedConfig", - key => key, - ParseString, - StringComparer.OrdinalIgnoreCase); - - var mergedResults = Result.Merge( - appIdResult, - universeResult, - nameResult, - stateFlagsResult, - installationDirectoryNameResult, - lastUpdatedResult, - sizeOnDiskResult, - stagingSizeResult, - buildIdResult, - lastOwnerResult, - updateResult, - bytesToDownloadResult, - bytesDownloadedResult, - bytesToStageResult, - bytesStagedResult, - targetBuildIdResult, - autoUpdateBehaviorResult, - backgroundDownloadBehaviorResult, - scheduledAutoUpdateResult, - fullValidateAfterNextUpdateResult, - installedDepotsResult, - installScriptsResult, - sharedDepotsResult, - userConfigResult, - mountedConfigResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - return Result.Ok( - new AppManifest - { - ManifestPath = manifestPath, - AppId = appIdResult.Value, - Universe = universeResult.Value, - Name = nameResult.Value, - StateFlags = stateFlagsResult.Value, - InstallationDirectory = installationDirectoryNameResult.Value, - - LastUpdated = lastUpdatedResult.Value, - SizeOnDisk = sizeOnDiskResult.Value, - StagingSize = stagingSizeResult.Value, - BuildId = buildIdResult.Value, - LastOwner = lastOwnerResult.Value, - UpdateResult = updateResult.Value, - BytesToDownload = bytesToDownloadResult.Value, - BytesDownloaded = bytesDownloadedResult.Value, - BytesToStage = bytesToStageResult.Value, - BytesStaged = bytesStagedResult.Value, - TargetBuildId = targetBuildIdResult.Value, - AutoUpdateBehavior = autoUpdateBehaviorResult.Value, - BackgroundDownloadBehavior = backgroundDownloadBehaviorResult.Value, - ScheduledAutoUpdate = scheduledAutoUpdateResult.Value, - FullValidateAfterNextUpdate = fullValidateAfterNextUpdateResult.Value, - - InstalledDepots = installedDepotsResult.Value, - InstallScripts = installScriptsResult.Value, - SharedDepots = sharedDepotsResult.Value, - UserConfig = userConfigResult.Value, - MountedConfig = mountedConfigResult.Value, - } - ); - } - catch (Exception ex) - { - return Result.Fail( - new ExceptionalError("Exception was thrown while parsing the manifest file!", ex) - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - } - - private static Result ParseInstallationDirectory(KVObject appState, IFileSystem fileSystem, AbsolutePath manifestPath) - { - var installDirectoryResult = FindRequiredChildObject(appState, "installdir"); - if (installDirectoryResult.IsFailed) return installDirectoryResult.ToResult(); - - var parseResult = ParseChildObjectValue(installDirectoryResult.Value, appState, ParseString); - if (parseResult.IsFailed) return parseResult.ToResult(); - - var rawPath = parseResult.Value; - var sanitizedPath = PathHelpers.Sanitize(rawPath, fileSystem.OS); - var isRelative = PathHelpers.GetRootLength(sanitizedPath, fileSystem.OS) == -1; - - if (isRelative) - { - var relativePath = new RelativePath(sanitizedPath); - return Result.Ok(manifestPath.Parent.Combine("common").Combine(relativePath)); - } - - var absolutePath = fileSystem.FromUnsanitizedFullPath(rawPath); - return absolutePath; - } - - private static Result> ParseInstalledDepots(KVObject appState) - { - var installedDepotsObject = FindOptionalChildObject(appState, "InstalledDepots"); - if (installedDepotsObject is null) - { - return Result.Ok( - (IReadOnlyDictionary)ImmutableDictionary.Empty - ); - } - - var installedDepotResults = installedDepotsObject.Children - .Select(ParseInstalledDepot) - .ToArray(); - - var mergedResults = Result.Merge(installedDepotResults); - return mergedResults.Bind(installedDepots => - Result.Ok( - (IReadOnlyDictionary)installedDepots - .ToDictionary(x => x.DepotId, x => x) - ) - ); - } - - private static Result ParseInstalledDepot(KVObject depotObject) - { - if (!uint.TryParse(depotObject.Name, NumberFormatInfo.InvariantInfo, out var rawDepotId)) - { - return Result.Fail( - new Error("Unable to parse Depot name as a 32-bit unsigned integer!") - .WithMetadata("OriginalName", depotObject.Name) - ); - } - - var depotId = DepotId.From(rawDepotId); - - var manifestIdResult = ParseRequiredChildObject(depotObject, "manifest", ParseManifestId); - var sizeOnDiskResult = ParseRequiredChildObject(depotObject, "size", ParseSize); - var dlcAppIdResult = ParseOptionalChildObject(depotObject, "dlcappid", ParseAppId, AppId.DefaultValue); - - var mergedResults = Result.Merge( - manifestIdResult, - sizeOnDiskResult, - dlcAppIdResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - var installedDepot = new InstalledDepot - { - DepotId = depotId, - ManifestId = manifestIdResult.Value, - SizeOnDisk = sizeOnDiskResult.Value, - DLCAppId = dlcAppIdResult.Value, - }; - - return Result.Ok(installedDepot); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LibraryFoldersManifestParser.cs b/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LibraryFoldersManifestParser.cs deleted file mode 100644 index 5ca02ee0..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LibraryFoldersManifestParser.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; -using static GameFinder.StoreHandlers.Steam.Services.ParserHelpers; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Parser for libraryfolders.vdf files. -/// -/// -[PublicAPI] -public static class LibraryFoldersManifestParser -{ - /// - /// Parses the libraryfolders.vdf file at the given path. - /// - public static Result ParseManifestFile(AbsolutePath manifestPath) - { - if (!manifestPath.FileExists) - { - return Result.Fail(new Error("Manifest file doesn't exist!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - try - { - using var stream = manifestPath.Read(); - - var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - var data = kv.Deserialize(stream, KVSerializerOptions.DefaultOptions); - - if (data is null) - { - return Result.Fail( - new Error($"{nameof(KVSerializer)} returned null trying to parse the manifest file!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - if (!data.Name.Equals("libraryfolders", StringComparison.Ordinal)) - { - return Result.Fail( - new Error("Manifest file is potentially broken because the name doesn't match!") - .WithMetadata("Path", manifestPath.GetFullPath()) - .WithMetadata("ExpectedName", "libraryfolders") - .WithMetadata("ActualName", data.Name) - ); - } - - var libraryFoldersResult = ParseLibraryFolders(data, manifestPath.FileSystem); - if (libraryFoldersResult.IsFailed) return libraryFoldersResult.ToResult(); - - return Result.Ok( - new LibraryFoldersManifest - { - ManifestPath = manifestPath, - LibraryFolders = libraryFoldersResult.Value, - } - ); - } - catch (Exception ex) - { - return Result.Fail( - new ExceptionalError("Exception was thrown while parsing the manifest file!", ex) - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - } - - private static Result> ParseLibraryFolders(KVObject data, IFileSystem fileSystem) - { - var libraryFolderResults = data.Children - .Select(c => ParseLibraryFolder(c, fileSystem)) - .ToArray(); - - return Result.Merge(libraryFolderResults).Bind(e => Result.Ok((IReadOnlyList)e.ToList())); - } - - private static Result ParseLibraryFolder(KVObject parent, IFileSystem fileSystem) - { - var pathResult = ParseRequiredChildObject(parent, "path", value => ParseAbsolutePath(value, fileSystem)); - var labelResult = ParseOptionalChildObject(parent, "label", ParseString, string.Empty); - var totalSizeResult = ParseOptionalChildObject(parent, "totalsize", ParseSize, Size.Zero); - - var appSizesResult = ParseBasicDictionary( - parent, - "apps", - key => AppId.From(uint.Parse(key)), - ParseSize); - - var mergedResults = Result.Merge( - pathResult, - labelResult, - totalSizeResult, - appSizesResult - ); - - if (mergedResults.IsFailed) return mergedResults; - return Result.Ok( - new LibraryFolder - { - Path = pathResult.Value, - Label = labelResult.Value, - TotalDiskSize = totalSizeResult.Value, - AppSizes = appSizesResult.Value, - } - ); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LocalUserConfigParser.cs b/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LocalUserConfigParser.cs deleted file mode 100644 index fcbfb5a6..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/LocalUserConfigParser.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; -using static GameFinder.StoreHandlers.Steam.Services.ParserHelpers; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Parser for -/// -/// -[PublicAPI] -public static class LocalUserConfigParser -{ - /// - /// Parses the local user config file. - /// - public static Result ParseConfigFile(SteamId steamId, AbsolutePath configPath) - { - if (!configPath.FileExists) - { - return Result.Fail(new Error("Config file doesn't exist!") - .WithMetadata("Path", configPath.GetFullPath()) - ); - } - - try - { - using var stream = configPath.Read(); - - var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - var localConfigStore = kv.Deserialize(stream, KVSerializerOptions.DefaultOptions); - - if (localConfigStore is null) - { - return Result.Fail( - new Error($"{nameof(KVSerializer)} returned null trying to parse the config file!") - .WithMetadata("Path", configPath.GetFullPath()) - ); - } - - if (!localConfigStore.Name.Equals("UserLocalConfigStore", StringComparison.Ordinal)) - { - return Result.Fail( - new Error("Config file is potentially broken because the name doesn't match!") - .WithMetadata("Path", configPath.GetFullPath()) - .WithMetadata("ExpectedName", "UserLocalConfigStore") - .WithMetadata("ActualName", localConfigStore.Name) - ); - } - - var localAppDataResult = ParseLocalAppData(localConfigStore); - - var webStorageObject = FindOptionalChildObject(localConfigStore, "WebStorage"); - var systemObject = FindOptionalChildObject(webStorageObject, "system"); - var inGameOverlayScreenshotSaveUncompressedPathResult = systemObject is null - ? Result.Ok(value: null) - : ParseOptionalChildObject( - systemObject, - "InGameOverlayScreenshotSaveUncompressedPath", - x => (AbsolutePath?)ParseAbsolutePath(x, configPath.FileSystem), - defaultValue: null - ); - - var mergedResults = Result.Merge( - localAppDataResult, - inGameOverlayScreenshotSaveUncompressedPathResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - return Result.Ok( - new LocalUserConfig - { - ConfigPath = configPath, - User = steamId, - LocalAppData = localAppDataResult.Value, - InGameOverlayScreenshotSaveUncompressedPath = inGameOverlayScreenshotSaveUncompressedPathResult.Value, - } - ); - } - catch (Exception ex) - { - return Result.Fail( - new ExceptionalError("Exception was thrown while parsing the config file!", ex) - .WithMetadata("Path", configPath.GetFullPath()) - ); - } - } - - private static Result> ParseLocalAppData(KVObject localConfigStore) - { - var softwareResult = FindRequiredChildObject(localConfigStore, "Software"); - if (softwareResult.IsFailed) return softwareResult.ToResult(); - - var valveResult = FindRequiredChildObject(softwareResult.Value, "Valve"); - if (valveResult.IsFailed) return valveResult.ToResult(); - - var steamResult = FindRequiredChildObject(valveResult.Value, "Steam"); - if (steamResult.IsFailed) return steamResult.ToResult(); - - var appsResult = FindRequiredChildObject(steamResult.Value, "apps"); - if (appsResult.IsFailed) return appsResult.ToResult(); - - var appResults = appsResult.Value.Children - .Select(ParseSingleLocalAppData) - .ToArray(); - - var mergedResults = Result.Merge(appResults); - return mergedResults.Bind(appData => - Result.Ok( - (IReadOnlyDictionary)appData - .ToDictionary(x => x.AppId, x => x) - ) - ); - } - - private static Result ParseSingleLocalAppData(KVObject appObject) - { - if (!uint.TryParse(appObject.Name, NumberFormatInfo.InvariantInfo, out var rawAppId)) - { - return Result.Fail( - new Error("Unable to parse AppId as a 32-bit unsigned integer!") - .WithMetadata("OriginalName", appObject.Name) - ); - } - - var appId = AppId.From(rawAppId); - - var lastPlayedResult = ParseOptionalChildObject(appObject, "LastPlayed", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var playtimeResult = ParseOptionalChildObject(appObject, "Playtime", ParseUInt32, default).Map(x => TimeSpan.FromMinutes(x)); - var launchOptionsResult = ParseOptionalChildObject(appObject, "LaunchOptions", ParseString, string.Empty); - - var mergedResults = Result.Merge( - lastPlayedResult, - playtimeResult, - launchOptionsResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - var localAppData = new LocalAppData - { - AppId = appId, - LastPlayed = lastPlayedResult.Value, - Playtime = playtimeResult.Value, - LaunchOptions = launchOptionsResult.Value, - }; - - return Result.Ok(localAppData); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/ParserHelpers.cs b/src/GameFinder.StoreHandlers.Steam/Services/Parsers/ParserHelpers.cs deleted file mode 100644 index ce238269..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/ParserHelpers.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using NexusMods.Paths; -using NexusMods.Paths.Utilities; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -internal static class ParserHelpers -{ - #region Core Parsers - - internal static Result> ParseBasicDictionary( - KVObject parentObject, - string dictionaryObjectName, - Func keyParser, - Func valueParser, - IEqualityComparer? equalityComparer = null) - where TKey : notnull - { - var dictionaryObject = FindOptionalChildObject(parentObject, dictionaryObjectName); - if (dictionaryObject is null) - { - return Result.Ok( - (IReadOnlyDictionary)ImmutableDictionary.Empty - ); - } - - var dictionaryValueResults = dictionaryObject.Children - .Select>>(childObject => - { - var keyResult = Result.Try(() => keyParser(childObject.Name)); - var valueResult = ParseValue(childObject.Value, valueParser); - - var mergedResult = Result.Merge( - keyResult, - valueResult - ); - - if (mergedResult.IsFailed) return mergedResult; - return Result.Ok(new KeyValuePair(keyResult.Value, valueResult.Value)); - }).ToArray(); - - var mergedResults = Result.Merge(dictionaryValueResults); - return mergedResults.Bind(values => Result.Ok( - (IReadOnlyDictionary)values.ToDictionary(x => x.Key, x => x.Value, equalityComparer) - ) - ); - } - - internal static Result ParseValue(KVValue value, Func parser) - { - return Result.Try( - () => parser(value), - ex => new ExceptionalError("Unable to parse value!", ex) - ); - } - - internal static Result ParseChildObjectValue( - KVObject childObject, - KVObject parentObject, - Func parser) - { - return Result.Try( - () => parser(childObject.Value), - ex => new ExceptionalError("Unable to parse value of child object!", ex) - .WithMetadata("ChildObjectName", childObject.Name) - .WithMetadata("ParentObjectName", parentObject.Name) - ); - } - - internal static KVObject? FindOptionalChildObject(KVObject? parentObject, string childObjectName) - { - if (parentObject is null) return null; - - var childObject = parentObject - .Children - .FirstOrDefault(child => child.Name.Equals(childObjectName, StringComparison.OrdinalIgnoreCase)); - - if (childObject is null && Debugger.IsLogging()) - { - Debugger.Log(0, Debugger.DefaultCategory, $"Optional child object {childObjectName} was not found in {parentObject.Name}"); - } - - return childObject; - } - - internal static Result ParseOptionalChildObject( - KVObject parentObject, - string childObjectName, - Func parser, - T defaultValue) - { - var childObject = FindOptionalChildObject(parentObject, childObjectName); - return childObject is null - ? Result.Ok(defaultValue) - : ParseChildObjectValue(childObject, parentObject, parser); - } - - internal static Result FindRequiredChildObject(KVObject parentObject, string childObjectName) - { - var childObject = FindOptionalChildObject(parentObject, childObjectName); - - if (childObject is null) - { - return Result.Fail( - new Error("Unable to find required child object by name in parent!") - .WithMetadata("ChildObjectName", childObjectName) - .WithMetadata("ParentObjectName", parentObject.Name) - ); - } - - return Result.Ok(childObject); - } - - internal static Result ParseRequiredChildObject( - KVObject parentObject, - string childObjectName, - Func parser) - { - var childObjectResult = FindRequiredChildObject(parentObject, childObjectName); - return childObjectResult.Bind(childObject => ParseChildObjectValue(childObject, parentObject, parser)); - } - - #endregion - - #region Type Parser - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static byte ParseByte(KVValue value) => byte.Parse(ParseString(value), CultureInfo.InvariantCulture); - - internal static bool ParseBool(KVValue value) - { - var s = ParseString(value); - if (string.Equals(s, "0", StringComparison.Ordinal)) return false; - if (string.Equals(s, "1", StringComparison.Ordinal)) return true; - throw new FormatException($"Unable to parse '{value}' as a boolean!"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static uint ParseUInt32(KVValue value) => uint.Parse(ParseString(value), CultureInfo.InvariantCulture); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong ParseUInt64(KVValue value) => ulong.Parse(ParseString(value), CultureInfo.InvariantCulture); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static string ParseString(KVValue value) => value.ToString(CultureInfo.InvariantCulture); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static DateTimeOffset ParseDateTimeOffset(KVValue value) => DateTimeOffset.FromUnixTimeSeconds(ParseUInt32(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static SteamId ParseSteamId(KVValue value) => SteamId.From(ParseUInt64(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static AppId ParseAppId(KVValue value) => AppId.From(ParseUInt32(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static BuildId ParseBuildId(KVValue value) => BuildId.From(ParseUInt32(value)); - - // [MethodImpl(MethodImplOptions.AggressiveInlining)] - // internal static DepotId ParseDepotId(KVValue value) => DepotId.From(ParseUInt32(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ManifestId ParseManifestId(KVValue value) => ManifestId.From(ParseUInt64(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static WorkshopManifestId ParseWorkshopManifestId(KVValue value) => WorkshopManifestId.From(ParseUInt64(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Size ParseSize(KVValue value) => Size.From(ParseUInt64(value)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static RelativePath ParseRelativePath(KVValue value, IFileSystem fileSystem) => new(PathHelpers.Sanitize(ParseString(value), fileSystem.OS)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static AbsolutePath ParseAbsolutePath(KVValue value, IFileSystem fileSystem) => fileSystem.FromUnsanitizedFullPath(ParseString(value)); - - #endregion -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/WorkshopManifestParser.cs b/src/GameFinder.StoreHandlers.Steam/Services/Parsers/WorkshopManifestParser.cs deleted file mode 100644 index bd009a90..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Parsers/WorkshopManifestParser.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; -using static GameFinder.StoreHandlers.Steam.Services.ParserHelpers; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Parser for appworkshop_*.acf files. -/// -/// -[PublicAPI] -public static class WorkshopManifestParser -{ - /// - /// Parses the appworkshop_*.acf file at the given path. - /// - public static Result ParseManifestFile(AbsolutePath manifestPath) - { - if (!manifestPath.FileExists) - { - return Result.Fail(new Error("Manifest file doesn't exist!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - try - { - using var stream = manifestPath.Read(); - - var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - var appWorkshop = kv.Deserialize(stream, KVSerializerOptions.DefaultOptions); - - if (appWorkshop is null) - { - return Result.Fail( - new Error($"{nameof(KVSerializer)} returned null trying to parse the manifest file!") - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - - if (!appWorkshop.Name.Equals("AppWorkshop", StringComparison.Ordinal)) - { - return Result.Fail( - new Error("Manifest file is potentially broken because the name doesn't match!") - .WithMetadata("Path", manifestPath.GetFullPath()) - .WithMetadata("ExpectedName", "AppWorkshop") - .WithMetadata("ActualName", appWorkshop.Name) - ); - } - - var appIdResult = ParseRequiredChildObject(appWorkshop, "appid", ParseAppId); - var sizeOnDiskResult = ParseOptionalChildObject(appWorkshop, "SizeOnDisk", ParseSize, Size.Zero); - var needsUpdateResult = ParseOptionalChildObject(appWorkshop, "NeedsUpdate", ParseBool, default); - var needsDownloadResult = ParseOptionalChildObject(appWorkshop, "NeedsDownload", ParseBool, default); - var lastUpdatedResult = ParseOptionalChildObject(appWorkshop, "TimeLastUpdated", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var lastAppStartResult = ParseOptionalChildObject(appWorkshop, "TimeLastAppRan", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - - var installedWorkshopItems = ParseInstalledWorkshopItems(appWorkshop); - - var mergedResults = Result.Merge( - appIdResult, - sizeOnDiskResult, - needsUpdateResult, - needsDownloadResult, - lastUpdatedResult, - lastAppStartResult, - installedWorkshopItems - ); - - if (mergedResults.IsFailed) return mergedResults; - - return Result.Ok( - new WorkshopManifest - { - ManifestPath = manifestPath, - AppId = appIdResult.Value, - SizeOnDisk = sizeOnDiskResult.Value, - NeedsUpdate = needsUpdateResult.Value, - NeedsDownload = needsDownloadResult.Value, - LastUpdated = lastUpdatedResult.Value, - LastAppStart = lastAppStartResult.Value, - InstalledWorkshopItems = installedWorkshopItems.Value, - } - ); - } - catch (Exception ex) - { - return Result.Fail( - new ExceptionalError("Exception was thrown while parsing the manifest file!", ex) - .WithMetadata("Path", manifestPath.GetFullPath()) - ); - } - } - - private static Result> ParseInstalledWorkshopItems(KVObject appWorkshop) - { - var installedWorkshopItemsObject = FindOptionalChildObject(appWorkshop, "WorkshopItemsInstalled"); - if (installedWorkshopItemsObject is null) - { - return Result.Ok( - (IReadOnlyDictionary)ImmutableDictionary.Empty - ); - } - - var installedWorkshopItemResults = installedWorkshopItemsObject.Children - .Select(ParseInstalledWorkshopItem) - .ToArray(); - - var mergedResults = Result.Merge(installedWorkshopItemResults); - if (mergedResults.IsFailed) return mergedResults.ToResult(); - - var workshopItemDetailsObject = FindOptionalChildObject(appWorkshop, "WorkshopItemDetails"); - if (workshopItemDetailsObject is null) - { - return Result.Ok( - (IReadOnlyDictionary)mergedResults.Value.ToDictionary(x => x.ItemId, x => x) - ); - } - - var installedWorkshopItems = mergedResults.Value!.ToList(); - installedWorkshopItemResults = workshopItemDetailsObject.Children - .Select(workshopItemDetailObject => ParseWorkshopItemDetails(workshopItemDetailObject, installedWorkshopItems)) - .ToArray(); - - mergedResults = Result.Merge(installedWorkshopItemResults); - if (mergedResults.IsFailed) return mergedResults.ToResult(); - - return Result.Ok( - (IReadOnlyDictionary)mergedResults.Value.ToDictionary(x => x.ItemId, x => x) - ); - } - - private static Result ParseInstalledWorkshopItem(KVObject installedWorkshopItemObject) - { - if (!ulong.TryParse(installedWorkshopItemObject.Name, NumberFormatInfo.InvariantInfo, out var rawWorkshopItemId)) - { - return Result.Fail( - new Error("Unable to parse WorkshopItem name as a 64-bit unsigned integer!") - .WithMetadata("OriginalName", installedWorkshopItemObject.Name) - ); - } - - var workshopItemId = WorkshopItemId.From(rawWorkshopItemId); - - var sizeOnDiskResult = ParseRequiredChildObject(installedWorkshopItemObject, "size", ParseSize); - var lastUpdatedResult = ParseOptionalChildObject(installedWorkshopItemObject, "timeupdated", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var manifestResult = ParseRequiredChildObject(installedWorkshopItemObject, "manifest", ParseWorkshopManifestId); - - var mergedResults = Result.Merge( - sizeOnDiskResult, - lastUpdatedResult, - manifestResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - return Result.Ok( - new WorkshopItemDetails - { - ItemId = workshopItemId, - SizeOnDisk = sizeOnDiskResult.Value, - ManifestId = manifestResult.Value, - LastUpdated = lastUpdatedResult.Value, - } - ); - } - - private static Result ParseWorkshopItemDetails( - KVObject workshopItemDetailObject, - IEnumerable installedWorkshopItems) - { - if (!ulong.TryParse(workshopItemDetailObject.Name, NumberFormatInfo.InvariantInfo, out var rawWorkshopItemId)) - { - return Result.Fail( - new Error("Unable to parse WorkshopItem name as a 64-bit unsigned integer!") - .WithMetadata("OriginalName", workshopItemDetailObject.Name) - ); - } - - var workshopItemId = WorkshopItemId.From(rawWorkshopItemId); - - var manifestResult = ParseRequiredChildObject(workshopItemDetailObject, "manifest", ParseWorkshopManifestId); - var lastUpdatedResult = ParseOptionalChildObject(workshopItemDetailObject, "timeupdated", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var lastTouchedResult = ParseOptionalChildObject(workshopItemDetailObject, "timetouched", ParseDateTimeOffset, DateTimeOffset.UnixEpoch); - var subscribedByResult = ParseOptionalChildObject(workshopItemDetailObject, "subscribedby", ParseUInt32, default).Map(x => SteamId.FromAccountId(x)); - - var mergedResults = Result.Merge( - manifestResult, - lastUpdatedResult, - lastTouchedResult, - subscribedByResult - ); - - if (mergedResults.IsFailed) return mergedResults; - - var installedWorkshopItem = installedWorkshopItems.FirstOrDefault(x => x.ItemId == workshopItemId); - if (installedWorkshopItem is null) - { - return Result.Fail( - new Error("Unable to find previously parsed installed workshop item!") - .WithMetadata("WorkshopItemId", workshopItemId) - ); - } - - return Result.Ok( - installedWorkshopItem with - { - ManifestId = manifestResult.Value, - LastUpdated = lastUpdatedResult.Value, - LastTouched = lastTouchedResult.Value, - SubscribedBy = subscribedByResult.Value - } - ); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/SteamLocationFinder.cs b/src/GameFinder.StoreHandlers.Steam/Services/SteamLocationFinder.cs deleted file mode 100644 index d71ec254..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/SteamLocationFinder.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using FluentResults; -using GameFinder.RegistryUtils; -using GameFinder.StoreHandlers.Steam.Models; -using JetBrains.Annotations; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Finds the current Steam installation. -/// -[PublicAPI] -public static class SteamLocationFinder -{ - /// - /// The name of the config directory. - /// - /// - public static readonly RelativePath ConfigDirectoryName = "config".ToRelativePath(); - - /// - /// The name of the libraryfolders.vdf file. - /// - /// - public static readonly RelativePath LibraryFoldersFileName = "libraryfolders.vdf".ToRelativePath(); - - /// - /// The name of the userdata directory. - /// - /// - public static readonly RelativePath UserDataDirectoryName = "userdata".ToRelativePath(); - - /// - /// The registry key used to find Steam. - /// - /// - public const string SteamRegistryKey = @"Software\Valve\Steam"; - - /// - /// The registry value name to find Steam. - /// - /// - public const string SteamRegistryValueName = "SteamPath"; - - /// - /// Tries to find a valid Steam installation. - /// - /// - /// This uses , - /// and to find a valid installation. - /// - public static Result FindSteam(IFileSystem fileSystem, IRegistry? registry) - { - // 1) try the default installation paths - var defaultSteamInstallationPath = GetDefaultSteamInstallationPaths(fileSystem) - .FirstOrDefault(IsValidSteamInstallation); - - if (defaultSteamInstallationPath != default) return Result.Ok(defaultSteamInstallationPath); - - // 2) try the registry, if there is any - if (registry is null) - { - return Result.Fail( - new Error("Unable to find a valid Steam installation at the default installation paths!") - ); - } - - var pathFromRegistryResult = GetSteamPathFromRegistry(fileSystem, registry); - if (pathFromRegistryResult.IsFailed || !IsValidSteamInstallation(pathFromRegistryResult.Value)) - { - return Result.Merge( - Result.Fail( - new Error("Unable to find a valid Steam installation at the default installation paths, and in the Registry!") - ), - pathFromRegistryResult - ).ToResult(); - } - - return Result.Ok(pathFromRegistryResult.Value); - } - - /// - /// Checks whether the given Steam installation path is valid. - /// - /// - /// A valid Steam installation requires a existing directory, - /// and a existing libraryfolders.vdf file. This method - /// uses to get that file path. - /// - public static bool IsValidSteamInstallation(AbsolutePath steamPath) - { - if (!steamPath.DirectoryExists()) return false; - - var libraryFoldersFile = GetLibraryFoldersFilePath(steamPath); - return libraryFoldersFile.FileExists; - } - - /// - /// Returns the path to the libraryfolders.vdf file inside the Steam config - /// directory. - /// - public static AbsolutePath GetLibraryFoldersFilePath(AbsolutePath steamPath) - { - return steamPath - .Combine(ConfigDirectoryName) - .Combine(LibraryFoldersFileName); - } - - /// - /// Returns the path to the user data directory of the provided user. - /// - public static AbsolutePath GetUserDataDirectoryPath(AbsolutePath steamPath, SteamId steamId) - { - return steamPath - .Combine(UserDataDirectoryName) - .Combine(steamId.AccountId.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Tries to get the Steam installation path from the registry. - /// - public static Result GetSteamPathFromRegistry( - IFileSystem fileSystem, - IRegistry registry) - { - try - { - var currentUser = registry.OpenBaseKey(RegistryHive.CurrentUser); - - using var regKey = currentUser.OpenSubKey(SteamRegistryKey); - if (regKey is null) - { - return Result.Fail( - new Error("Unable to open the Steam registry key!") - .WithMetadata("RegistryKey", SteamRegistryKey) - ); - } - - if (!regKey.TryGetString(SteamRegistryValueName, out var steamPath)) - { - return Result.Fail( - new Error("Unable to get string value from the Steam registry key!") - .WithMetadata("RegistryKey", SteamRegistryKey) - .WithMetadata("ValueName", SteamRegistryValueName) - ); - } - - var directoryInfo = fileSystem.FromUnsanitizedFullPath(steamPath); - return directoryInfo; - } - catch (Exception e) - { - return Result.Fail( - new ExceptionalError("Exception thrown while getting the Steam installation path from the registry!", e) - ); - } - } - - /// - /// Returns all possible default Steam installation paths for the given platform. - /// - [SuppressMessage("ReSharper", "StringLiteralTypo")] - [SuppressMessage("ReSharper", "CommentTypo")] - public static IEnumerable GetDefaultSteamInstallationPaths(IFileSystem fileSystem) - { - if (fileSystem.OS.IsWindows) - { - yield return fileSystem - .GetKnownPath(KnownPath.ProgramFilesX86Directory) - .Combine("Steam"); - - yield break; - } - - if (fileSystem.OS.IsLinux) - { - // "$XDG_DATA_HOME/Steam" which is usually "~/.local/share/Steam" - yield return fileSystem - .GetKnownPath(KnownPath.LocalApplicationDataDirectory) - .Combine("Steam"); - - // "~/.steam/debian-installation" - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".steam") - .Combine("debian-installation"); - - // "~/.var/app/com.valvesoftware.Steam/data/Steam" (flatpak installation) - // see https://github.com/flatpak/flatpak/wiki/Filesystem for details - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".var/app/com.valvesoftware.Steam/data/Steam"); - - // "~/.steam/steam" - // this is a legacy installation directory and is often soft linked to - // the actual installation directory - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".steam") - .Combine("steam"); - - // "~/.steam" - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".steam"); - - // "~/.local/.steam" - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".local") - .Combine(".steam"); - - yield break; - } - - if (fileSystem.OS.IsOSX) - { - // ~/Library/Application Support/Steam - yield return fileSystem.GetKnownPath(KnownPath.LocalApplicationDataDirectory) - .Combine("Steam"); - - yield break; - } - - throw new PlatformNotSupportedException("GameFinder doesn't support the current platform!"); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Writers/AppManifestWriter.cs b/src/GameFinder.StoreHandlers.Steam/Services/Writers/AppManifestWriter.cs deleted file mode 100644 index b9bf4611..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Writers/AppManifestWriter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Writer for . -/// -/// -[PublicAPI] -public static class AppManifestWriter -{ - /// - /// Saves the manifest to file. - /// - public static Result Write(AppManifest manifest, AbsolutePath outputPath) - { - var values = new List(); - values.AddValue("appid", manifest.AppId, AppId.DefaultValue); - values.AddValue("Universe", (byte)manifest.Universe, -1); - values.AddValue("name", manifest.Name, string.Empty); - values.AddValue("StateFlags", (byte)manifest.StateFlags, -1); - values.AddValue("installdir", manifest.InstallationDirectory.Name.ToString(), string.Empty); - values.AddValue("LastUpdated", manifest.LastUpdated.ToUnixTimeSeconds(), default); - values.AddValue("SizeOnDisk", manifest.SizeOnDisk.Value, default); - values.AddValue("StagingSize", manifest.StagingSize.Value, default); - values.AddValue("buildid", manifest.BuildId, BuildId.DefaultValue); - values.AddValue("LastOwner", manifest.LastOwner.RawId, SteamId.Empty.RawId); - values.AddValue("UpdateResult", manifest.UpdateResult, default); - values.AddValue("BytesToDownload", manifest.BytesToDownload.Value, default); - values.AddValue("BytesDownloaded", manifest.BytesDownloaded.Value, default); - values.AddValue("BytesToStage", manifest.BytesToStage.Value, default); - values.AddValue("BytesStaged", manifest.BytesStaged.Value, default); - values.AddValue("TargetBuildID", manifest.TargetBuildId, BuildId.DefaultValue); - values.AddValue("AutoUpdateBehavior", (byte)manifest.AutoUpdateBehavior, -1); - values.AddValue("AllowOtherDownloadsWhileRunning", (byte)manifest.BackgroundDownloadBehavior, -1); - values.AddValue("ScheduledAutoUpdate", manifest.ScheduledAutoUpdate.ToUnixTimeSeconds(), default); - values.AddValue("FullValidateAfterNextUpdate", manifest.FullValidateAfterNextUpdate ? "1" : "0", string.Empty); - - if (manifest.InstalledDepots.Count != 0) - { - var children = new List(); - - foreach (var kv in manifest.InstalledDepots) - { - var (depotId, installedDepot) = kv; - var objValues = new List(); - objValues.AddValue("manifest", installedDepot.ManifestId, ManifestId.DefaultValue); - objValues.AddValue("size", installedDepot.SizeOnDisk.Value, default); - objValues.AddValue("dlcappid", installedDepot.DLCAppId, AppId.DefaultValue); - - var obj = new KVObject(depotId.ToString(), objValues); - children.Add(obj); - } - - values.Add(new KVObject("InstalledDepots", children)); - } - - values.AddDictionary("InstallScripts", manifest.InstallScripts, RelativePath.Empty); - values.AddDictionary("SharedDepots", manifest.SharedDepots, AppId.DefaultValue); - values.AddDictionary("UserConfig", manifest.UserConfig, string.Empty); - values.AddDictionary("MountedConfig", manifest.MountedConfig, string.Empty); - - var data = new KVObject("AppState", values); - - try - { - var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - using var stream = outputPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None); - serializer.Serialize(stream, data); - } - catch (Exception e) - { - return Result.Fail( - new ExceptionalError("Exception while writing the AppManifest to file!", e) - .WithMetadata("AppId", manifest.AppId) - .WithMetadata("Path", outputPath) - ); - } - - return Result.Ok(); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Writers/LibraryFoldersManifestWriter.cs b/src/GameFinder.StoreHandlers.Steam/Services/Writers/LibraryFoldersManifestWriter.cs deleted file mode 100644 index cbb5e0b3..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Writers/LibraryFoldersManifestWriter.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Writer for . -/// -/// -[PublicAPI] -public static class LibraryFoldersManifestWriter -{ - /// - /// Saves the manifest to file. - /// - public static Result Write(LibraryFoldersManifest manifest, AbsolutePath outputPath) - { - var values = new List(); - - for (var i = 0; i < manifest.Count; i++) - { - var libraryFolder = manifest[i]; - var children = new List(); - children.AddValue("path", libraryFolder.Path.ToString(), string.Empty); - children.AddValue("label", libraryFolder.Label, string.Empty); - children.AddValue("totalsize", libraryFolder.TotalDiskSize.Value, default); - children.AddDictionary("apps", libraryFolder.AppSizes - .Select(kv => new KeyValuePair(kv.Key, kv.Value.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value), - default - ); - - values.Add(new KVObject($"{i.ToString(CultureInfo.InvariantCulture)}", children)); - } - - var data = new KVObject("libraryfolders", values); - - try - { - var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - using var stream = outputPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None); - serializer.Serialize(stream, data); - } - catch (Exception e) - { - return Result.Fail( - new ExceptionalError("Exception while writing the Manifest to file!", e) - .WithMetadata("Path", outputPath) - ); - } - - return Result.Ok(); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Writers/LocalUserConfigWriter.cs b/src/GameFinder.StoreHandlers.Steam/Services/Writers/LocalUserConfigWriter.cs deleted file mode 100644 index 72040db7..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Writers/LocalUserConfigWriter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Writer for . -/// -/// -[PublicAPI] -public static class LocalUserConfigWriter -{ - /// - /// Saves the local user config to file. - /// - public static Result Write(LocalUserConfig config, AbsolutePath outputPath) - { - var values = new List(); - - var appDataObjects = config.LocalAppData.Select(x => - { - var (appId, data) = x; - - var innerValues = new List(); - innerValues.AddValue("LastPlayed", data.LastPlayed.ToUnixTimeSeconds(), default); - innerValues.AddValue("Playtime", (int)data.Playtime.TotalMinutes, default); - innerValues.AddValue("LaunchOptions", data.LaunchOptions, string.Empty); - - return new KVObject(appId.Value.ToString(CultureInfo.InvariantCulture), innerValues); - }); - - values.Add(new KVObject("Software", new[] - { - new KVObject("Valve", new [] - { - new KVObject("Steam", new [] - { - new KVObject("apps", appDataObjects), - }), - }), - })); - - var systemObjects = new List(); - systemObjects.AddValue("InGameOverlayScreenshotSaveUncompressedPath", config.InGameOverlayScreenshotSaveUncompressedPath?.ToString() ?? string.Empty, string.Empty); - - values.Add(new KVObject("WebStorage", new[] - { - new KVObject("system", systemObjects), - })); - - var data = new KVObject("UserLocalConfigStore", values); - - try - { - var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - using var stream = outputPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None); - serializer.Serialize(stream, data); - } - catch (Exception e) - { - return Result.Fail( - new ExceptionalError("Exception while writing the local user config to file!", e) - .WithMetadata("Path", outputPath) - ); - } - - return Result.Ok(); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Writers/WorkshopManifestWriter.cs b/src/GameFinder.StoreHandlers.Steam/Services/Writers/WorkshopManifestWriter.cs deleted file mode 100644 index 898e06f5..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Writers/WorkshopManifestWriter.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using FluentResults; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using JetBrains.Annotations; -using NexusMods.Paths; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -/// -/// Writer for . -/// -/// -[PublicAPI] -public class WorkshopManifestWriter -{ - /// - /// Saves the manifest to file. - /// - public static Result Write(WorkshopManifest manifest, AbsolutePath outputPath) - { - var values = new List(); - values.AddValue("appid", manifest.AppId, AppId.DefaultValue); - values.AddValue("SizeOnDisk", manifest.SizeOnDisk.Value, default); - values.AddValue("NeedsUpdate", manifest.NeedsUpdate ? "1" : "0", string.Empty); - values.AddValue("NeedsDownload", manifest.NeedsDownload ? "1" : "0", string.Empty); - values.AddValue("TimeLastUpdated", manifest.LastUpdated.ToUnixTimeSeconds(), default); - values.AddValue("TimeLastAppRan", manifest.LastAppStart.ToUnixTimeSeconds(), default); - - if (manifest.InstalledWorkshopItems.Count != 0) - { - var workshopItemsInstalledChildren = new List(); - var workshopItemDetailsChildren = new List(); - - foreach (var kv in manifest.InstalledWorkshopItems) - { - var (workshopItemId, workshopItemDetails) = kv; - - var workshopItemInstalledValues = new List(); - workshopItemInstalledValues.AddValue("size", workshopItemDetails.SizeOnDisk.Value, default); - workshopItemInstalledValues.AddValue("timeupdated", workshopItemDetails.LastUpdated.ToUnixTimeSeconds(), default); - workshopItemInstalledValues.AddValue("manifest", workshopItemDetails.ManifestId, WorkshopManifestId.DefaultValue); - - var workshopItemDetailsValues = new List(); - workshopItemDetailsValues.AddValue("manifest", workshopItemDetails.ManifestId, WorkshopManifestId.DefaultValue); - workshopItemDetailsValues.AddValue("timeupdated", workshopItemDetails.LastUpdated.ToUnixTimeSeconds(), default); - workshopItemDetailsValues.AddValue("timetouched", workshopItemDetails.LastTouched.ToUnixTimeSeconds(), default); - workshopItemDetailsValues.AddValue("subscribedby", workshopItemDetails.SubscribedBy.AccountId, default); - - workshopItemsInstalledChildren.Add(new KVObject(workshopItemId.ToString(), workshopItemInstalledValues)); - workshopItemDetailsChildren.Add(new KVObject(workshopItemId.ToString(), workshopItemDetailsValues)); - } - - values.Add(new KVObject("WorkshopItemsInstalled", workshopItemsInstalledChildren)); - values.Add(new KVObject("WorkshopItemDetails", workshopItemDetailsChildren)); - } - - var data = new KVObject("AppWorkshop", values); - - try - { - var serializer = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); - using var stream = outputPath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None); - serializer.Serialize(stream, data); - } - catch (Exception e) - { - return Result.Fail( - new ExceptionalError("Exception while writing the manifest to file!", e) - .WithMetadata("AppId", manifest.AppId) - .WithMetadata("Path", outputPath) - ); - } - - return Result.Ok(); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/Services/Writers/WriterHelpers.cs b/src/GameFinder.StoreHandlers.Steam/Services/Writers/WriterHelpers.cs deleted file mode 100644 index e2d23243..00000000 --- a/src/GameFinder.StoreHandlers.Steam/Services/Writers/WriterHelpers.cs +++ /dev/null @@ -1,70 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam.Services; - -internal static class WriterHelpers -{ - internal static void AddValue(this List list, string name, TValue value, TValue defaultValue) - where TValue : notnull - { - if (value.Equals(defaultValue)) return; - list.Add(new KVObject(name, new StringValue(value))); - } - - internal static void AddDictionary(this List list, string name, IReadOnlyDictionary dictionary, TValue defaultValue) - where TKey : notnull - where TValue : notnull - { - if (dictionary.Count == 0) return; - - var children = new List(); - foreach (var kv in dictionary) - { - children.AddValue(kv.Key.ToString()!, kv.Value, defaultValue); - } - - list.Add(new KVObject(name, children)); - } - - [ExcludeFromCodeCoverage] - private class StringValue : KVValue - { - private readonly string _value; - - public StringValue(string value) - { - _value = value; - } - - public StringValue(object obj) - { - _value = obj.ToString() ?? throw new ArgumentException($"Doesn't have a ToString: {obj.GetType()}", nameof(obj)); - } - - public override string ToString() => _value; - - public override KVValueType ValueType => KVValueType.String; - public override TypeCode GetTypeCode() => TypeCode.String; - - public override bool ToBoolean(IFormatProvider? provider) => Convert.ToBoolean(_value, provider); - public override byte ToByte(IFormatProvider? provider) => Convert.ToByte(_value, provider); - public override char ToChar(IFormatProvider? provider) => Convert.ToChar(_value, provider); - public override DateTime ToDateTime(IFormatProvider? provider) => Convert.ToDateTime(_value, provider); - public override decimal ToDecimal(IFormatProvider? provider) => Convert.ToDecimal(_value, provider); - public override double ToDouble(IFormatProvider? provider) => Convert.ToDouble(_value, provider); - public override short ToInt16(IFormatProvider? provider) => Convert.ToInt16(_value, provider); - public override int ToInt32(IFormatProvider? provider) => Convert.ToInt32(_value, provider); - public override long ToInt64(IFormatProvider? provider) => Convert.ToInt64(_value, provider); - public override sbyte ToSByte(IFormatProvider? provider) => Convert.ToSByte(_value, provider); - public override float ToSingle(IFormatProvider? provider) => Convert.ToSingle(_value, provider); - public override string ToString(IFormatProvider? provider) => Convert.ToString(_value, provider); - public override object ToType(Type conversionType, IFormatProvider? provider) => throw new NotSupportedException(); - public override ushort ToUInt16(IFormatProvider? provider) => Convert.ToUInt16(_value, provider); - public override uint ToUInt32(IFormatProvider? provider) => Convert.ToUInt32(_value, provider); - public override ulong ToUInt64(IFormatProvider? provider) => Convert.ToUInt64(_value, provider); - } -} diff --git a/src/GameFinder.StoreHandlers.Steam/SteamGame.cs b/src/GameFinder.StoreHandlers.Steam/SteamGame.cs deleted file mode 100644 index da2a479d..00000000 --- a/src/GameFinder.StoreHandlers.Steam/SteamGame.cs +++ /dev/null @@ -1,93 +0,0 @@ -using FluentResults; -using GameFinder.Common; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam; - -/// -/// Represents a game installed with Steam. -/// -[PublicAPI] -public sealed record SteamGame : IGame -{ - /// - /// Gets the parsed of this game. - /// - public required AppManifest AppManifest { get; init; } - - /// - /// Gets the library folder that contains this game. - /// - public required LibraryFolder LibraryFolder { get; init; } - - /// - /// Gets the path to the global Steam installation. - /// - public required AbsolutePath SteamPath { get; init; } - - #region Helpers - - /// - public AppId AppId => AppManifest.AppId; - - /// - public string Name => AppManifest.Name; - - /// - /// Gets the absolute path to the game's installation directory. - /// - public AbsolutePath Path => AppManifest.InstallationDirectory; - - /// - /// Gets the absolute path to the cloud saves directory. - /// - public AbsolutePath GetCloudSavesDirectoryPath() => AppManifest.GetUserDataDirectoryPath(SteamPath); - - /// - /// Gets the Wine prefix managed by Proton for this game, if it exists. - /// - public ProtonWinePrefix? GetProtonPrefix() - { - var protonDirectory = AppManifest.GetCompatabilityDataDirectoryPath(); - if (!protonDirectory.DirectoryExists()) return null; - - var configurationDirectory = protonDirectory.Combine("pfx"); - return new ProtonWinePrefix - { - ConfigurationDirectory = configurationDirectory, - ProtonDirectory = protonDirectory, - }; - } - - /// - /// Uses to parse the workshop manifest - /// file at . - /// - /// - [Pure] - [System.Diagnostics.Contracts.Pure] - [MustUseReturnValue] - public Result ParseWorkshopManifest() - { - var workshopManifestFilePath = AppManifest.GetWorkshopManifestFilePath(); - var result = WorkshopManifestParser.ParseManifestFile(workshopManifestFilePath); - return result; - } - - #endregion - - #region Overrides - - /// - public bool Equals(SteamGame? other) => AppManifest.Equals(other?.AppManifest); - - /// - public override int GetHashCode() => AppManifest.GetHashCode(); - - #endregion -} - diff --git a/src/GameFinder.StoreHandlers.Steam/SteamHandler.cs b/src/GameFinder.StoreHandlers.Steam/SteamHandler.cs deleted file mode 100644 index 757a0cd6..00000000 --- a/src/GameFinder.StoreHandlers.Steam/SteamHandler.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentResults; -using GameFinder.Common; -using GameFinder.RegistryUtils; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using JetBrains.Annotations; -using NexusMods.Paths; -using OneOf; -using ValveKeyValue; - -namespace GameFinder.StoreHandlers.Steam; - -/// -/// Handler for finding games installed with Steam. -/// -[PublicAPI] -public class SteamHandler : AHandler -{ - private readonly IRegistry? _registry; - private readonly IFileSystem _fileSystem; - - private static readonly KVSerializerOptions KvSerializerOptions = - new() - { - HasEscapeSequences = true, - EnableValveNullByteBugBehavior = true, - }; - - /// - /// Constructor. - /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. - /// - /// - /// The implementation of to use. For a shared instance - /// use on Windows. On Linux use null. - /// For tests either use , a custom implementation or just a mock - /// of the interface. - /// - public SteamHandler(IFileSystem fileSystem, IRegistry? registry) - { - _fileSystem = fileSystem; - _registry = registry; - } - - /// - public override Func IdSelector => game => game.AppId; - - /// - public override IEqualityComparer? IdEqualityComparer => null; - - /// - public override IEnumerable> FindAllGames() - { - var steamPathResult = SteamLocationFinder.FindSteam(_fileSystem, _registry); - if (steamPathResult.IsFailed) - { - yield return ConvertResultToErrorMessage(steamPathResult); - yield break; - } - - var steamPath = steamPathResult.Value; - var libraryFoldersFilePath = SteamLocationFinder.GetLibraryFoldersFilePath(steamPath); - - var libraryFoldersResult = LibraryFoldersManifestParser.ParseManifestFile(libraryFoldersFilePath); - if (libraryFoldersResult.IsFailed) - { - yield return ConvertResultToErrorMessage(libraryFoldersResult); - yield break; - } - - var libraryFolders = libraryFoldersResult.Value; - if (libraryFolders.Count == 0) yield break; - - foreach (var libraryFolder in libraryFolders) - { - var libraryFolderPath = libraryFolder.Path; - if (!_fileSystem.DirectoryExists(libraryFolderPath)) - { - yield return new ErrorMessage($"Steam Library at {libraryFolderPath} doesn't exist!"); - continue; - } - - foreach (var acfFilePath in libraryFolder.EnumerateAppManifestFilePaths()) - { - var appManifestResult = AppManifestParser.ParseManifestFile(acfFilePath); - if (appManifestResult.IsFailed) - { - yield return ConvertResultToErrorMessage(appManifestResult); - continue; - } - - var steamGame = new SteamGame - { - SteamPath = steamPath, - AppManifest = appManifestResult.Value, - LibraryFolder = libraryFolder, - }; - - yield return steamGame; - } - } - } - - private static ErrorMessage ConvertResultToErrorMessage(Result result) - { - // TODO: for compatability, remove this mapping once FindAllGames uses FluentResults - return new ErrorMessage(result.Errors.Select(x => x.Message).Aggregate((a, b) => $"{a}\n{b}")); - } -} diff --git a/src/GameFinder.StoreHandlers.Xbox/AppManifest.cs b/src/GameFinder.StoreHandlers.Xbox/AppManifest.cs deleted file mode 100644 index 6f496db9..00000000 --- a/src/GameFinder.StoreHandlers.Xbox/AppManifest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Xml.Serialization; -using JetBrains.Annotations; -#pragma warning disable CS1591 - -namespace GameFinder.StoreHandlers.Xbox; - -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[XmlRoot(ElementName = "Identity", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] -public class Identity -{ - [XmlAttribute(AttributeName = "Name", Namespace = "")] - public string Name { get; set; } = null!; -} - -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[XmlRoot(ElementName = "Properties", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] -public class Properties -{ - [XmlElement(ElementName = "DisplayName", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] - public string DisplayName { get; set; } = null!; -} - -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[XmlRoot(ElementName = "Package", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] -public class Package -{ - [XmlElement(ElementName = "Identity", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] - public Identity Identity { get; set; } = null!; - - [XmlElement(ElementName = "Properties", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] - public Properties Properties { get; set; } = null!; -} diff --git a/src/GameFinder.StoreHandlers.Xbox/LogMessages.cs b/src/GameFinder.StoreHandlers.Xbox/LogMessages.cs new file mode 100644 index 00000000..8f6f8027 --- /dev/null +++ b/src/GameFinder.StoreHandlers.Xbox/LogMessages.cs @@ -0,0 +1,76 @@ +using System; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.Xbox; + +internal static partial class LogMessages +{ + [LoggerMessage( + EventId = 0, EventName = nameof(ExceptionWhileParsingManifest), + Level = LogLevel.Warning, + Message = "Exception while parsing Manifest file `{manifestFilePath}`" + )] + public static partial void ExceptionWhileParsingManifest( + ILogger logger, + Exception e, + AbsolutePath manifestFilePath + ); + + [LoggerMessage( + EventId = 1, EventName = nameof(ManifestDeserializationFailed), + Level = LogLevel.Warning, + Message = "Deserialization of Manifest file `{manifestFilePath}` failed. Return value is `{returnValue} ({returnType})`" + )] + public static partial void ManifestDeserializationFailed( + ILogger logger, + AbsolutePath manifestFilePath, + object? returnValue, + Type? returnType + ); + + [LoggerMessage( + EventId = 2, EventName = nameof(ExceptionWhileParsingGamingRootFile), + Level = LogLevel.Warning, + Message = "Exception while parsing GamingRoot file `{filePath}`" + )] + public static partial void ExceptionWhileParsingGamingRootFile( + ILogger logger, + Exception e, + AbsolutePath filePath + ); + + [LoggerMessage( + EventId = 3, EventName = nameof(MagicMismatch), + Level = LogLevel.Warning, + Message = "Magic mismatch in GamingRoot file `{filePath}`: expected `{expected}`, found `{actual}`" + )] + public static partial void MagicMismatch( + ILogger logger, + uint expected, + uint actual, + AbsolutePath filePath + ); + + [LoggerMessage( + EventId = 4, EventName = nameof(CountTooHigh), + Level = LogLevel.Warning, + Message = "Found count too high in GamingRoot file `{filePath}`: max is `{max}`, found `{actual}`" + )] + public static partial void CountTooHigh( + ILogger logger, + uint max, + uint actual, + AbsolutePath filePath + ); + + [LoggerMessage( + EventId = 5, EventName = nameof(MissingNullTerminator), + Level = LogLevel.Warning, + Message = "Missing null terminator in GamingRoot file `{filePath}`" + )] + public static partial void MissingNullTerminator( + ILogger logger, + AbsolutePath filePath + ); +} diff --git a/src/GameFinder.StoreHandlers.Xbox/Serialization/AppManifest.cs b/src/GameFinder.StoreHandlers.Xbox/Serialization/AppManifest.cs new file mode 100644 index 00000000..b87fdcef --- /dev/null +++ b/src/GameFinder.StoreHandlers.Xbox/Serialization/AppManifest.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Xml; +using System.Xml.Serialization; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; + +namespace GameFinder.StoreHandlers.Xbox.Serialization; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +[PublicAPI] +public static class AppManifest +{ + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + [XmlRoot(ElementName = "Identity", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public record Identity + { + [XmlAttribute(AttributeName = "Name", Namespace = "")] + public required string Name { get; init; } + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + [XmlRoot(ElementName = "Properties", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public record Properties + { + [XmlElement(ElementName = "DisplayName", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public required string DisplayName { get; init; } + } + + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + [XmlRoot(ElementName = "Package", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public record Package + { + [XmlElement(ElementName = "Identity", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public required Identity Identity { get; init; } + + [XmlElement(ElementName = "Properties", Namespace = "http://schemas.microsoft.com/appx/manifest/foundation/windows10")] + public required Properties Properties { get; init; } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Deserializes the data in the provided and returns a . + /// + [RequiresUnreferencedCode($"Requires {nameof(Package)} to not be trimmed for System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader)")] + public static Package? ParseAppManifest( + ILogger logger, + XmlReader xmlReader, + AbsolutePath manifestFilePath) + { + try + { + var obj = new XmlSerializer(typeof(Package)).Deserialize(xmlReader); + if (obj is Package package) return package; + + LogMessages.ManifestDeserializationFailed(logger, manifestFilePath, obj, obj?.GetType()); + return null; + + } + catch (Exception e) + { + LogMessages.ExceptionWhileParsingManifest(logger, e, manifestFilePath); + return null; + } + } +} diff --git a/src/GameFinder.StoreHandlers.Xbox/Serialization/GamingRootFile.cs b/src/GameFinder.StoreHandlers.Xbox/Serialization/GamingRootFile.cs new file mode 100644 index 00000000..9f39428a --- /dev/null +++ b/src/GameFinder.StoreHandlers.Xbox/Serialization/GamingRootFile.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using NexusMods.Paths; +using Reloaded.Memory.Extensions; + +namespace GameFinder.StoreHandlers.Xbox.Serialization; + +/// +/// Represents a GamingRoot file. +/// +[PublicAPI] +public record GamingRootFile +{ + /// + /// Expected magic constant at the start of the file. + /// + public const uint ExpectedMagic = 0x58424752; + + /// + /// Max folder count. Anything more than this indicates a parsing/file error. + /// It's very unlikely that someone has more than 255 folders. + /// + public const uint MaxFolderCount = byte.MaxValue; + + /// + /// Gets the absolute path to the parsed file. + /// + public required AbsolutePath FilePath { get; init; } + + /// + /// Gets the array of folder paths parsed from the file. + /// All of these paths are relative to the parent directory of + /// . + /// + /// + public required RelativePath[] Folders { get; init; } + + /// + /// Converts into . + /// + public AbsolutePath[] GetAbsoluteFolderPaths() + { + var parent = FilePath.Parent; + return Folders.Select(x => parent.Combine(x)).ToArray(); + } + + /// + /// Parses the provided span. + /// + public static GamingRootFile? ParseGamingRootFiles( + ILogger logger, + ReadOnlySpan bytes, + AbsolutePath filePath) + { + try + { + var actualMagic = ParseUInt32(ref bytes); + if (actualMagic != ExpectedMagic) + { + LogMessages.MagicMismatch(logger, ExpectedMagic, actualMagic, filePath); + return null; + } + + var folderCount = ParseUInt32(ref bytes); + if (folderCount >= MaxFolderCount) + { + LogMessages.CountTooHigh(logger, MaxFolderCount, folderCount, filePath); + return null; + } + + // NOTE(erri120): Strings in the file are encoded with Unicode, which is the + // same representation of strings in C#. This allows us to just cast the bytes + // to chars and have much easier and faster parsing. + var span = bytes.Cast(); + + var folders = GC.AllocateUninitializedArray(length: (int)folderCount); + for (var i = 0; i < folderCount; i++) + { + var nullIndex = span.IndexOf('\0'); + if (nullIndex == -1) + { + LogMessages.MissingNullTerminator(logger, filePath); + return null; + } + + var slice = span[..nullIndex]; + var path = RelativePath.FromUnsanitizedInput(slice); + folders[i] = path; + } + + return new GamingRootFile + { + FilePath = filePath, + Folders = folders, + }; + } + catch (Exception e) + { + LogMessages.ExceptionWhileParsingGamingRootFile(logger, e, filePath); + return null; + } + + static uint ParseUInt32(ref ReadOnlySpan span) + { + var value = BitConverter.ToUInt32(span); + span = span[sizeof(uint)..]; + return value; + } + } +} diff --git a/src/GameFinder.StoreHandlers.Xbox/XboxGame.cs b/src/GameFinder.StoreHandlers.Xbox/XboxGame.cs index b5f791e8..44cd52aa 100644 --- a/src/GameFinder.StoreHandlers.Xbox/XboxGame.cs +++ b/src/GameFinder.StoreHandlers.Xbox/XboxGame.cs @@ -5,10 +5,17 @@ namespace GameFinder.StoreHandlers.Xbox; /// -/// Represents a game installed with Xbox Game Pass. +/// Represents a game installed with the Xbox App. /// -/// -/// -/// [PublicAPI] -public record XboxGame(XboxGameId Id, string DisplayName, AbsolutePath Path) : IGame; +public record XboxGame : IGame, IGameName +{ + /// + public required XboxGameId Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required AbsolutePath Path { get; init; } +} diff --git a/src/GameFinder.StoreHandlers.Xbox/XboxGameId.cs b/src/GameFinder.StoreHandlers.Xbox/XboxGameId.cs index ad75e16b..1a30ba7e 100644 --- a/src/GameFinder.StoreHandlers.Xbox/XboxGameId.cs +++ b/src/GameFinder.StoreHandlers.Xbox/XboxGameId.cs @@ -1,51 +1,21 @@ using System; using System.Collections.Generic; +using GameFinder.Common; using JetBrains.Annotations; using TransparentValueObjects; namespace GameFinder.StoreHandlers.Xbox; /// -/// Represents an id for games installed with the Xbox Game Pass. +/// Represents an ID for games installed with the Xbox App. /// +[PublicAPI] [ValueObject] -public readonly partial struct XboxGameId : IAugmentWith +public readonly partial struct XboxGameId : IId, IAugmentWith { /// public static IEqualityComparer InnerValueDefaultEqualityComparer { get; } = StringComparer.OrdinalIgnoreCase; -} - -/// -[PublicAPI] -public class XboxGameIdComparer : IEqualityComparer -{ - private static XboxGameIdComparer? _default; - - /// - /// Default equality comparer that uses . - /// - public static XboxGameIdComparer Default => _default ??= new(); - - private readonly StringComparison _stringComparison; - - /// - /// Default constructor that uses . - /// - public XboxGameIdComparer() : this(StringComparison.OrdinalIgnoreCase) { } - - /// - /// Constructor. - /// - /// - public XboxGameIdComparer(StringComparison stringComparison) - { - _stringComparison = stringComparison; - } /// - public bool Equals(XboxGameId x, XboxGameId y) => string.Equals(x.Value, y.Value, _stringComparison); - - /// - public int GetHashCode(XboxGameId obj) => obj.Value.GetHashCode(_stringComparison); + public bool Equals(IId? other) => other is XboxGameId same && Equals(same); } - diff --git a/src/GameFinder.StoreHandlers.Xbox/XboxHandler.cs b/src/GameFinder.StoreHandlers.Xbox/XboxHandler.cs deleted file mode 100644 index 081e6e7d..00000000 --- a/src/GameFinder.StoreHandlers.Xbox/XboxHandler.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using GameFinder.Common; -using JetBrains.Annotations; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; -using OneOf; - -namespace GameFinder.StoreHandlers.Xbox; - -/// -/// Handler for finding games installed with Xbox Game Pass. -/// -[PublicAPI] -[RequiresUnreferencedCode($"Calls System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader) with {nameof(Package)}. Make sure {nameof(Package)} is preserved by using TrimmerRootDescriptor or TrimmerRootAssembly!")] -public class XboxHandler : AHandler -{ - private readonly IFileSystem _fileSystem; - - /// - /// Constructor. - /// - /// - /// The implementation of to use. For a shared instance use - /// . For tests either use , - /// a custom implementation or just a mock of the interface. - /// - public XboxHandler(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - /// - public override Func IdSelector => game => game.Id; - - /// - public override IEqualityComparer IdEqualityComparer => XboxGameIdComparer.Default; - - /// - public override IEnumerable> FindAllGames() - { - var (paths, errors) = GetAppFolders(_fileSystem); - foreach (var error in errors) - { - yield return error; - } - - if (paths.Count == 0) - { - yield return new ErrorMessage("Unable to find any app folders!"); - } - - foreach (var path in paths) - { - if (!_fileSystem.DirectoryExists(path)) continue; - var directories = _fileSystem - .EnumerateDirectories(path, recursive: false) - .ToArray(); - - if (directories.Length == 0) - { - yield return new ErrorMessage($"App folder {path} does not contain any sub directories!"); - continue; - } - - foreach (var directory in directories) - { - var appManifestFilePath = directory.Combine("appxmanifest.xml"); - if (!_fileSystem.FileExists(appManifestFilePath)) - { - var contentDirectory = directory.Combine("Content"); - if (_fileSystem.DirectoryExists(contentDirectory)) - { - appManifestFilePath = contentDirectory.Combine("appxmanifest.xml"); - if (!_fileSystem.FileExists(appManifestFilePath)) - { - yield return new ErrorMessage($"Manifest file does not exist at {appManifestFilePath}"); - continue; - } - } - else - { - yield return new ErrorMessage($"Manifest file does not exist at {appManifestFilePath} and there is no Content folder at {contentDirectory}"); - continue; - } - } - - var result = ParseAppManifest(_fileSystem, appManifestFilePath); - if (result.TryGetGame(out var game)) - { - yield return game; - } - else - { - yield return result.AsError(); - } - } - } - } - - internal static (List paths, List errors) GetAppFolders(IFileSystem fileSystem) - { - var paths = new List(); - var errors = new List(); - - foreach (var rootDirectory in fileSystem.EnumerateRootDirectories()) - { - if (!fileSystem.DirectoryExists(rootDirectory)) continue; - - var modifiableWindowsAppsPath = rootDirectory - .Combine("Program Files") - .Combine("ModifiableWindowsApps"); - - var gamingRootFilePath = rootDirectory.Combine(".GamingRoot"); - - var modifiableWindowsAppsDirectoryExists = fileSystem.DirectoryExists(modifiableWindowsAppsPath); - var gamingRootFileExists = fileSystem.FileExists(gamingRootFilePath); - - if (modifiableWindowsAppsDirectoryExists) paths.Add(modifiableWindowsAppsPath); - - if (!modifiableWindowsAppsDirectoryExists && !gamingRootFileExists) - { - errors.Add($"Neither {modifiableWindowsAppsPath} nor {gamingRootFilePath} exist on the current drive."); - continue; - } - - if (!gamingRootFileExists) continue; - - var parseGamingRootFileResult = ParseGamingRootFile(fileSystem, gamingRootFilePath); - parseGamingRootFileResult.Switch( - additionalPaths => paths.AddRange(additionalPaths), - error => errors.Add(error) - ); - } - - return (paths, errors); - } - - internal static OneOf ParseAppManifest(IFileSystem fileSystem, - AbsolutePath manifestFilePath) - { - try - { - using var stream = fileSystem.ReadFile(manifestFilePath); - using var reader = XmlReader.Create(stream, new XmlReaderSettings - { - IgnoreComments = true, - IgnoreWhitespace = true, - ValidationFlags = XmlSchemaValidationFlags.AllowXmlAttributes, - }); - - var obj = new XmlSerializer(typeof(Package)).Deserialize(reader); - if (obj is null) - { - return new ErrorMessage($"Unable to deserialize file {manifestFilePath}"); - } - - if (obj is not Package appManifest) - { - return new ErrorMessage($"Deserialization of {manifestFilePath} failed: resulting object is not of type {typeof(Package)} but {obj.GetType()}"); - } - - var displayName = appManifest.Properties.DisplayName; - var id = appManifest.Identity.Name; - var game = new XboxGame(XboxGameId.From(id), displayName, manifestFilePath.Parent); - return game; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Unable to parse manifest file {manifestFilePath}"); - } - } - - internal static OneOf, ErrorMessage> ParseGamingRootFile( - IFileSystem fileSystem, AbsolutePath gamingRootFilePath) - { - try - { - using var stream = fileSystem.ReadFile(gamingRootFilePath); - using var reader = new BinaryReader(stream, Encoding.Unicode); - - const uint expectedMagic = 0x58424752; - var magic = reader.ReadUInt32(); - if (magic != expectedMagic) - { - return new ErrorMessage($"Unable to parse {gamingRootFilePath}, file magic does not match: expected {expectedMagic.ToString("x8", NumberFormatInfo.InvariantInfo)} got {magic.ToString("x8", NumberFormatInfo.InvariantInfo)}"); - } - - var folderCount = reader.ReadUInt32(); - if (folderCount >= byte.MaxValue) - { - return new ErrorMessage($"Folder count exceeds the limit: {folderCount}"); - } - - var parentFolder = gamingRootFilePath.Parent; - var folders = new List((int)folderCount); - for (var i = 0; i < folderCount; i++) - { - var sb = new StringBuilder(); - var c = reader.ReadChar(); - while (c != '\0') - { - sb.Append(c); - c = reader.ReadChar(); - } - - var part = RelativePath.FromUnsanitizedInput(sb.ToString()); - folders.Add(parentFolder.Combine(part)); - } - - return folders; - } - catch (Exception e) - { - return new ErrorMessage(e, $"Unable to parse gaming root file {gamingRootFilePath}"); - } - - } -} diff --git a/src/GameFinder.Wine/AWinePrefix.cs b/src/GameFinder.Wine/AWinePrefix.cs deleted file mode 100644 index 75179778..00000000 --- a/src/GameFinder.Wine/AWinePrefix.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Text; -using GameFinder.RegistryUtils; -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.Wine; - -/// -/// Abstract class for wine prefixes. -/// -[PublicAPI] -public abstract record AWinePrefix -{ - /// - /// Absolute path to the Wine prefix directory. - /// - public required AbsolutePath ConfigurationDirectory { get; init; } - - /// - /// Returns the absolute path to the virtual drive directory of the prefix. - /// - /// - public AbsolutePath GetVirtualDrivePath() - { - return ConfigurationDirectory.Combine("drive_c"); - } - - /// - /// Returns the absolute path to the system.reg file of the prefix. - /// - /// - public AbsolutePath GetSystemRegistryFile() - { - return ConfigurationDirectory.Combine("system.reg"); - } - - /// - /// Returns the absolute path to the user.reg file of the prefix. - /// - /// - public AbsolutePath GetUserRegistryFile() - { - return ConfigurationDirectory.Combine("user.reg"); - } - - /// - /// Returns the username for this wine prefix. - /// - /// - protected virtual string GetUserName() - { - var user = Environment.GetEnvironmentVariable("USER", EnvironmentVariableTarget.Process); - if (user is null) throw new PlatformNotSupportedException(); - return user; - } - - /// - /// Creates an overlay with path - /// mappings into the wine prefix. - /// - /// - /// - public IFileSystem CreateOverlayFileSystem(IFileSystem fileSystem) - { - var rootDirectory = GetVirtualDrivePath(); - - var newHomeDirectory = rootDirectory - .Combine("Users") - .Combine(GetUserName()); - - var (pathMappings, knownPathMappings) = BaseFileSystem.CreateWinePathMappings( - fileSystem, - rootDirectory, - newHomeDirectory); - - return fileSystem.CreateOverlayFileSystem(pathMappings, knownPathMappings, convertCrossPlatformPaths: true); - } - - /// - /// Creates a new implementation, - /// based on the registry files in the configuration - /// directory. - /// - /// - public IRegistry CreateRegistry(IFileSystem fileSystem) - { - var registry = new InMemoryRegistry(); - - var registryFile = GetSystemRegistryFile(); - if (!fileSystem.FileExists(registryFile)) return registry; - - using var stream = fileSystem.ReadFile(registryFile); - using var reader = new StreamReader(stream, Encoding.UTF8); - - InMemoryRegistryKey? currentKey = null; - - while (true) - { - var line = reader.ReadLine(); - if (line is null) break; - - if (string.IsNullOrWhiteSpace(line)) - continue; - - if (line.StartsWith("WINE REGISTRY VERSION", StringComparison.OrdinalIgnoreCase)) - continue; - - if (line.StartsWith(";;", StringComparison.OrdinalIgnoreCase) || - line.StartsWith('#')) - continue; - - if (line.StartsWith('[')) - { - var squareBracketIndex = line.IndexOf(']', StringComparison.OrdinalIgnoreCase); - var keyName = line.Substring(1, squareBracketIndex - 1); - - currentKey = registry.AddKey(RegistryHive.LocalMachine, keyName); - continue; - } - - if (line.StartsWith("@=", StringComparison.OrdinalIgnoreCase)) - { - if (currentKey is null) throw new UnreachableException(); - if (line[2] == '"') - { - var endIndex = line.LastIndexOf('"'); - var value = line.Substring(3, endIndex - 3); - - currentKey.GetParent().AddValue(currentKey.GetKeyName(), value); - continue; - } - - // TODO: handle other cases - } - - if (line.StartsWith('"')) - { - if (currentKey is null) throw new UnreachableException(); - var splitIndex = line.IndexOf("\"=\"", StringComparison.OrdinalIgnoreCase); - - // TODO: handle more cases - if (splitIndex == -1) continue; - - var valueName = line.Substring(1, splitIndex - 1); - var value = line.Substring(splitIndex + 3, line.Length - splitIndex - 4); - currentKey.AddValue(valueName, value); - continue; - } - - // TODO: handle other cases - } - - return registry; - } -} diff --git a/src/GameFinder.Wine/Bottles/BottlesWinePrefix.cs b/src/GameFinder.Wine/Bottles/BottlesWinePrefix.cs deleted file mode 100644 index 7ce6dc6b..00000000 --- a/src/GameFinder.Wine/Bottles/BottlesWinePrefix.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using NexusMods.Paths; - -namespace GameFinder.Wine.Bottles; - -/// -/// Represents a Wineprefix managed by Bottles. -/// -[PublicAPI] -public record BottlesWinePrefix : AWinePrefix -{ - /// - /// Returns the absolute path to bottle.yml. - /// - /// - public AbsolutePath GetBottlesConfigFile() - { - return ConfigurationDirectory.Combine("bottle.yml"); - } -} diff --git a/src/GameFinder.Wine/Bottles/BottlesWinePrefixManager.cs b/src/GameFinder.Wine/Bottles/BottlesWinePrefixManager.cs deleted file mode 100644 index 9c698bf2..00000000 --- a/src/GameFinder.Wine/Bottles/BottlesWinePrefixManager.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GameFinder.Common; -using NexusMods.Paths; -using OneOf; - -namespace GameFinder.Wine.Bottles; - -/// -/// Wineprefix manager for prefixes created and managed by Bottles. -/// -public class BottlesWinePrefixManager : IWinePrefixManager -{ - private readonly IFileSystem _fileSystem; - - /// - /// Constructor. - /// - /// - public BottlesWinePrefixManager(IFileSystem fs) - { - _fileSystem = fs; - } - - /// - public IEnumerable> FindPrefixes() - { - var defaultLocation = GetDefaultLocations(_fileSystem) - .FirstOrDefault(x => _fileSystem.DirectoryExists(x)); - - if (string.IsNullOrEmpty(defaultLocation.Directory)) - { - yield return new ErrorMessage("Unable to find any bottles installation."); - yield break; - } - - var bottles = defaultLocation.Combine("bottles"); - if (!bottles.DirectoryExists()) - { - yield return new ErrorMessage($"Bottles directory {bottles.GetFullPath()} does not exist"); - yield break; - } - - foreach (var bottle in _fileSystem.EnumerateDirectories(bottles, recursive: false)) - { - var res = IsValidBottlesPrefix(_fileSystem, bottle); - yield return res.Match>( - _ => new BottlesWinePrefix - { - ConfigurationDirectory = bottle, - }, - error => error); - } - } - - internal static OneOf IsValidBottlesPrefix(IFileSystem fs, AbsolutePath directory) - { - var defaultWinePrefixRes = DefaultWinePrefixManager.IsValidPrefix(fs, directory); - if (defaultWinePrefixRes.IsError()) - { - return defaultWinePrefixRes.AsError(); - } - - var bottlesConfigFile = directory.Combine("bottle.yml"); - if (!fs.FileExists(bottlesConfigFile)) - { - return new ErrorMessage($"Bottles configuration file is missing at {bottlesConfigFile}"); - } - - return true; - } - - internal static IEnumerable GetDefaultLocations(IFileSystem fs) - { - // $XDG_DATA_HOME/bottles aka ~/.local/share/bottles - yield return fs.GetKnownPath(KnownPath.LocalApplicationDataDirectory) - .Combine("bottles"); - - // ~/.var/app/com.usebottles.bottles/data/bottles (flatpak installation) - // https://github.com/flatpak/flatpak/wiki/Filesystem - yield return fs.GetKnownPath(KnownPath.HomeDirectory) - .Combine(".var/app/com.usebottles.bottles/data/bottles"); - } -} diff --git a/src/GameFinder.Wine/DefaultWinePrefixManager.cs b/src/GameFinder.Wine/DefaultWinePrefixManager.cs deleted file mode 100644 index 6d4ac69d..00000000 --- a/src/GameFinder.Wine/DefaultWinePrefixManager.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using GameFinder.Common; -using JetBrains.Annotations; -using NexusMods.Paths; -using OneOf; - -namespace GameFinder.Wine; - -/// -/// Prefix manager for a vanilla Wine installation that searches for prefixes inside -/// the default locations. -/// -[PublicAPI] -public class DefaultWinePrefixManager : IWinePrefixManager -{ - private readonly IFileSystem _fileSystem; - - /// - /// Constructor. - /// - /// - public DefaultWinePrefixManager(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - /// - public IEnumerable> FindPrefixes() - { - foreach (var defaultWinePrefixLocation in GetDefaultWinePrefixLocations(_fileSystem)) - { - if (!_fileSystem.DirectoryExists(defaultWinePrefixLocation)) continue; - - var res = IsValidPrefix(_fileSystem, defaultWinePrefixLocation); - yield return res.Match>( - _ => new WinePrefix - { - ConfigurationDirectory = defaultWinePrefixLocation, - }, - error => error); - } - } - - internal static OneOf IsValidPrefix(IFileSystem fileSystem, AbsolutePath directory) - { - var virtualDrive = directory.Combine("drive_c"); - if (!fileSystem.DirectoryExists(virtualDrive)) - { - return new ErrorMessage($"Virtual C: drive does not exist at {virtualDrive}"); - } - - var systemRegistryFile = directory.Combine("system.reg"); - if (!fileSystem.FileExists(systemRegistryFile)) - { - return new ErrorMessage($"System registry file does not exist at {systemRegistryFile}"); - } - - var userRegistryFile = directory.Combine("user.reg"); - if (!fileSystem.FileExists(userRegistryFile)) - { - return new ErrorMessage($"User registry file does not exist at {userRegistryFile}"); - } - - return true; - } - - internal static IEnumerable GetDefaultWinePrefixLocations(IFileSystem fileSystem) - { - // from the docs: https://wiki.winehq.org/FAQ#Wineprefixes - - // ~/.wine is the default prefix - yield return fileSystem.GetKnownPath(KnownPath.HomeDirectory).Combine(".wine"); - - var winePrefixEnvVariable = Environment.GetEnvironmentVariable("WINEPREFIX"); - if (winePrefixEnvVariable is not null) - { - yield return fileSystem.FromUnsanitizedFullPath(winePrefixEnvVariable); - } - - // WINEPREFIX0, WINEPREFIX1, ... - foreach (var numberedEnvVariable in GetNumberedEnvironmentVariables()) - { - yield return fileSystem.FromUnsanitizedFullPath(numberedEnvVariable); - } - - // Bottling standards: https://wiki.winehq.org/Bottling_Standards - // TODO: not sure which 3rd party applications actually use this - } - - internal static IEnumerable GetNumberedEnvironmentVariables() - { - for (var i = 0; i < 10; i++) - { - var envVariable = Environment - .GetEnvironmentVariable($"WINEPREFIX{i.ToString(CultureInfo.InvariantCulture)}"); - if (envVariable is null) yield break; - yield return envVariable; - } - } -} diff --git a/src/GameFinder.Wine/Extensions.cs b/src/GameFinder.Wine/Extensions.cs deleted file mode 100644 index d704a6bd..00000000 --- a/src/GameFinder.Wine/Extensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using GameFinder.Common; -using JetBrains.Annotations; -using OneOf; - -namespace GameFinder.Wine; - -/// -/// Extension methods. -/// -[PublicAPI] -public static class Extensions -{ - /// - /// Returns true if the result is of type . - /// - /// - /// - /// - public static bool IsPrefix(this OneOf result) - where TPrefix : AWinePrefix - { - return result.IsT0; - } - - /// - /// Returns the part of the result. This can throw if - /// the result is not of type . Use - /// instead. - /// - /// - /// - /// - /// - /// Thrown when the result is not of type . - /// - public static TPrefix AsPrefix(this OneOf result) - where TPrefix : AWinePrefix - { - return result.AsT0; - } - - /// - /// Returns the part of the result using the try-get - /// pattern. - /// - /// - /// - /// - /// - public static bool TryGetPrefix( - this OneOf result, - [MaybeNullWhen(false)] out TPrefix prefix) - where TPrefix : AWinePrefix - { - prefix = null; - if (!result.IsPrefix()) return false; - - prefix = result.AsPrefix(); - return true; - } -} diff --git a/src/GameFinder.Wine/IWinePrefixManager.cs b/src/GameFinder.Wine/IWinePrefixManager.cs deleted file mode 100644 index 34166c38..00000000 --- a/src/GameFinder.Wine/IWinePrefixManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using GameFinder.Common; -using JetBrains.Annotations; -using OneOf; - -namespace GameFinder.Wine; - -/// -/// Implementation for wine prefix managers. -/// -/// -[PublicAPI] -public interface IWinePrefixManager where TPrefix : AWinePrefix -{ - /// - /// Finds all prefixes associated with this manager. - /// - /// - IEnumerable> FindPrefixes(); -} diff --git a/src/GameFinder.Wine/WinePrefix.cs b/src/GameFinder.Wine/WinePrefix.cs deleted file mode 100644 index b0974e6b..00000000 --- a/src/GameFinder.Wine/WinePrefix.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JetBrains.Annotations; - -namespace GameFinder.Wine; - -/// -/// Represents a wine prefix. -/// -[PublicAPI] -public record WinePrefix : AWinePrefix; diff --git a/tests/GameFinder.Common.Tests/ExtensionTests.cs b/tests/GameFinder.Common.Tests/ExtensionTests.cs deleted file mode 100644 index 56bb3a03..00000000 --- a/tests/GameFinder.Common.Tests/ExtensionTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using NSubstitute; -using OneOf; - -namespace GameFinder.Common.Tests; - -public class ExtensionTests -{ - private static readonly IGame Game = Substitute.For(); - private static readonly ErrorMessage Error = new(string.Empty); - - private static readonly OneOf GameResult = OneOf.FromT0(Game); - private static readonly OneOf ErrorResult = OneOf.FromT1(Error); - - [Fact] - public void Test_CustomToDictionary() - { - var input = new[] { "a", "b", "ab" }; - var output = input.CustomToDictionary(x => x.Length, x => x); - output.Should().ContainKeys(1, 2); - output.Should().ContainValues("a", "ab"); - } - - [Fact] - public void Test_IsGame_True() - { - var result = GameResult; - result.IsGame().Should().BeTrue(); - } - - [Fact] - public void Test_IsGame_False() - { - var result = ErrorResult; - result.IsGame().Should().BeFalse(); - } - - [Fact] - public void Test_IsError_True() - { - var result = ErrorResult; - result.IsError().Should().BeTrue(); - } - - [Fact] - public void Test_IsError_False() - { - var result = GameResult; - result.IsError().Should().BeFalse(); - } - - [Fact] - public void Test_AsGame() - { - var result = GameResult; - result.AsGame().Should().Be(Game); - } - - [Fact] - public void Test_AsGame_InvalidOperationException() - { - var result = ErrorResult; - result - .Invoking(x => x.AsGame()) - .Should().ThrowExactly(); - } - - [Fact] - public void Test_AsError() - { - var result = ErrorResult; - result.AsError().Should().Be(string.Empty); - } - - [Fact] - public void Test_AsError_InvalidOperationException() - { - var result = GameResult; - result - .Invoking(x => x.AsError()) - .Should().ThrowExactly(); - } - - [Fact] - public void Test_TryGetGame_True() - { - var result = GameResult; - result.TryGetGame(out _).Should().BeTrue(); - } - - [Fact] - public void Test_TryGetGame_False() - { - var result = ErrorResult; - result.TryGetGame(out var game).Should().BeFalse(); - game.Should().BeNull(); - } - - [Fact] - public void Test_TryGetError_True() - { - var result = ErrorResult; - result.TryGetError(out _).Should().BeTrue(); - } - - [Fact] - public void Test_TryGetError_False() - { - var result = GameResult; - result.TryGetError(out var error).Should().BeFalse(); - error.Should().Be(default(ErrorMessage)); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/ArrangeHelpers.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/ArrangeHelpers.cs deleted file mode 100644 index 0e6fcc31..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text.Json; -using GameFinder.StoreHandlers.EADesktop.Crypto; -using NexusMods.Paths; -using NSubstitute; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class EADesktopTests -{ - private static IHardwareInfoProvider SetupHardwareInfoProvider() - { - return Substitute.For(); - } - - private static ( - EADesktopHandler handler, - IHardwareInfoProvider hardwareInfoProvider, - AbsolutePath parentFolder) - SetupHandler(InMemoryFileSystem fs) - { - var dataFolder = EADesktopHandler.GetDataFolder(fs); - fs.AddDirectory(dataFolder); - - var hardwareInfoProvider = SetupHardwareInfoProvider(); - var handler = new EADesktopHandler(fs, hardwareInfoProvider); - return (handler, hardwareInfoProvider, dataFolder); - } - - [SuppressMessage("Design", "MA0051:Method is too long")] - private static IEnumerable SetupGames( - InMemoryFileSystem fs, IHardwareInfoProvider hardwareInfoProvider, AbsolutePath dataFolder) - { - var fixture = new Fixture(); - - var installInfoFile = EADesktopHandler.GetInstallInfoFile(dataFolder); - fs.AddDirectory(installInfoFile.Parent); - - fixture.Customize(composer => composer - .FromFactory((softwareId, baseSlug) => - { - var baseInstallPath = fs - .GetKnownPath(KnownPath.TempDirectory) - .Combine(baseSlug); - - var installerDataPath = baseInstallPath - .Combine("__Installer") - .Combine("installerdata.xml"); - - fs.AddDirectory(baseInstallPath); - fs.AddFile(installerDataPath, ""); - - var game = new EADesktopGame(EADesktopGameId.From(softwareId), baseSlug, baseInstallPath); - return game; - }) - .OmitAutoProperties()); - - var games = fixture.CreateMany().ToArray(); - - var installInfos = games.Select(game => new InstallInfo( - game.BaseInstallPath + "\\", - game.BaseSlug, - InstallCheck: null, - game.EADesktopGameId.Value)) - .ToList(); - - var installInfo = new InstallInfoFile( - installInfos, - new Schema(EADesktopHandler.SupportedSchemaVersion)); - - var encryptionKey = Decryption.CreateDecryptionKey(hardwareInfoProvider); - - using (var aes = Aes.Create()) - { - aes.Key = encryptionKey; - aes.IV = Decryption.CreateDecryptionIV(); - - var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); - - using var stream = new MemoryStream(); - stream.Write(stackalloc byte[64]); - - using (var cryptoStream = new CryptoStream(stream, encryptor, CryptoStreamMode.Write)) - { - JsonSerializer.Serialize(cryptoStream, installInfo, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - } - - var buffer = stream.ToArray(); - fs.AddFile(installInfoFile, buffer); - } - - return games; - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_Decryption.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_Decryption.cs deleted file mode 100644 index 0bc457e2..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_Decryption.cs +++ /dev/null @@ -1,25 +0,0 @@ -using GameFinder.StoreHandlers.EADesktop.Crypto; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class CryptoTests -{ - [Fact] - public void Test_Decryption() - { - var key = new byte[] - { - 0x01, 0xb4, 0x2f, 0x0e, 0x7e, 0x3b, 0x32, 0xe7, 0xc4, 0x25, 0x1b, 0xc3, 0x8f, - 0xa2, 0xae, 0x2e, 0xdb, 0x8d, 0xc2, 0x64, 0x98, 0xe5, 0xb7, 0x3e, 0x2a, 0x92, - 0xac, 0x9e, 0x8f, 0xfc, 0xb4, 0xf4, - }; - - var iv = Decryption.CreateDecryptionIV(); - - var cipherText = File.ReadAllBytes(Path.Combine("files", "IS_erri120.encrypted")); - var expectedPlainText = File.ReadAllText(Path.Combine("files", "IS_erri120.decrypted")); - - var plainText = Decryption.DecryptFile(cipherText, key, iv); - plainText.Should().Be(expectedPlainText); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA1.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA1.cs deleted file mode 100644 index da11caae..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using GameFinder.StoreHandlers.EADesktop.Crypto; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class CryptoTests -{ - [Theory] - [InlineData("erri120", "3f37d8cece4441299a6abbd8db2acc0102cfd398")] - public void Test_SHA1(string input, string expectedOutput) - { - var actualOutput = Hashing.CalculateSHA1Hash(input); - actualOutput.Should().Be(expectedOutput); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA3.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA3.cs deleted file mode 100644 index fcfb5a35..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/CryptoTests/Test_SHA3.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Globalization; -using GameFinder.StoreHandlers.EADesktop.Crypto; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class CryptoTests -{ - [Theory] - [InlineData("allUsersGenericId", "530c11479fe252fc5aabc24935b9776d4900eb3ba58fdc271e0d6229413ad40e")] - [InlineData("allUsersGenericIdIS", "84efc4b836119c20419398c3f3f2bcef6fc52f9d86c6e4e8756aec5a8279e492")] - public void Test_SHA3(string input, string expectedOutput) - { - var actualOutput = Hashing.CalculateSHA3_256Hash(input); - var hexOutput = Convert.ToHexString(actualOutput).ToLower(CultureInfo.InvariantCulture); - hexOutput.Should().Be(expectedOutput); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_CreateSchemaVersionMessage.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_CreateSchemaVersionMessage.cs deleted file mode 100644 index 9aff1470..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_CreateSchemaVersionMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class EADesktopTests -{ - [Theory] - [InlineData(SchemaPolicy.Error, EADesktopHandler.SupportedSchemaVersion, true, false)] - [InlineData(SchemaPolicy.Ignore, EADesktopHandler.SupportedSchemaVersion + 1, true, false)] - [InlineData(SchemaPolicy.Warn, EADesktopHandler.SupportedSchemaVersion + 1, false, false)] - [InlineData(SchemaPolicy.Error, EADesktopHandler.SupportedSchemaVersion + 1, false, true)] - public void Test_CreateSchemaVersionMessage( - SchemaPolicy schemaPolicy, int schemaVersion, bool shouldMessageBeNull, - bool expectedIsErrorValue) - { - var (message, isError) = EADesktopHandler.CreateSchemaVersionMessage( - schemaPolicy, - schemaVersion, - new InMemoryFileSystem().GetKnownPath(KnownPath.TempDirectory)); - - if (shouldMessageBeNull) - message.Should().BeNull(); - else - message.Should().NotBeNull(); - - isError.Should().Be(expectedIsErrorValue); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_InstallInfoToGame.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_InstallInfoToGame.cs deleted file mode 100644 index 1347fbf5..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_InstallInfoToGame.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class EADesktopTests -{ - [Theory, AutoFileSystem] - public void Test_InstallInfoToGame(InMemoryFileSystem fileSystem, string baseSlug, string installCheck, string baseInstallPathName, string softwareId) - { - var baseInstallPath = fileSystem.GetKnownPath(KnownPath.TempDirectory) - .Combine(baseInstallPathName); - - var installInfo = new InstallInfo( - baseInstallPath.GetFullPath(), - baseSlug, - installCheck, - softwareId); - - var fs = new InMemoryFileSystem(); - var result = EADesktopHandler.InstallInfoToGame(fs, installInfo, 0, fs.GetKnownPath(KnownPath.TempDirectory)); - result.IsT0.Should().BeTrue(); - result.IsT1.Should().BeFalse(); - } -} diff --git a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_ShouldWork_FindAllGames.cs b/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_ShouldWork_FindAllGames.cs deleted file mode 100644 index fae69d7b..00000000 --- a/tests/GameFinder.StoreHandlers.EADesktop.Tests/Test_ShouldWork_FindAllGames.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EADesktop.Tests; - -public partial class EADesktopTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGames(InMemoryFileSystem fs) - { - var (handler, hardwareInfoProvider, dataFolder) = SetupHandler(fs); - var expectedGames = SetupGames(fs, hardwareInfoProvider, dataFolder); - handler.ShouldFindAllGames(expectedGames); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGamesById(InMemoryFileSystem fs) - { - var (handler, hardwareInfoProvider, dataFolder) = SetupHandler(fs); - var expectedGames = SetupGames(fs, hardwareInfoProvider, dataFolder).ToArray(); - handler.ShouldFindAllGamesById(expectedGames, game => game.EADesktopGameId); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllInterfacesGames(InMemoryFileSystem fs) - { - var (handler, hardwareInfoProvider, dataFolder) = SetupHandler(fs); - var expectedGames = SetupGames(fs, hardwareInfoProvider, dataFolder).ToArray(); - handler.ShouldFindAllInterfacesGames(expectedGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/ArrangeHelpers.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/ArrangeHelpers.cs deleted file mode 100644 index f2db7f55..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,53 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - private static (EGSHandler handler, AbsolutePath manifestDir) SetupHandler(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var fixture = new Fixture(); - - var manifestDirName = fixture.Create(); - var manifestDir = fs - .GetKnownPath(KnownPath.TempDirectory) - .Combine(manifestDirName); - - fs.AddDirectory(manifestDir); - - var regKey = registry.AddKey(RegistryHive.CurrentUser, EGSHandler.RegKey); - regKey.AddValue(EGSHandler.ModSdkMetadataDir, manifestDir.GetFullPath()); - - var handler = new EGSHandler(registry, fs); - return (handler, manifestDir); - } - - private static IEnumerable SetupGames(InMemoryFileSystem fs, AbsolutePath manifestDir) - { - var fixture = new Fixture(); - - fixture - .Customize(composer => composer - .FromFactory((catalogItemId, displayName) => - { - var manifestItem = manifestDir.Combine($"{catalogItemId}.item"); - var installLocation = manifestDir.Combine(displayName); - - var mockData = $@"{{ - ""CatalogItemId"": ""{catalogItemId}"", - ""DisplayName"": ""{displayName}"", - ""InstallLocation"": ""{installLocation.GetFullPath().ToEscapedString()}"" -}}"; - - fs.AddDirectory(installLocation); - fs.AddFile(manifestItem, mockData); - - return new EGSGame(EGSGameId.From(catalogItemId), displayName, installLocation); - }) - .OmitAutoProperties()); - - return fixture.CreateMany(); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_InvalidManifest.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_InvalidManifest.cs deleted file mode 100644 index da5fd24e..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_InvalidManifest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_InvalidManifest_Exception(InMemoryFileSystem fs, - InMemoryRegistry registry, string manifestItemName) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var randomBytes = new byte[128]; - Random.Shared.NextBytes(randomBytes); - - var manifestItem = manifestDir.Combine($"{manifestItemName}.item"); - fs.AddFile(manifestItem, randomBytes); - - var error = handler.ShouldOnlyBeOneError(); - error.ToString().Should().StartWith($"Unable to deserialize file {manifestItem}:\n"); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_InvalidManifest_Null(InMemoryFileSystem fs, - InMemoryRegistry registry, string manifestItemName) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var manifestItem = manifestDir.Combine($"{manifestItemName}.item"); - fs.AddFile(manifestItem, "null"); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Unable to deserialize file {manifestItem}"); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_DefaultPath.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_DefaultPath.cs deleted file mode 100644 index 48a50846..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_DefaultPath.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingDirectory_DefaultPath(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var handler = new EGSHandler(registry, fs); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"The manifest directory {EGSHandler.GetDefaultManifestsPath(fs)} does not exist!"); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_Registry.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_Registry.cs deleted file mode 100644 index 54f5cfb4..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingDirectory_Registry.cs +++ /dev/null @@ -1,19 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingDirectory_Registry(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - fs.DeleteDirectory(manifestDir, recursive: true); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"The manifest directory {manifestDir} does not exist!"); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingValues.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingValues.cs deleted file mode 100644 index bc7b6c52..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_MissingValues.cs +++ /dev/null @@ -1,48 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_Missing_CatalogItemId(InMemoryFileSystem fs, - InMemoryRegistry registry, string manifestItemName) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var manifest = manifestDir.Combine($"{manifestItemName}.item"); - fs.AddFile(manifest, "{}"); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"CatalogItemId\""); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_Missing_DisplayName(InMemoryFileSystem fs, - InMemoryRegistry registry, string manifestItemName, string value) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var manifest = manifestDir.Combine($"{manifestItemName}.item"); - fs.AddFile(manifest, $@"{{ ""CatalogItemId"": ""{value}"" }}"); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"DisplayName\""); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_Missing_InstallLocation(InMemoryFileSystem fs, - InMemoryRegistry registry, string manifestItemName, string value) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var manifest = manifestDir.Combine($"{manifestItemName}.item"); - fs.AddFile(manifest, $@"{{ ""CatalogItemId"": ""{value}"", ""DisplayName"": ""{value}"" }}"); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"InstallLocation\""); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_NoManifests.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_NoManifests.cs deleted file mode 100644 index a86a2772..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldError_NoManifests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_NoManifests(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"The manifest directory {manifestDir} does not contain any .item files"); - } -} diff --git a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldWork_FindAllGames.cs b/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldWork_FindAllGames.cs deleted file mode 100644 index ada63312..00000000 --- a/tests/GameFinder.StoreHandlers.EGS.Tests/Test_ShouldWork_FindAllGames.cs +++ /dev/null @@ -1,36 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.EGS.Tests; - -public partial class EGSTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGames(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - var expectedGames = SetupGames(fs, manifestDir); - - handler.ShouldFindAllGames(expectedGames); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGamesById(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - var expectedGames = SetupGames(fs, manifestDir).ToArray(); - - handler.ShouldFindAllGamesById(expectedGames, game => game.CatalogItemId); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllInterfaceGames(InMemoryFileSystem fs, InMemoryRegistry registry) - { - var (handler, manifestDir) = SetupHandler(fs, registry); - var expectedGames = SetupGames(fs, manifestDir); - - handler.ShouldFindAllInterfacesGames(expectedGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/ArrangeHelpers.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/ArrangeHelpers.cs deleted file mode 100644 index b7553b9f..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Globalization; -using GameFinder.RegistryUtils; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - public static (GOGHandler handler, InMemoryRegistryKey gogKey) SetupHandler(IFileSystem fileSystem, InMemoryRegistry registry) - { - var gogKey = registry.AddKey(RegistryHive.LocalMachine, GOGHandler.GOGRegKey); - var handler = new GOGHandler(registry, fileSystem); - return (handler, gogKey); - } - - public static IEnumerable SetupGames(IFileSystem fileSystem, InMemoryRegistryKey gogKey) - { - var fixture = new Fixture(); - - fixture.Customize(composer => composer - .FromFactory((id, name) => - { - var path = fileSystem - .GetKnownPath(KnownPath.TempDirectory) - .Combine(name); - - var gameKey = gogKey.AddSubKey(id.ToString(CultureInfo.InvariantCulture)); - gameKey.AddValue("gameID", id.ToString(CultureInfo.InvariantCulture)); - gameKey.AddValue("gameName", name); - gameKey.AddValue("path", path.GetFullPath()); - - return new GOGGame(GOGGameId.From(id), name, path); - }) - .OmitAutoProperties()); - - return fixture.CreateMany(); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_InvalidValues.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_InvalidValues.cs deleted file mode 100644 index 15b2717c..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_InvalidValues.cs +++ /dev/null @@ -1,21 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_InvalidGameId(IFileSystem fileSystem, InMemoryRegistry registry, string keyName, string gameId) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - - var invalidKey = gogKey.AddSubKey(keyName); - invalidKey.AddValue("gameId", gameId); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"The value \"gameID\" of {invalidKey.GetName()} is not a number: \"{gameId}\""); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingGOGKey.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingGOGKey.cs deleted file mode 100644 index b54109ae..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingGOGKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingGOGKey(IFileSystem fileSystem, InMemoryRegistry registry) - { - var handler = new GOGHandler(registry, fileSystem); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Unable to open HKEY_LOCAL_MACHINE\\{GOGHandler.GOGRegKey}"); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingValues.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingValues.cs deleted file mode 100644 index 8a90038b..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_MissingValues.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Globalization; -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingGameId(IFileSystem fileSystem, InMemoryRegistry registry, string keyName) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - - var invalidKey = gogKey.AddSubKey(keyName); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"{invalidKey.GetName()} doesn't have a string value \"gameID\""); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingGameName(IFileSystem fileSystem, InMemoryRegistry registry, string keyName, long gameId) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - - var invalidKey = gogKey.AddSubKey(keyName); - invalidKey.AddValue("gameId", gameId.ToString(CultureInfo.InvariantCulture)); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"{invalidKey.GetName()} doesn't have a string value \"gameName\""); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingPath(IFileSystem fileSystem, InMemoryRegistry registry, string keyName, long gameId, string gameName) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - - var invalidKey = gogKey.AddSubKey(keyName); - invalidKey.AddValue("gameId", gameId.ToString(CultureInfo.InvariantCulture)); - invalidKey.AddValue("gameName", gameName); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"{invalidKey.GetName()} doesn't have a string value \"path\""); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_NoSubKeys.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_NoSubKeys.cs deleted file mode 100644 index edd1acb6..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldError_NoSubKeys.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_NoSubKeys(IFileSystem fileSystem, InMemoryRegistry registry) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Registry key {gogKey.GetName()} has no sub-keys"); - } -} diff --git a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldWork_FindAllGames.cs b/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldWork_FindAllGames.cs deleted file mode 100644 index 6abdfdef..00000000 --- a/tests/GameFinder.StoreHandlers.GOG.Tests/Test_ShouldWork_FindAllGames.cs +++ /dev/null @@ -1,36 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.GOG.Tests; - -public partial class GOGTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGames(IFileSystem fileSystem, InMemoryRegistry registry) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - var expectedGames = SetupGames(fileSystem, gogKey); - - handler.ShouldFindAllGames(expectedGames); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGamesById(IFileSystem fileSystem, InMemoryRegistry registry) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - var expectedGames = SetupGames(fileSystem, gogKey).ToArray(); - - handler.ShouldFindAllGamesById(expectedGames, game => game.Id); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllInterfaceGames(IFileSystem fileSystem, InMemoryRegistry registry) - { - var (handler, gogKey) = SetupHandler(fileSystem, registry); - var expectedGames = SetupGames(fileSystem, gogKey); - - handler.ShouldFindAllInterfacesGames(expectedGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/ArrangeHelpers.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/ArrangeHelpers.cs deleted file mode 100644 index 7eb7fe21..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Web; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - private static (OriginHandler handler, AbsolutePath manifestDir) SetupHandler(InMemoryFileSystem fs) - { - var manifestDir = OriginHandler.GetManifestDir(fs); - fs.AddDirectory(manifestDir); - - var handler = new OriginHandler(fs); - return (handler, manifestDir); - } - - private static IEnumerable SetupGames(InMemoryFileSystem fs, AbsolutePath manifestDir) - { - var fixture = new Fixture(); - - fixture.Customize(composer => composer - .FromFactory(id => - { - var installPath = manifestDir.Combine(id); - var manifest = manifestDir.Combine($"{id}.mfst"); - - fs.AddFile(manifest, $"?id={HttpUtility.UrlEncode(id)}&dipInstallPath={HttpUtility.UrlEncode(installPath.GetFullPath())}"); - return new OriginGame(OriginGameId.From(id), installPath); - }) - .OmitAutoProperties()); - - return fixture.CreateMany(); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/GameFinder.StoreHandlers.Origin.Tests.csproj b/tests/GameFinder.StoreHandlers.Origin.Tests/GameFinder.StoreHandlers.Origin.Tests.csproj index 97abd25c..f5167bf4 100644 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/GameFinder.StoreHandlers.Origin.Tests.csproj +++ b/tests/GameFinder.StoreHandlers.Origin.Tests/GameFinder.StoreHandlers.Origin.Tests.csproj @@ -3,4 +3,13 @@ + + + + <_Files Include="files\**" /> + + + + + diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/ManifestParserTests.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/ManifestParserTests.cs new file mode 100644 index 00000000..1b79f549 --- /dev/null +++ b/tests/GameFinder.StoreHandlers.Origin.Tests/ManifestParserTests.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Logging; +using NexusMods.Paths; +using NexusMods.Paths.TestingHelpers; +using TestUtils; +using Xunit.Abstractions; + +namespace GameFinder.StoreHandlers.Origin.Tests; + +public class ManifestParserTests +{ + private readonly ILogger _logger; + public ManifestParserTests(ITestOutputHelper output) + { + _logger = new XunitLogger(output); + } + + [Theory, AutoFileSystem] + public void Test_ParseManifestFile(IFileSystem fileSystem, AbsolutePath manifestFile, AbsolutePath installPath) + { + var res = ManifestParser.ParseManifestFile( + _logger, + fileSystem, + contents: $"iD=foo&id=&dipInstallPatH=&dipInstallPath={installPath.ToString()}", + manifestFile + ); + + res.Should().NotBeNull(); + res.Should().Be(new OriginGame + { + Id = OriginGameId.From("foo"), + Path = installPath, + }); + } + + [Theory, AutoFileSystem] + public void Test_SkipSteamGame(IFileSystem fileSystem, AbsolutePath manifestFile) + { + var res = ManifestParser.ParseManifestFile( + _logger, + fileSystem, + contents: "id=foo@steam&dipInstallPath=bar", + manifestFile + ); + + res.Should().BeNull(); + } + + [Theory] + [InlineData("Origin.OFR.50.0002694.mfst")] + public async Task Test_WithFile(string fileName) + { + var fileSystem = FileSystem.Shared; + + var file = fileSystem.GetKnownPath(KnownPath.EntryDirectory).Combine("files").Combine(fileName); + var contents = await file.ReadAllTextAsync(); + + var fakeFileSystem = new InMemoryFileSystem(OSInformation.FakeWindows); + + var res = ManifestParser.ParseManifestFile( + _logger, + fakeFileSystem, + contents, + file + ); + + res.Should().NotBeNull(); + res.Should().Be(new OriginGame + { + Id = OriginGameId.From("Origin.OFR.50.0002694"), + Path = fakeFileSystem.FromUnsanitizedFullPath("C:/Program Files (x86)/Origin Games/Apex"), + }); + } +} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_InvalidManifest.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_InvalidManifest.cs deleted file mode 100644 index 1af953d1..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_InvalidManifest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_InvalidManifest(InMemoryFileSystem fs, string manifestName) - { - var (handler, manifestDir) = SetupHandler(fs); - - var randomBytes = new byte[128]; - Random.Shared.NextBytes(randomBytes); - - var manifest = manifestDir.Combine($"{manifestName}.mfst"); - fs.AddFile(manifest, randomBytes); - - // can't seem to reach the exception, even random garbage doesn't throw - // an exception... - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"id\""); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_MissingValues.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_MissingValues.cs deleted file mode 100644 index 513e109d..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_MissingValues.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Web; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingId(InMemoryFileSystem fs, string manifestName) - { - var (handler, manifestDir) = SetupHandler(fs); - - var manifest = manifestDir.Combine($"{manifestName}.mfst"); - fs.AddFile(manifest, ""); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"id\""); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingInstallPath(InMemoryFileSystem fs, - string manifestName, string id) - { - var (handler, manifestDir) = SetupHandler(fs); - - var manifest = manifestDir.Combine($"{manifestName}.mfst"); - fs.AddFile(manifest, $"?id={HttpUtility.UrlEncode(id)}"); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest {manifest} does not have a value \"dipInstallPath\""); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifestDir.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifestDir.cs deleted file mode 100644 index af20a413..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifestDir.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_NoManifestDir(InMemoryFileSystem fs) - { - var manifestDir = OriginHandler.GetManifestDir(fs); - var handler = new OriginHandler(fs); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest folder {manifestDir.GetFullPath()} does not exist!"); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifests.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifests.cs deleted file mode 100644 index 773788bd..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldError_NoManifests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_NoManifests(InMemoryFileSystem fs) - { - var (handler, manifestDir) = SetupHandler(fs); - - var error = handler.ShouldOnlyBeOneError(); - error.Should().Be($"Manifest folder {manifestDir} does not contain any .mfst files"); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldSkip_SteamGames.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldSkip_SteamGames.cs deleted file mode 100644 index 572edec2..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldSkip_SteamGames.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Web; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldSkip_SteamGames(InMemoryFileSystem fs, string manifestName, - string id, AbsolutePath installPath) - { - var (handler, manifestDir) = SetupHandler(fs); - - var manifest = manifestDir.Combine($"{manifestName}.mfst"); - fs.AddFile(manifest, $"?id={HttpUtility.UrlEncode(id)}@steam" + - $"&dipInstallPath={HttpUtility.UrlEncode(installPath.GetFullPath())}"); - - var results = handler.FindAllGames().ToArray(); - results.Should().BeEmpty(); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_FindAllGames.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_FindAllGames.cs deleted file mode 100644 index 281e61a0..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_FindAllGames.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGames(InMemoryFileSystem fs) - { - var (handler, manifestDir) = SetupHandler(fs); - var expectedGames = SetupGames(fs, manifestDir); - handler.ShouldFindAllGames(expectedGames); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGamesById(InMemoryFileSystem fs) - { - var (handler, manifestDir) = SetupHandler(fs); - var expectedGames = SetupGames(fs, manifestDir).ToArray(); - handler.ShouldFindAllGamesById(expectedGames, game => game.Id); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllInterfaceGames(InMemoryFileSystem fs) - { - var (handler, manifestDir) = SetupHandler(fs); - var expectedGames = SetupGames(fs, manifestDir); - handler.ShouldFindAllInterfacesGames(expectedGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_WithDuplicateKeys.cs b/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_WithDuplicateKeys.cs deleted file mode 100644 index 9999fa04..00000000 --- a/tests/GameFinder.StoreHandlers.Origin.Tests/Test_ShouldWork_WithDuplicateKeys.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Web; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Origin.Tests; - -public partial class OriginTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_WithDuplicateKeys(InMemoryFileSystem fs, string manifestName, - string id, AbsolutePath installPath) - { - var (handler, manifestDir) = SetupHandler(fs); - - var manifest = manifestDir.Combine($"{manifestName}.mfst"); - fs.AddFile(manifest, $"?id={HttpUtility.UrlEncode(id)}" + - $"&ID={HttpUtility.UrlEncode(id)}" + - $"&dipInstallPath={HttpUtility.UrlEncode(installPath.GetFullPath())}" + - $"&dipinstallpath={HttpUtility.UrlEncode(installPath.GetFullPath())}"); - - var results = handler.FindAllGames().ToArray(); - - var games = results.ShouldOnlyBeGames().ToArray(); - games.Should().HaveCount(1); - - var game = games[0]; - game.Id.Should().Be(OriginGameId.From(id)); - game.InstallPath.Should().Be(installPath); - } -} diff --git a/tests/GameFinder.StoreHandlers.Origin.Tests/files/Origin.OFR.50.0002694.mfst b/tests/GameFinder.StoreHandlers.Origin.Tests/files/Origin.OFR.50.0002694.mfst new file mode 100644 index 00000000..e5c4106b --- /dev/null +++ b/tests/GameFinder.StoreHandlers.Origin.Tests/files/Origin.OFR.50.0002694.mfst @@ -0,0 +1 @@ +?activerepair=1&autoresume=0&autostart=0&buildid=%2fshift%2fapex_legends%2fapex_legends%2ffg__ww_us_retail_binaries%2fapex_legendsnintendo_3dsfg__ww_us_retail_binariesconcept_2905__r5pc_r5201_j29_cl6350311_ex6402312_6403685_2024_021aa0f00732f410b83ea30e46079ebd7.zip&contentversion=1.1.5.6¤tstate=kPaused&ddinitialdownload=1&ddinstallalreadycompleted=0&ddrequiredportion=53081289215&dipInstallPath=&dipinstallpath=C%3a%5cProgram%20Files%20(x86)%5cOrigin%20Games%5cApex%5c&downloaderversion=9.0.0.0&downloading=1&dynamicdownload=1&eula____installer_directx_eula_en_us_txt=2103371362&eula____installer_vc_vc2010sp1_eula_en_us_rtf=774049465&eula____installer_vc_vc2012update4_eula_en_us_rtf=3624828530&eula____installer_vc_vc2015_eula_en_us_rtf=4167961192&eula__support_privacy and cookie policy_en_us_html=1663766667&eula__support_user agreement_en_us_html=3025288659&eulasaccepted=1&id=Origin.OFR.50.0002694&installdesktopshortcut=0&installerchanged=0&installstartmenushortcut=0&isitoflow=0&islocalsource=0&ispreload=0&isrepair=0&jobid=%7b6146febc-d64d-4357-b2dc-f50a73c9d140%7d&locale=en_US&optionalcomponentstoinstall=0&paused=1&previousstate=kTransferring&repairstate=repairing&savedbytes=42446633&stagedfilecount=0&totalbytes=77329918421&totaldownloadbytes=75632539861 diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/ArrangeHelper.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/ArrangeHelper.cs deleted file mode 100644 index 13852b47..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/ArrangeHelper.cs +++ /dev/null @@ -1,190 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; - -namespace GameFinder.StoreHandlers.Steam.Tests; - -public static class ArrangeHelper -{ - private static AbsolutePath CreateOrReturnPath(IFileSystem fileSystem, AbsolutePath path, bool createDirectory) - { - if (!createDirectory) return path; - fileSystem.CreateDirectory(path); - return path; - } - - public static AbsolutePath CreateSteamPath(IFileSystem fileSystem, bool createDirectory = false) - { - var steamPath = fileSystem - .GetKnownPath(KnownPath.TempDirectory) - .Combine($"Steam-{Guid.NewGuid():N}"); - - return CreateOrReturnPath(fileSystem, steamPath, createDirectory); - } - - public static AbsolutePath CreateSteamLibraryPath(IFileSystem fileSystem, bool createDirectory = false) - { - var libraryPath = fileSystem - .GetKnownPath(KnownPath.TempDirectory) - .Combine($"SteamLibrary-{Guid.NewGuid():N}"); - - return CreateOrReturnPath(fileSystem, libraryPath, createDirectory); - } - - public static AbsolutePath CreateAppManifestPath(IFileSystem fileSystem, AbsolutePath libraryPath = default) - { - if (libraryPath == default) libraryPath = CreateSteamLibraryPath(fileSystem); - return libraryPath.Combine("steamapps").Combine($"appmanifest_{Guid.NewGuid():N}.acf"); - } - - public static SteamId CreateSteamId() => SteamId.From(76561198110222274); - - private static Fixture SetupFixture() - { - var fixture = new Fixture(); - fixture.Customize(composer => composer.FromFactory(Size.From)); - fixture.Customize(composer => composer.FromFactory(BuildId.From)); - fixture.Customize(composer => composer.FromFactory(SteamId.From)); - fixture.Customize(composer => composer.FromFactory(DepotId.From)); - fixture.Customize(composer => composer.FromFactory(AppId.From)); - fixture.Customize(composer => composer.FromFactory(WorkshopItemId.From)); - fixture.Customize(composer => composer.FromFactory(WorkshopManifestId.From)); - fixture.Customize(composer => composer.FromFactory(ManifestId.From)); - fixture.Customize(composer => composer.FromFactory(() => DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.ToUnixTimeSeconds()))); - fixture.Customize(composer => composer.FromFactory(x => TimeSpan.FromMinutes(x))); - - return fixture; - } - - public static AppManifest CreateAppManifest(AbsolutePath manifestPath) - { - var fixture = SetupFixture(); - return new AppManifest - { - ManifestPath = manifestPath, - AppId = fixture.Create(), - Universe = SteamUniverse.Public, - Name = fixture.Create(), - StateFlags = StateFlags.FullyInstalled, - InstallationDirectory = manifestPath.Parent.Combine("common").Combine(fixture.Create()), - LastUpdated = fixture.Create(), - SizeOnDisk = fixture.Create(), - StagingSize = fixture.Create(), - BuildId = fixture.Create(), - LastOwner = fixture.Create(), - UpdateResult = 0, - BytesToDownload = fixture.Create(), - BytesDownloaded = fixture.Create(), - BytesToStage = fixture.Create(), - BytesStaged = fixture.Create(), - TargetBuildId = fixture.Create(), - AutoUpdateBehavior = AutoUpdateBehavior.AlwaysUpdated, - BackgroundDownloadBehavior = BackgroundDownloadBehavior.AlwaysAllow, - ScheduledAutoUpdate = fixture.Create(), - FullValidateAfterNextUpdate = true, - InstalledDepots = fixture - .CreateMany() - .Select(depotId => new InstalledDepot - { - DepotId = depotId, - ManifestId = fixture.Create(), - SizeOnDisk = fixture.Create(), - }) - .ToDictionary(depot => depot.DepotId, depot => depot), - InstallScripts = fixture - .CreateMany() - .Select(depotId => (depotId, fixture.Create().ToRelativePath())) - .ToDictionary(kv => kv.Item1, kv => kv.Item2), - SharedDepots = fixture - .CreateMany() - .Select(depotId => (depotId, fixture.Create())) - .ToDictionary(kv => kv.Item1, kv => kv.Item2), - UserConfig = fixture - .CreateMany() - .Select(key => (key, fixture.Create())) - .ToDictionary(kv => kv.Item1, kv => kv.Item2, StringComparer.OrdinalIgnoreCase), - MountedConfig = fixture - .CreateMany() - .Select(key => (key, fixture.Create())) - .ToDictionary(kv => kv.Item1, kv => kv.Item2, StringComparer.OrdinalIgnoreCase), - }; - } - - public static WorkshopManifest CreateWorkshopManifest(AbsolutePath manifestPath) - { - var fixture = SetupFixture(); - - return new WorkshopManifest - { - ManifestPath = manifestPath, - AppId = fixture.Create(), - SizeOnDisk = fixture.Create(), - NeedsUpdate = fixture.Create(), - NeedsDownload = fixture.Create(), - LastUpdated = fixture.Create(), - LastAppStart = fixture.Create(), - InstalledWorkshopItems = fixture - .CreateMany() - .Select(x => new WorkshopItemDetails - { - ItemId = x, - SizeOnDisk = fixture.Create(), - ManifestId = fixture.Create(), - LastUpdated = fixture.Create(), - LastTouched = fixture.Create(), - SubscribedBy = SteamId.FromAccountId(fixture.Create()), - }) - .ToDictionary(x => x.ItemId, x => x), - }; - } - - public static LibraryFoldersManifest CreateLibraryFoldersManifest(AbsolutePath manifestPath) - { - var fixture = SetupFixture(); - - return new LibraryFoldersManifest - { - ManifestPath = manifestPath, - LibraryFolders = fixture.CreateMany() - .Select(folderNames => - { - return new LibraryFolder - { - Path = manifestPath.FileSystem - .GetKnownPath(KnownPath.TempDirectory) - .Combine(folderNames), - Label = fixture.Create(), - TotalDiskSize = fixture.Create(), - AppSizes = fixture.CreateMany() - .Select(id => (id, fixture.Create())) - .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2), - }; - }) - .ToList(), - }; - } - - public static LocalUserConfig CreateLocalUserConfig(AbsolutePath configPath) - { - var fixture = SetupFixture(); - - return new LocalUserConfig - { - ConfigPath = configPath, - User = fixture.Create(), - LocalAppData = fixture.CreateMany() - .Select(id => new LocalAppData - { - AppId = id, - LastPlayed = fixture.Create(), - Playtime = fixture.Create(), - LaunchOptions = fixture.Create(), - }) - .ToDictionary(x => x.AppId, x => x), - InGameOverlayScreenshotSaveUncompressedPath = configPath.FileSystem - .GetKnownPath(KnownPath.TempDirectory) - .Combine(fixture.Create()), - }; - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/AppManifestTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/AppManifestTests.cs deleted file mode 100644 index c15f297f..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/AppManifestTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentResults.Extensions.FluentAssertions; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models; - -public class AppManifestTests -{ - [Theory, AutoFileSystem] - public void Test_Equality(AbsolutePath manifestPath) - { - var value = ArrangeHelper.CreateAppManifest(manifestPath); - value.Equals(value).Should().BeTrue(); - value.GetHashCode().Should().Be(value.GetHashCode()); - } - - [Theory, AutoFileSystem] - public void Test_Reload(AbsolutePath manifestPath) - { - var value = ArrangeHelper.CreateAppManifest(manifestPath); - AppManifestWriter.Write(value, manifestPath).Should().BeSuccess(); - - var result = value.Reload(); - result.Should().BeSuccess().And.HaveValue(value); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/LibraryFolderTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/LibraryFolderTests.cs deleted file mode 100644 index 5469217e..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/LibraryFolderTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models; - -public class LibraryFolderTests -{ - [Fact] - public void Test_Sizes() - { - var fixture = new Fixture(); - fixture.Customize(composer => composer.FromFactory(x => AppId.From((uint)x.GetHashCode()))); - - var fs = new InMemoryFileSystem(); - var libraryFolder = new LibraryFolder - { - Path = fs.GetKnownPath(KnownPath.TempDirectory), - TotalDiskSize = Size.GB * 100, - AppSizes = new Dictionary - { - { fixture.Create(), Size.GB * 2 }, - { fixture.Create(), Size.GB * 8 }, - }, - }; - - libraryFolder.GetTotalSizeOfInstalledApps().Should().Be(Size.GB * 10); - libraryFolder.GetFreeSpaceEstimate().Should().Be(Size.GB * 90); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamAccountTypeTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamAccountTypeTests.cs deleted file mode 100644 index 6b370da4..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamAccountTypeTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models; - -public class SteamAccountTypeTests -{ - [Theory] - [InlineData(SteamAccountType.Invalid, 'I')] - [InlineData(SteamAccountType.Individual, 'U')] - [InlineData(SteamAccountType.Multiseat, 'M')] - [InlineData(SteamAccountType.GameServer, 'G')] - [InlineData(SteamAccountType.AnonGameServer, 'A')] - [InlineData(SteamAccountType.Pending, 'P')] - [InlineData(SteamAccountType.ContentServer, 'C')] - [InlineData(SteamAccountType.Clan, 'g')] - [InlineData(SteamAccountType.Chat, 'T')] - [InlineData(SteamAccountType.AnonUser, 'a')] - [InlineData((SteamAccountType)byte.MaxValue, '?')] - public void Test_GetLetter(SteamAccountType accountType, char expectedLetter) - { - var actualLetter = accountType.GetLetter(); - actualLetter.Should().Be(expectedLetter); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamIdTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamIdTests.cs deleted file mode 100644 index 8731a4ed..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/SteamIdTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models; - -public class SteamIdTests -{ - [Theory] - [InlineData( - 76561198092541763, - SteamUniverse.Public, - SteamAccountType.Individual, - 132276035, - 66138017, - "STEAM_1:1:66138017", - "[U:1:132276035]")] - [InlineData( - 76561198110222274, - SteamUniverse.Public, - SteamAccountType.Individual, - 149956546, - 74978273, - "STEAM_1:0:74978273", - "[U:1:149956546]")] - public void Test_SteamId( - ulong input, - SteamUniverse expectedUniverse, - SteamAccountType expectedAccountType, - uint expectedAccountId, - uint expectedAccountNumber, - string expectedSteam2Id, - string expectedSteam3Id) - { - var steamId = new SteamId(input); - - steamId.Universe.Should().Be(expectedUniverse); - steamId.AccountType.Should().Be(expectedAccountType); - steamId.AccountId.Should().Be(expectedAccountId); - steamId.AccountNumber.Should().Be(expectedAccountNumber); - steamId.Steam2Id.Should().Be(expectedSteam2Id); - steamId.Steam3Id.Should().Be(expectedSteam3Id); - - steamId.GetProfileUrl().Should().EndWith($"/{input}"); - steamId.GetSteam3IdProfileUrl().Should().EndWith($"/{expectedSteam3Id}"); - - steamId.ToString().Should().Be(expectedSteam3Id); - } - - [Theory] - [InlineData( - 149956546, - 76561193815254978, - 74978273, - "STEAM_1:0:74978273", - "[U:1:149956546]")] - public void Test_FromAccountId( - uint input, - ulong expectedRawId, - uint expectedAccountNumber, - string expectedSteam2Id, - string expectedSteam3Id) - { - var steamId = SteamId.FromAccountId(input); - - steamId.RawId.Should().Be(expectedRawId); - steamId.Universe.Should().Be(SteamUniverse.Public); - steamId.AccountType.Should().Be(SteamAccountType.Individual); - steamId.AccountId.Should().Be(input); - steamId.AccountNumber.Should().Be(expectedAccountNumber); - steamId.Steam2Id.Should().Be(expectedSteam2Id); - steamId.Steam3Id.Should().Be(expectedSteam3Id); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/AppIdTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/AppIdTests.cs deleted file mode 100644 index 841141f7..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/AppIdTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models.ValueTypes; - -public class AppIdTests -{ - [Fact] - public void Test_Empty() { AppId.DefaultValue.Value.Should().Be(0); } - - [Theory] - [InlineData(262060, "https://store.steampowered.com/app/262060")] - public void Test_SteamStoreUrl(uint input, string expectedUrl) - { - var appId = AppId.From(input); - appId.GetSteamStoreUrl().Should().Be(expectedUrl); - } - - [Theory] - [InlineData(262060, "https://steamdb.info/app/262060")] - public void Test_SteamDbUrl(uint input, string expectedUrl) - { - var appId = AppId.From(input); - appId.GetSteamDbUrl().Should().Be(expectedUrl); - } - - [Theory] - [InlineData(262060, "äüö", "https://store.steampowered.com/app/262060/?utm_source=%c3%a4%c3%bc%c3%b6")] - public void Test_GetSteamStoreUrlWithTracking(uint input, string source, string expectedUrl) - { - var appId = AppId.From(input); - appId.GetSteamStoreUrl(source).Should().Be(expectedUrl); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/BuildIdTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/BuildIdTests.cs deleted file mode 100644 index a63e142e..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/BuildIdTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models.ValueTypes; - -public class BuildIdTests -{ - [Fact] - public void Test_Empty() { BuildId.DefaultValue.Value.Should().Be(0); } - - [Theory] - [InlineData(6248449, "https://steamdb.info/patchnotes/6248449")] - public void Test_SteamDbUpdateNotesUrl(uint input, string expectedUrl) - { - var buildId = BuildId.From(input); - buildId.GetSteamDbUpdateNotesUrl().Should().Be(expectedUrl); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/DepotIdTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/DepotIdTests.cs deleted file mode 100644 index 5f0d26d7..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/DepotIdTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models.ValueTypes; - -public class DepotIdTests -{ - [Fact] - public void Test_Empty() { DepotId.DefaultValue.Value.Should().Be(0); } - - [Theory] - [InlineData(262061, "https://steamdb.info/depot/262061")] - public void Test_SteamDbUpdateNotesUrl(uint input, string expectedUrl) - { - var depotId = DepotId.From(input); - depotId.GetSteamDbUrl().Should().Be(expectedUrl); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/ManifestIdTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/ManifestIdTests.cs deleted file mode 100644 index 8cf988f5..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Models/ValueTypes/ManifestIdTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; - -namespace GameFinder.StoreHandlers.Steam.Tests.Models.ValueTypes; - -public class ManifestIdTests -{ - [Fact] - public void Test_Empty() { ManifestId.DefaultValue.Value.Should().Be(0); } - - [Theory] - [InlineData(5542773349944116172, 262061, "https://steamdb.info/depot/262061/history/?changeid=M:5542773349944116172")] - public void Test_GetSteamDbChangesetUrl(ulong input, uint depot, string expectedUrl) - { - var manifestId = ManifestId.From(input); - var depotId = DepotId.From(depot); - manifestId.GetSteamDbChangesetUrl(depotId).Should().Be(expectedUrl); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/AppManifestParserTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Services/AppManifestParserTests.cs deleted file mode 100644 index 43b896d5..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/AppManifestParserTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentResults.Extensions.FluentAssertions; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Steam.Tests.Services; - -public class AppManifestParserTests -{ - [Theory, AutoFileSystem] - public void Test_Success_OnlyRequired(AbsolutePath manifestFilePath) - { - var expected = new AppManifest - { - ManifestPath = manifestFilePath, - AppId = AppId.From(262060), - Name = "Darkest Dungeon", - StateFlags = StateFlags.FullyInstalled, - InstallationDirectory = manifestFilePath.Parent.Combine("common").Combine("DarkestDungeon"), - }; - - var writeResult = AppManifestWriter.Write(expected, manifestFilePath); - writeResult.Should().BeSuccess(); - - var result = AppManifestParser.ParseManifestFile(manifestFilePath); - result.Should().BeSuccess().And.HaveValue(expected); - } - - [Theory, AutoFileSystem] - public void Test_Success_Everything(AbsolutePath manifestFilePath) - { - var expected = ArrangeHelper.CreateAppManifest(manifestFilePath); - - var writeResult = AppManifestWriter.Write(expected, manifestFilePath); - writeResult.Should().BeSuccess(); - - var result = AppManifestParser.ParseManifestFile(manifestFilePath); - result.Should().BeSuccess().And.HaveValue(expected); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LibraryFoldersManifestParserTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LibraryFoldersManifestParserTests.cs deleted file mode 100644 index 8d854d54..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LibraryFoldersManifestParserTests.cs +++ /dev/null @@ -1,23 +0,0 @@ - - -using FluentResults.Extensions.FluentAssertions; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Steam.Tests.Services; - -public class LibraryFoldersManifestParserTests -{ - [Theory, AutoFileSystem] - public void Test_Everything(AbsolutePath manifestFilePath) - { - var expected = ArrangeHelper.CreateLibraryFoldersManifest(manifestFilePath); - - var writeResult = LibraryFoldersManifestWriter.Write(expected, manifestFilePath); - writeResult.Should().BeSuccess(); - - var result = LibraryFoldersManifestParser.ParseManifestFile(manifestFilePath); - result.Should().BeSuccess().And.HaveValue(expected); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LocalUserConfigParserTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LocalUserConfigParserTests.cs deleted file mode 100644 index 63ad54d1..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/LocalUserConfigParserTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentResults.Extensions.FluentAssertions; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Steam.Tests.Services; - -public class LocalUserConfigParserTests -{ - [Theory, AutoFileSystem] - public async Task Test_Success(AbsolutePath configPath) - { - var expected = ArrangeHelper.CreateLocalUserConfig(configPath); - - var writeResult = LocalUserConfigWriter.Write(expected, configPath); - writeResult.Should().BeSuccess(); - - var result = LocalUserConfigParser.ParseConfigFile(expected.User, configPath); - result.Should().BeSuccess().And.HaveValue(expected); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/SteamLocationFinderTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Services/SteamLocationFinderTests.cs deleted file mode 100644 index 839a05ac..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/SteamLocationFinderTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Globalization; -using System.Runtime.InteropServices; -using FluentResults.Extensions.FluentAssertions; -using GameFinder.RegistryUtils; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Tests.Services; - -public class SteamLocationFinderTests -{ - [Fact] - public void Test_FindSteam() - { - var fs = new InMemoryFileSystem(); - var registry = new InMemoryRegistry(); - - var res = SteamLocationFinder.FindSteam(fs, registry: null); - res - .Should().BeFailure() - .And.HaveError("Unable to find a valid Steam installation at the default installation paths!"); - - res = SteamLocationFinder.FindSteam(fs, registry); - res - .Should().BeFailure() - .And.HaveError("Unable to find a valid Steam installation at the default installation paths, and in the Registry!"); - - var steamPathFromRegistry = ArrangeHelper.CreateSteamPath(fs, createDirectory: true); - var libraryFoldersFilePathFromRegistry = SteamLocationFinder.GetLibraryFoldersFilePath(steamPathFromRegistry); - fs.AddEmptyFile(libraryFoldersFilePathFromRegistry); - - var key = registry.AddKey(RegistryHive.CurrentUser, SteamLocationFinder.SteamRegistryKey); - key.AddValue(SteamLocationFinder.SteamRegistryValueName, steamPathFromRegistry.GetFullPath()); - - res = SteamLocationFinder.FindSteam(fs, registry); - res - .Should().BeSuccess() - .And.HaveValue(steamPathFromRegistry); - - var defaultSteamPath = SteamLocationFinder.GetDefaultSteamInstallationPaths(fs).First(); - var defaultLibraryFoldersFilePath = SteamLocationFinder.GetLibraryFoldersFilePath(defaultSteamPath); - - fs.AddDirectory(defaultSteamPath); - fs.AddEmptyFile(defaultLibraryFoldersFilePath); - - res = SteamLocationFinder.FindSteam(fs, registry: null); - res - .Should().BeSuccess() - .And.HaveValue(defaultSteamPath); - } - - [Fact] - public void Test_IsValidSteamInstallation() - { - var fs = new InMemoryFileSystem(); - var steamPath = ArrangeHelper.CreateSteamPath(fs); - - SteamLocationFinder - .IsValidSteamInstallation(steamPath) - .Should().BeFalse(because: "The Steam directory doesn't exist"); - - fs.AddDirectory(steamPath); - - SteamLocationFinder - .IsValidSteamInstallation(steamPath) - .Should().BeFalse(because: "The libraryfolders.vdf file doesn't exist"); - - var libraryFoldersFilePath = SteamLocationFinder.GetLibraryFoldersFilePath(steamPath); - fs.AddEmptyFile(libraryFoldersFilePath); - - SteamLocationFinder - .IsValidSteamInstallation(steamPath) - .Should().BeTrue(); - } - - [Fact] - public void Test_GetLibraryFoldersFile() - { - var fs = new InMemoryFileSystem(); - var steamPath = ArrangeHelper.CreateSteamPath(fs); - var libraryFoldersFilePath = steamPath.Combine("config").Combine("libraryfolders.vdf"); - - SteamLocationFinder.GetLibraryFoldersFilePath(steamPath).Should().Be(libraryFoldersFilePath); - } - - [Fact] - public void Test_GetUserDataDirectoryPath() - { - var fs = new InMemoryFileSystem(); - var steamPath = ArrangeHelper.CreateSteamPath(fs); - var steamId = ArrangeHelper.CreateSteamId(); - var userDataDirectoryPath = steamPath - .Combine("userdata") - .Combine(steamId.AccountId.ToString(CultureInfo.InvariantCulture)); - - SteamLocationFinder.GetUserDataDirectoryPath(steamPath, steamId).Should().Be(userDataDirectoryPath); - } - - [Fact] - public void Test_GetSteamPathFromRegistry() - { - var fs = new InMemoryFileSystem(); - var registry = new InMemoryRegistry(); - var steamPath = ArrangeHelper.CreateSteamPath(fs, createDirectory: true); - - var res = SteamLocationFinder.GetSteamPathFromRegistry(fs, registry); - res - .Should().BeFailure() - .And.HaveError("Unable to open the Steam registry key!"); - - var key = registry.AddKey(RegistryHive.CurrentUser, SteamLocationFinder.SteamRegistryKey); - - res = SteamLocationFinder.GetSteamPathFromRegistry(fs, registry); - res - .Should().BeFailure() - .And.HaveError("Unable to get string value from the Steam registry key!"); - - key.AddValue(SteamLocationFinder.SteamRegistryValueName, steamPath.GetFullPath()); - - res = SteamLocationFinder.GetSteamPathFromRegistry(fs, registry); - res - .Should().BeSuccess() - .And.HaveValue(steamPath); - } - - [Fact] - public void Test_GetDefaultSteamInstallationPaths_Linux() - { - var fs = new InMemoryFileSystem(new OSInformation(OSPlatform.Linux)); - SteamLocationFinder - .GetDefaultSteamInstallationPaths(fs) - .ToArray() - .Should().HaveCount(6); - } - - [Fact] - public void Test_GetDefaultSteamInstallationPaths_Windows() - { - var fs = new InMemoryFileSystem(new OSInformation(OSPlatform.Windows)); - var overlayFileSystem = fs.CreateOverlayFileSystem( - new Dictionary(), - new Dictionary - { - { KnownPath.ProgramFilesX86Directory, fs.GetKnownPath(KnownPath.TempDirectory) }, - }); - - SteamLocationFinder - .GetDefaultSteamInstallationPaths(overlayFileSystem) - .ToArray() - .Should().HaveCount(1); - } - - [Fact] - public void Test_GetDefaultSteamInstallationPaths_OSX() - { - var fs = new InMemoryFileSystem(new OSInformation(OSPlatform.OSX)); - - var overlayFileSystem = fs.CreateOverlayFileSystem( - new Dictionary(), - new Dictionary - { - { KnownPath.ProgramFilesX86Directory, fs.GetKnownPath(KnownPath.TempDirectory) }, - }); - - SteamLocationFinder - .GetDefaultSteamInstallationPaths(overlayFileSystem) - .ToArray() - .Should().HaveCount(1); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/WorkshopManifestParserTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/Services/WorkshopManifestParserTests.cs deleted file mode 100644 index 17c2ff9f..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/Services/WorkshopManifestParserTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FluentResults.Extensions.FluentAssertions; -using GameFinder.StoreHandlers.Steam.Models; -using GameFinder.StoreHandlers.Steam.Models.ValueTypes; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Steam.Tests.Services; - -public class WorkshopManifestParserTests -{ - [Theory, AutoFileSystem] - public void Test_Success_OnlyRequired(AbsolutePath manifestFilePath) - { - var expected = new WorkshopManifest - { - ManifestPath = manifestFilePath, - AppId = AppId.From(262060), - }; - - var writeResult = WorkshopManifestWriter.Write(expected, manifestFilePath); - writeResult.Should().BeSuccess(); - - var result = WorkshopManifestParser.ParseManifestFile(manifestFilePath); - result.Should().BeSuccess().And.HaveValue(expected); - } - - [Theory, AutoFileSystem] - public void Test_Success_Everything(AbsolutePath manifestFilePath) - { - var expected = ArrangeHelper.CreateWorkshopManifest(manifestFilePath); - - var writeResult = WorkshopManifestWriter.Write(expected, manifestFilePath); - writeResult.Should().BeSuccess(); - - var result = WorkshopManifestParser.ParseManifestFile(manifestFilePath); - result.Should().BeSuccess().And.HaveValue(expected); - } -} diff --git a/tests/GameFinder.StoreHandlers.Steam.Tests/SteamHandlerTests.cs b/tests/GameFinder.StoreHandlers.Steam.Tests/SteamHandlerTests.cs deleted file mode 100644 index 129998f7..00000000 --- a/tests/GameFinder.StoreHandlers.Steam.Tests/SteamHandlerTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FluentResults.Extensions.FluentAssertions; -using GameFinder.Common; -using GameFinder.StoreHandlers.Steam.Services; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Steam.Tests; - -public class SteamHandlerTests -{ - [Fact] - public void Test_FindAllGames() - { - var fs = new InMemoryFileSystem(); - - var steamHandler = new SteamHandler(fs, registry: null); - steamHandler.FindAllGames() - .Should().ContainSingle() - .Which.Value - .Should().BeOfType() - .Which.Message - .Should().Be("Unable to find a valid Steam installation at the default installation paths!"); - - var steamPath = SteamLocationFinder.GetDefaultSteamInstallationPaths(fs).First(); - fs.AddDirectory(steamPath); - - var libraryFoldersFilePath = SteamLocationFinder.GetLibraryFoldersFilePath(steamPath); - fs.AddEmptyFile(libraryFoldersFilePath); - - steamHandler.FindAllGames() - .Should().ContainSingle() - .Which.Value - .Should().BeOfType() - .Which.Message - .Should().Be("Exception was thrown while parsing the manifest file!"); - - var libraryFoldersManifest = ArrangeHelper.CreateLibraryFoldersManifest(libraryFoldersFilePath); - LibraryFoldersManifestWriter.Write(libraryFoldersManifest, libraryFoldersFilePath).Should().BeSuccess(); - - var expectedSteamGames = new List(); - foreach (var libraryFolder in libraryFoldersManifest) - { - fs.AddDirectory(libraryFolder.Path); - var appManifestPath = ArrangeHelper.CreateAppManifestPath(fs, libraryFolder.Path); - var appManifest = ArrangeHelper.CreateAppManifest(appManifestPath); - AppManifestWriter.Write(appManifest, appManifestPath).Should().BeSuccess(); - - var steamGame = new SteamGame - { - AppManifest = appManifest, - LibraryFolder = libraryFolder, - SteamPath = steamPath, - }; - - expectedSteamGames.Add(steamGame); - } - - var results = steamHandler.FindAllGames().ToArray(); - results - .Select(x => x.Value) - .Should().AllBeOfType() - .Which - .Should().Equal(expectedSteamGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/AppManifestTests.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/AppManifestTests.cs new file mode 100644 index 00000000..6e7061d4 --- /dev/null +++ b/tests/GameFinder.StoreHandlers.Xbox.Tests/AppManifestTests.cs @@ -0,0 +1,45 @@ +using System.Xml; +using System.Xml.Schema; +using GameFinder.StoreHandlers.Xbox.Serialization; +using TestUtils; +using Xunit.Abstractions; + +namespace GameFinder.StoreHandlers.Xbox.Tests; + +public class AppManifestTests : TestWrapper +{ + public AppManifestTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Test_ParseManifest() + { + var file = GetTestFile("appxmanifest.xml"); + using var stream = FileSystem.ReadFile(file); + using var reader = XmlReader.Create(stream, new XmlReaderSettings + { + IgnoreComments = true, + IgnoreWhitespace = true, + ValidationFlags = XmlSchemaValidationFlags.AllowXmlAttributes, + }); + + var res = AppManifest.ParseAppManifest( + Logger, + reader, + file + ); + + res.Should().NotBeNull(); + res.Should().Be(new AppManifest.Package + { + Identity = new AppManifest.Identity + { + // ReSharper disable once StringLiteralTypo + Name = "BethesdaSoftworks.FalloutNewVegas", + }, + Properties = new AppManifest.Properties + { + DisplayName = "Fallout: New Vegas Ultimate Edition (PC)", + }, + }); + } +} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/ArrangeHelpers.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/ArrangeHelpers.cs deleted file mode 100644 index 6f188db3..00000000 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Text; -using NexusMods.Paths; - -namespace GameFinder.StoreHandlers.Xbox.Tests; - -public partial class XboxTests -{ - private static byte[] CreateGamingRootFile(ICollection folders) - { - using var ms = new MemoryStream(); - using var writer = new BinaryWriter(ms, Encoding.Unicode); - writer.Write(0x58424752); - writer.Write((uint)folders.Count); - - foreach (var folder in folders) - { - var nameBytes = Encoding.Unicode.GetBytes(folder.FileName); - writer.Write(nameBytes); - writer.Write('\0'); - } - - var bytes = ms.ToArray(); - return bytes; - } - - private static string CreateAppManifestFile(string id, string displayName) - { - var xmlContents = $""" - - - - - {displayName} - - -"""; - - return xmlContents; - } - - private static XboxHandler SetupHandler(InMemoryFileSystem fs, AbsolutePath appFolder) - { - fs.AddDirectory(fs.EnumerateRootDirectories().First()); - - var gamingRootFileContents = CreateGamingRootFile(new[] { appFolder }); - var gamingRootFilePath = fs - .EnumerateRootDirectories() - .First() - .Combine(".GamingRoot"); - - fs.AddFile(gamingRootFilePath, gamingRootFileContents); - return new XboxHandler(fs); - } - - private static IEnumerable SetupGames(InMemoryFileSystem fs, AbsolutePath appFolder) - { - var fixture = new Fixture(); - - fixture.Customize(composer => composer - .FromFactory((id, displayName) => - { - var gamePath = appFolder.Combine(id); - var appManifestPath = gamePath.Combine("appxmanifest.xml"); - var appManifestContents = CreateAppManifestFile(id, displayName); - - fs.AddDirectory(gamePath); - fs.AddFile(appManifestPath, appManifestContents); - - var game = new XboxGame(XboxGameId.From(id), displayName, gamePath); - return game; - }) - .OmitAutoProperties()); - - return fixture.CreateMany(); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/GameFinder.StoreHandlers.Xbox.Tests.csproj b/tests/GameFinder.StoreHandlers.Xbox.Tests/GameFinder.StoreHandlers.Xbox.Tests.csproj index f7b92103..aac3ea32 100644 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/GameFinder.StoreHandlers.Xbox.Tests.csproj +++ b/tests/GameFinder.StoreHandlers.Xbox.Tests/GameFinder.StoreHandlers.Xbox.Tests.csproj @@ -3,4 +3,12 @@ + + + + <_Files Include="files\**" /> + + + + diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/GamingRootFileTests.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/GamingRootFileTests.cs new file mode 100644 index 00000000..e9d1c84f --- /dev/null +++ b/tests/GameFinder.StoreHandlers.Xbox.Tests/GamingRootFileTests.cs @@ -0,0 +1,29 @@ +using GameFinder.StoreHandlers.Xbox.Serialization; +using NexusMods.Paths; +using TestUtils; +using Xunit.Abstractions; + +namespace GameFinder.StoreHandlers.Xbox.Tests; + +public class GamingRootFileTests : TestWrapper +{ + public GamingRootFileTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task Test_ParseManifest() + { + var file = GetTestFile("GamingRoot"); + var bytes = await FileSystem.ReadAllBytesAsync(file); + + var res = GamingRootFile.ParseGamingRootFiles( + Logger, + bytes, + file + ); + + res.Should().NotBeNull(); + res!.FilePath.Should().Be(file); + res.Folders.Should().Equal(RelativePath.FromUnsanitizedInput("XboxGames")); + res.GetAbsoluteFolderPaths().Should().Equal(file.Parent.Combine("XboxGames")); + } +} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_AppManifest.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_AppManifest.cs deleted file mode 100644 index f6039595..00000000 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_AppManifest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Xbox.Tests; - -public partial class XboxTests -{ - [Theory, AutoFileSystem] - public void Test_ParseAppManifest(InMemoryFileSystem fs, AbsolutePath appManifestPath, string id, string displayName) - { - var xmlContents = CreateAppManifestFile(id, displayName); - fs.AddFile(appManifestPath, xmlContents); - - var result = XboxHandler.ParseAppManifest(fs, appManifestPath); - result.IsT0.Should().BeTrue(); - result.IsT1.Should().BeFalse(); - - var game = result.AsT0; - game.Id.Should().Be(XboxGameId.From(id)); - game.DisplayName.Should().Be(displayName); - game.Path.Should().Be(appManifestPath.Parent); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_FindAllGames.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_FindAllGames.cs deleted file mode 100644 index 16a302fd..00000000 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_FindAllGames.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; -using TestUtils; - -namespace GameFinder.StoreHandlers.Xbox.Tests; - -public partial class XboxTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGames(InMemoryFileSystem fs, AbsolutePath appFolder) - { - var handler = SetupHandler(fs, appFolder); - var expectedGames = SetupGames(fs, appFolder); - handler.ShouldFindAllGames(expectedGames); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllGamesById(InMemoryFileSystem fs, AbsolutePath appFolder) - { - var handler = SetupHandler(fs, appFolder); - var expectedGames = SetupGames(fs, appFolder).ToArray(); - handler.ShouldFindAllGamesById(expectedGames, game => game.Id); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_FindAllInterfaceGames(InMemoryFileSystem fs, AbsolutePath appFolder) - { - var handler = SetupHandler(fs, appFolder); - var expectedGames = SetupGames(fs, appFolder); - handler.ShouldFindAllInterfacesGames(expectedGames); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GamingRoot.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GamingRoot.cs deleted file mode 100644 index 65d7bea1..00000000 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GamingRoot.cs +++ /dev/null @@ -1,45 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Xbox.Tests; - -public partial class XboxTests -{ - [Theory, AutoFileSystem] - public void Test_ParseGamingRootFile1(InMemoryFileSystem fs, AbsolutePath gamingRootFile, string[] folderNames) - { - var expectedFolders = folderNames - .Select(folderName => gamingRootFile.Parent.Combine(folderName)) - .ToList(); - - var bytes = CreateGamingRootFile(expectedFolders); - fs.AddFile(gamingRootFile, bytes); - - var results = XboxHandler.ParseGamingRootFile(fs, gamingRootFile); - results.IsT0.Should().BeTrue(); - results.IsT1.Should().BeFalse(); - - var actualFolders = results.AsT0; - actualFolders!.Should().BeEquivalentTo(expectedFolders); - } - - [Theory, AutoFileSystem] - public void Test_ParseGamingRootFile2(InMemoryFileSystem fs, AbsolutePath gamingRootFilePath) - { - var bytes = new byte[] - { - 0x52, 0x47, 0x42, 0x58, 0x01, 0x00, 0x00, 0x00, 0x58, 0x00, 0x62, - 0x00, 0x6f, 0x00, 0x78, 0x00, 0x47, 0x00, 0x61, 0x00, 0x6d, 0x00, - 0x65, 0x00, 0x73, 0x00, 0x00, 0x00, - }; - - fs.AddFile(gamingRootFilePath, bytes); - - var results = XboxHandler.ParseGamingRootFile(fs, gamingRootFilePath); - results.IsT0.Should().BeTrue(); - results.IsT1.Should().BeFalse(); - - var actualFolders = results.AsT0; - actualFolders.Should().ContainSingle(x => x.FileName.Equals("XboxGames", StringComparison.Ordinal)); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GetAppFolders.cs b/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GetAppFolders.cs deleted file mode 100644 index 4ca3078f..00000000 --- a/tests/GameFinder.StoreHandlers.Xbox.Tests/Test_GetAppFolders.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.StoreHandlers.Xbox.Tests; - -public partial class XboxTests -{ - [Theory, AutoFileSystem] - public void Test_GetAppFolders(InMemoryFileSystem fs) - { - var expectedPaths = fs - .EnumerateRootDirectories() - .Select(rootDirectory => - { - var modifiableWindowsAppsPath = rootDirectory - .Combine("Program Files") - .Combine("ModifiableWindowsApps"); - fs.AddDirectory(modifiableWindowsAppsPath); - return modifiableWindowsAppsPath; - }) - .ToArray(); - - var (paths, errors) = XboxHandler.GetAppFolders(fs); - errors.Should().BeEmpty(); - paths.Should().BeEquivalentTo(expectedPaths); - } -} diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/files/GamingRoot b/tests/GameFinder.StoreHandlers.Xbox.Tests/files/GamingRoot new file mode 100644 index 00000000..f85889d4 Binary files /dev/null and b/tests/GameFinder.StoreHandlers.Xbox.Tests/files/GamingRoot differ diff --git a/tests/GameFinder.StoreHandlers.Xbox.Tests/files/appxmanifest.xml b/tests/GameFinder.StoreHandlers.Xbox.Tests/files/appxmanifest.xml new file mode 100644 index 00000000..97965648 --- /dev/null +++ b/tests/GameFinder.StoreHandlers.Xbox.Tests/files/appxmanifest.xml @@ -0,0 +1,37 @@ + + + + + Fallout: New Vegas Ultimate Edition (PC) + Bethesda Softworks + FalloutNV_storelogo.png + Fallout: New Vegas Ultimate Edition (PC) + disabled + disabled + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/GameFinder.Wine.Tests/ArrangeHelpers.cs b/tests/GameFinder.Wine.Tests/ArrangeHelpers.cs deleted file mode 100644 index 8b92e5f9..00000000 --- a/tests/GameFinder.Wine.Tests/ArrangeHelpers.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NexusMods.Paths; - -namespace GameFinder.Wine.Tests; - -public partial class WineTests -{ - private static (AbsolutePath prefixDirectory, DefaultWinePrefixManager prefixManager) SetupWinePrefix(InMemoryFileSystem fs) - { - var location = DefaultWinePrefixManager - .GetDefaultWinePrefixLocations(fs) - .First(); - - fs.AddDirectory(location); - return (location, new DefaultWinePrefixManager(fs)); - } - - private static (AbsolutePath prefixDirectory, DefaultWinePrefixManager prefixManager) SetupValidWinePrefix(InMemoryFileSystem fs, AbsolutePath location) - { - fs.AddDirectory(location); - fs.AddDirectory(location.Combine("drive_c")); - fs.AddEmptyFile(location.Combine("system.reg")); - fs.AddEmptyFile(location.Combine("user.reg")); - - return (location, new DefaultWinePrefixManager(fs)); - } -} diff --git a/tests/GameFinder.Wine.Tests/Bottles/ArrangeHelpers.cs b/tests/GameFinder.Wine.Tests/Bottles/ArrangeHelpers.cs deleted file mode 100644 index 0ffedabe..00000000 --- a/tests/GameFinder.Wine.Tests/Bottles/ArrangeHelpers.cs +++ /dev/null @@ -1,35 +0,0 @@ -using GameFinder.Wine.Bottles; -using NexusMods.Paths; - -namespace GameFinder.Wine.Tests.Bottles; - -public partial class BottlesTests -{ - private static (AbsolutePath prefixDirectory, BottlesWinePrefixManager prefixManager) CreateBottle - (InMemoryFileSystem fs, string bottleName) - { - var bottlesDirectory = fs - .GetKnownPath(KnownPath.LocalApplicationDataDirectory) - .Combine("bottles"); - - var prefixDirectory = bottlesDirectory.Combine("bottles").Combine(bottleName); - fs.AddDirectory(prefixDirectory); - - var prefixManager = new BottlesWinePrefixManager(fs); - return (prefixDirectory, prefixManager); - } - - private static (AbsolutePath prefixDirectory, BottlesWinePrefixManager prefixManager) SetupValidBottlesPrefix - (InMemoryFileSystem fs, string bottleName) - { - var (prefixDirectory, prefixManager) = CreateBottle(fs, bottleName); - - fs.AddDirectory(prefixDirectory); - fs.AddDirectory(prefixDirectory.Combine("drive_c")); - fs.AddEmptyFile(prefixDirectory.Combine("system.reg")); - fs.AddEmptyFile(prefixDirectory.Combine("user.reg")); - fs.AddEmptyFile(prefixDirectory.Combine("bottle.yml")); - - return (prefixDirectory, prefixManager); - } -} diff --git a/tests/GameFinder.Wine.Tests/Bottles/Test_BottlesWinePrefixProperties.cs b/tests/GameFinder.Wine.Tests/Bottles/Test_BottlesWinePrefixProperties.cs deleted file mode 100644 index 0d29b480..00000000 --- a/tests/GameFinder.Wine.Tests/Bottles/Test_BottlesWinePrefixProperties.cs +++ /dev/null @@ -1,18 +0,0 @@ -using GameFinder.Wine.Bottles; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests.Bottles; - -public partial class BottlesTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_GetBottlesConfigurationFile(AbsolutePath prefixDirectory) - { - var bottleWinePrefix = new BottlesWinePrefix - { - ConfigurationDirectory = prefixDirectory, - }; - bottleWinePrefix.GetBottlesConfigFile().Should().Be(prefixDirectory.Combine("bottle.yml")); - } -} diff --git a/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldError_InvalidPrefix.cs b/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldError_InvalidPrefix.cs deleted file mode 100644 index 71ffd620..00000000 --- a/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldError_InvalidPrefix.cs +++ /dev/null @@ -1,27 +0,0 @@ -using GameFinder.Common; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests.Bottles; - -public partial class BottlesTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingBottleConfigFile(InMemoryFileSystem fs, string bottleName) - { - var (prefixDirectory, prefixManager) = CreateBottle(fs, bottleName); - - fs.AddDirectory(prefixDirectory.Combine("drive_c")); - fs.AddEmptyFile(prefixDirectory.Combine("system.reg")); - fs.AddEmptyFile(prefixDirectory.Combine("user.reg")); - - var bottlesConfigFile = prefixDirectory.Combine("bottle.yml"); - - prefixManager.FindPrefixes().Should() - .ContainSingle(result => result.IsError()) - .Which - .AsError() - .Should() - .Be($"Bottles configuration file is missing at {bottlesConfigFile}"); - } -} diff --git a/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldWork_ValidPrefix.cs b/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldWork_ValidPrefix.cs deleted file mode 100644 index c077be78..00000000 --- a/tests/GameFinder.Wine.Tests/Bottles/Test_ShouldWork_ValidPrefix.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests.Bottles; - -public partial class BottlesTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_ValidPrefix(InMemoryFileSystem fs, string bottleName) - { - var (prefixDirectory, prefixManager) = SetupValidBottlesPrefix(fs, bottleName); - - prefixManager - .FindPrefixes() - .Should() - .ContainSingle(result => result.IsPrefix()) - .Which - .AsPrefix() - .ConfigurationDirectory - .Should() - .Be(prefixDirectory); - } -} diff --git a/tests/GameFinder.Wine.Tests/Test_CreateRegistry.cs b/tests/GameFinder.Wine.Tests/Test_CreateRegistry.cs deleted file mode 100644 index c4b0c62f..00000000 --- a/tests/GameFinder.Wine.Tests/Test_CreateRegistry.cs +++ /dev/null @@ -1,51 +0,0 @@ -using GameFinder.RegistryUtils; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests; - -public partial class WineTests -{ - [Theory, AutoFileSystem] - public void Test_CreateRegistry(InMemoryFileSystem fs, AbsolutePath configurationDirectory) - { - fs.AddDirectory(configurationDirectory); - var prefix = new WinePrefix { ConfigurationDirectory = configurationDirectory }; - - fs.AddFile(configurationDirectory.Combine("system.reg"), """ -WINE REGISTRY VERSION 2 -;; something - -[foo\\bar\\baz] 1235123 -# something -@="1" -"2"="3" - -[baz\\bar\\foo] 1234123 -"4"="5" -"""); - - var registry = prefix.CreateRegistry(fs); - using var baseKey = registry.OpenBaseKey(RegistryHive.LocalMachine); - using var subKey1 = baseKey.OpenSubKey("foo\\bar\\baz"); - subKey1.Should().NotBeNull(); - - subKey1!.GetValue(valueName: null).Should().Be("1"); - subKey1.GetValue("2").Should().Be("3"); - - using var subKey2 = baseKey.OpenSubKey("baz\\bar\\foo"); - subKey2.Should().NotBeNull(); - - subKey2!.GetValue("4").Should().Be("5"); - } - - [Theory, AutoFileSystem] - public void Test_CreateRegistry_NoFile(InMemoryFileSystem fs, AbsolutePath configurationDirectory) - { - var prefix = new WinePrefix { ConfigurationDirectory = configurationDirectory }; - prefix - .Invoking(x => x.CreateRegistry(fs)) - .Should() - .NotThrow(); - } -} diff --git a/tests/GameFinder.Wine.Tests/Test_ShouldError_InvalidPrefix.cs b/tests/GameFinder.Wine.Tests/Test_ShouldError_InvalidPrefix.cs deleted file mode 100644 index f8e42385..00000000 --- a/tests/GameFinder.Wine.Tests/Test_ShouldError_InvalidPrefix.cs +++ /dev/null @@ -1,59 +0,0 @@ -using GameFinder.Common; -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests; - -public partial class WineTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingVirtualDrive(InMemoryFileSystem fs) - { - var (prefixDirectory, prefixManager) = SetupWinePrefix(fs); - var virtualDriveDirectory = prefixDirectory.Combine("drive_c"); - - prefixManager.FindPrefixes().Should() - .ContainSingle(result => result.IsError()) - .Which - .AsError() - .Should() - .Be($"Virtual C: drive does not exist at {virtualDriveDirectory}"); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingSystemRegistryFile(InMemoryFileSystem fs) - { - var (prefixDirectory, prefixManager) = SetupWinePrefix(fs); - var virtualDriveDirectory = prefixDirectory.Combine("drive_c"); - fs.AddDirectory(virtualDriveDirectory); - - var systemRegistryFile = prefixDirectory.Combine("system.reg"); - - prefixManager.FindPrefixes().Should() - .ContainSingle(result => result.IsError()) - .Which - .AsError() - .Should() - .Be($"System registry file does not exist at {systemRegistryFile}"); - } - - [Theory, AutoFileSystem] - public void Test_ShouldError_MissingUserRegistryFile(InMemoryFileSystem fs) - { - var (prefixDirectory, prefixManager) = SetupWinePrefix(fs); - var virtualDriveDirectory = prefixDirectory.Combine("drive_c"); - fs.AddDirectory(virtualDriveDirectory); - - var systemRegistryFile = prefixDirectory.Combine("system.reg"); - fs.AddEmptyFile(systemRegistryFile); - - var userRegistryFile = prefixDirectory.Combine("user.reg"); - - prefixManager.FindPrefixes().Should() - .ContainSingle(result => result.IsError()) - .Which - .AsError() - .Should() - .Be($"User registry file does not exist at {userRegistryFile}"); - } -} diff --git a/tests/GameFinder.Wine.Tests/Test_ShouldWork_ValidPrefix.cs b/tests/GameFinder.Wine.Tests/Test_ShouldWork_ValidPrefix.cs deleted file mode 100644 index ac28f667..00000000 --- a/tests/GameFinder.Wine.Tests/Test_ShouldWork_ValidPrefix.cs +++ /dev/null @@ -1,25 +0,0 @@ - -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests; - -public partial class WineTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_ValidPrefix(InMemoryFileSystem fs) - { - var (prefixDirectory, prefixManager) = SetupValidWinePrefix(fs, DefaultWinePrefixManager - .GetDefaultWinePrefixLocations(fs) - .First()); - prefixManager - .FindPrefixes() - .Should() - .ContainSingle(result => result.IsPrefix()) - .Which - .AsPrefix() - .ConfigurationDirectory - .Should() - .Be(prefixDirectory); - } -} diff --git a/tests/GameFinder.Wine.Tests/Test_WinePrefixProperties.cs b/tests/GameFinder.Wine.Tests/Test_WinePrefixProperties.cs deleted file mode 100644 index 2ffb97b7..00000000 --- a/tests/GameFinder.Wine.Tests/Test_WinePrefixProperties.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NexusMods.Paths; -using NexusMods.Paths.TestingHelpers; - -namespace GameFinder.Wine.Tests; - -public partial class WineTests -{ - [Theory, AutoFileSystem] - public void Test_ShouldWork_GetVirtualDrivePath(AbsolutePath prefixDirectory) - { - var winePrefix = new WinePrefix { ConfigurationDirectory = prefixDirectory }; - winePrefix.GetVirtualDrivePath().Should().Be(prefixDirectory.Combine("drive_c")); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_GetSystemRegistryFile(AbsolutePath prefixDirectory) - { - var winePrefix = new WinePrefix { ConfigurationDirectory = prefixDirectory }; - winePrefix.GetSystemRegistryFile().Should().Be(prefixDirectory.Combine("system.reg")); - } - - [Theory, AutoFileSystem] - public void Test_ShouldWork_GetUserRegistryFile(AbsolutePath prefixDirectory) - { - var winePrefix = new WinePrefix { ConfigurationDirectory = prefixDirectory }; - winePrefix.GetUserRegistryFile().Should().Be(prefixDirectory.Combine("user.reg")); - } -} diff --git a/tests/TestUtils/AssertionHelpers.cs b/tests/TestUtils/AssertionHelpers.cs deleted file mode 100644 index 369b51b6..00000000 --- a/tests/TestUtils/AssertionHelpers.cs +++ /dev/null @@ -1,79 +0,0 @@ -using GameFinder.Common; -using OneOf; - -namespace TestUtils; - -public static class AssertionHelpers -{ - public static IEnumerable ShouldOnlyBeGames(this ICollection> results) - where TGame : class, IGame - { - results.Should().AllSatisfy(result => - { - result.IsGame().Should().BeTrue(result.IsError() ? result.AsError().Message : string.Empty); - result.IsError().Should().BeFalse(); - }); - - return results.Select(result => result.AsGame()); - } - - private static ErrorMessage ShouldOnlyBeOneError( - this ICollection> results) - where TGame : class, IGame - { - results.Should().ContainSingle(); - - var result = results.First(); - result.IsError().Should().BeTrue(); - result.IsGame().Should().BeFalse(); - - return result.AsError(); - } - - public static ErrorMessage ShouldOnlyBeOneError( - this AHandler handler) - where TGame : class, IGame - where TId : notnull - { - var results = handler.FindAllGames().ToArray(); - return results.ShouldOnlyBeOneError(); - } - - public static void ShouldFindAllGames( - this AHandler handler, - IEnumerable expectedGames) - where TGame : class, IGame - where TId : notnull - { - var results = handler.FindAllGames().ToArray(); - var games = results.ShouldOnlyBeGames(); - - games.Should().Equal(expectedGames); - } - - public static void ShouldFindAllGamesById( - this AHandler handler, - ICollection expectedGames, - Func keySelector) - where TGame : class, IGame - where TId : notnull - { - var results = handler.FindAllGamesById(out var errors); - errors.Should().BeEmpty(); - - results.Should().ContainKeys(expectedGames.Select(keySelector)); - results.Should().ContainValues(expectedGames); - } - - public static void ShouldFindAllInterfacesGames( - this AHandler handler, - IEnumerable expectedGames) - where TGame : class, IGame - where TId : notnull - { - var results = handler.FindAllInterfaceGames().ToArray(); - var games = results.ShouldOnlyBeGames(); - - games.Should().AllBeOfType().Which.Should().Equal(expectedGames); - } -} diff --git a/tests/TestUtils/StringExtensions.cs b/tests/TestUtils/StringExtensions.cs deleted file mode 100644 index aafb83f3..00000000 --- a/tests/TestUtils/StringExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text; - -namespace TestUtils; - -public static class StringExtensions -{ - public static string ToEscapedString(this string s) - { - var sb = new StringBuilder(s); - - sb.Replace("\\", "\\\\"); - - return sb.ToString(); - } -} diff --git a/tests/TestUtils/TestWrapper.cs b/tests/TestUtils/TestWrapper.cs new file mode 100644 index 00000000..6ab3f6df --- /dev/null +++ b/tests/TestUtils/TestWrapper.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; +using NexusMods.Paths; +using Xunit.Abstractions; + +namespace TestUtils; + +public class TestWrapper +{ + protected readonly ILogger Logger; + protected readonly IFileSystem FileSystem; + + protected TestWrapper(ITestOutputHelper output) + { + Logger = new XunitLogger(output); + FileSystem = NexusMods.Paths.FileSystem.Shared; + } + + protected AbsolutePath GetTestFile(RelativePath fileName) + { + return FileSystem.GetKnownPath(KnownPath.EntryDirectory).Combine("files").Combine(fileName); + } +} diff --git a/tests/TestUtils/XunitLogger.cs b/tests/TestUtils/XunitLogger.cs new file mode 100644 index 00000000..21e3484d --- /dev/null +++ b/tests/TestUtils/XunitLogger.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace TestUtils; + +public class XunitLogger : ILogger +{ + private readonly ITestOutputHelper _output; + public XunitLogger(ITestOutputHelper output) + { + _output = output; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + var date = DateTimeOffset.Now; + _output.WriteLine($"{date.ToString("T", CultureInfo.InvariantCulture)}|{logLevel}|{formatter(state, exception)}"); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) where TState : notnull => EmptyDisposable.Instance; + + private sealed class EmptyDisposable : IDisposable + { + public static readonly IDisposable Instance = new EmptyDisposable(); + private EmptyDisposable() { } + public void Dispose() { } + } +}