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
+
+ ///