From 837e0a78151b79850dbaa1c3f9b56fc1cb8a00c3 Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Sat, 21 Sep 2024 02:33:53 -0700 Subject: [PATCH 1/5] Add experimental handler for Windows Package Manager (winget) --- CHANGELOG.md | 6 + Directory.Packages.props | 13 +- GameCollector.sln | 10 + README.md | 11 +- codecov.yml | 4 + .../GameFinder.Example.csproj | 4 + other/GameFinder.Example/Options.cs | 3 + other/GameFinder.Example/Program.cs | 18 +- src/Directory.Build.props | 2 +- .../GameCollector.PkgHandlers.Winget.csproj | 19 + .../WingetGame.cs | 83 ++ .../WingetGameId.cs | 50 ++ .../WingetHandler.cs | 774 ++++++++++++++++++ src/GameFinder.Common/Handler.cs | 5 + src/GameFinder/GameFinder.csproj | 1 + 15 files changed, 994 insertions(+), 9 deletions(-) create mode 100644 src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj create mode 100644 src/GameCollector.PkgHandlers.Winget/WingetGame.cs create mode 100644 src/GameCollector.PkgHandlers.Winget/WingetGameId.cs create mode 100644 src/GameCollector.PkgHandlers.Winget/WingetHandler.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a25c13..90f4c560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ Though upstream [GameFinder](https://github.com/erri120/GameFinder) adheres to [ - Add [DataHandlers](https://github.com/Nutzzz/GameCollector/pull/13) to download metadata and images from online sources +## [4.2.4-2](https://github.com/Nutzzz/GameCollector/compare/v4.2.4.1...v4.2.4.2) - UNRELEASED + +### Added + +- Add Winget package handler to detect both installed apps and show free game packages + ## [Released](https://github.com/Nutzzz/GameCollector/releases) ## [4.2.4-1](https://github.com/Nutzzz/GameCollector/compare/v4.2.3.2...v4.2.4.1) - 2024-07-08 diff --git a/Directory.Packages.props b/Directory.Packages.props index 52373ea6..94c5b638 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,16 +15,25 @@ - + + + + + + + + + + - + diff --git a/GameCollector.sln b/GameCollector.sln index c3ec654c..4a86d154 100644 --- a/GameCollector.sln +++ b/GameCollector.sln @@ -134,6 +134,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCollector.EmuHandlers.D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCollector.EmuHandlers.MAME", "src\GameCollector.EmuHandlers.MAME\GameCollector.EmuHandlers.MAME.csproj", "{EA0A79A3-639B-4983-A28F-1D07EC1113C7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PkgHandlers", "PkgHandlers", "{55345886-D1F6-4733-9943-1283AF39CEA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCollector.PkgHandlers.Winget", "src\GameCollector.PkgHandlers.Winget\GameCollector.PkgHandlers.Winget.csproj", "{7C74842C-69FE-4908-BA71-210652A15744}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataHandlers", "DataHandlers", "{82816E34-196A-4304-8A28-BF22FA937DF0}" EndProject Global @@ -318,6 +322,10 @@ Global {EA0A79A3-639B-4983-A28F-1D07EC1113C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA0A79A3-639B-4983-A28F-1D07EC1113C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA0A79A3-639B-4983-A28F-1D07EC1113C7}.Release|Any CPU.Build.0 = Release|Any CPU + {7C74842C-69FE-4908-BA71-210652A15744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C74842C-69FE-4908-BA71-210652A15744}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C74842C-69FE-4908-BA71-210652A15744}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C74842C-69FE-4908-BA71-210652A15744}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -371,6 +379,8 @@ Global {1F5F230D-146C-4794-A2E6-F9C13279D721} = {EFB4E7DB-DE24-4BDE-B660-DAF1394ECC21} {6173D1F4-48C2-4B14-B907-C7A746CF081D} = {1F5F230D-146C-4794-A2E6-F9C13279D721} {EA0A79A3-639B-4983-A28F-1D07EC1113C7} = {1F5F230D-146C-4794-A2E6-F9C13279D721} + {55345886-D1F6-4733-9943-1283AF39CEA5} = {EFB4E7DB-DE24-4BDE-B660-DAF1394ECC21} + {7C74842C-69FE-4908-BA71-210652A15744} = {55345886-D1F6-4733-9943-1283AF39CEA5} {82816E34-196A-4304-8A28-BF22FA937DF0} = {EFB4E7DB-DE24-4BDE-B660-DAF1394ECC21} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/README.md b/README.md index ea77af59..3dd28709 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ .NET library for finding games. GameCollector expands on upstream GameFinder (which is primarily designed to support modding tools), by adding additional supported store launchers, emulators, and data sources, and includes additional information about each game (sufficient for a multi-store game launcher such as [GLC](https://github.com/Solaire/GLC)). This fork is indebted to erri120's hard work, but as [he is reticent](https://github.com/erri120/GameFinder/issues/49) to expand the scope of GameFinder, GameCollector continues with a different philosophy. -The following launchers and emulators are supported: +The following launchers, emulators, and package managers are supported: | handler | package | | -- | -- | @@ -35,6 +35,7 @@ The following launchers and emulators are supported: | [Steam](#steam) | [![Nuget](https://img.shields.io/nuget/v/GameCollector.StoreHandlers.Steam)](https://www.nuget.org/packages/GameCollector.StoreHandlers.Steam) [![Nuget](https://img.shields.io/nuget/v/GameFinder.StoreHandlers.Steam?color=red&label=upstream)](https://www.nuget.org/packages/GameFinder.StoreHandlers.Steam) | | Ubisoft Connect | [![Nuget](https://img.shields.io/nuget/v/GameCollector.StoreHandlers.Ubisoft)](https://www.nuget.org/packages/GameCollector.StoreHandlers.Ubisoft) | | Wargaming.net Game Center | [![Nuget](https://img.shields.io/nuget/v/GameCollector.StoreHandlers.WargamingNet)](https://www.nuget.org/packages/GameCollector.StoreHandlers.WargamingNet) | +| Windows Package Manager | [![Nuget](https://img.shields.io/nuget/v/GameCollector.PkgHandlers.Winget)](https://www.nuget.org/packages/GameCollector.PkgHandlers.Winget) | | [Xbox Game Pass](#xbox-game-pass) | [![Nuget](https://img.shields.io/nuget/v/GameCollector.StoreHandlers.Xbox)](https://www.nuget.org/packages/GameCollector.StoreHandlers.Xbox) [![Nuget](https://img.shields.io/nuget/v/GameFinder.StoreHandlers.Xbox?color=red&label=upstream)](https://www.nuget.org/packages/GameFinder.StoreHandlers.Xbox) | If you are interested in understanding _how_ GameCollector/GameFinder finds these games, check [the upstream wiki](https://github.com/erri120/GameFinder/wiki) for more information (descriptions of the added handlers should eventually be added to a wiki here). @@ -97,6 +98,12 @@ The TGame implementations of GameCollector's handlers inherit a generic GameData The Metadata dictionary may include (depending on available information): "ReleaseDate", "Description", "Developers", "Publishers", "Genres", "ImageUrl", etc. +### Supported Package Managers + +This is a new category of handler for GameCollector. It is Windows-only for now. + +- Winget + ### Supported Emulators This is a new category of handler for GameCollector. They are Windows-only for now. @@ -417,7 +424,7 @@ Self-contained deployments and executables can be [trimmed](https://learn.micros **NOT Trimmable**: -- `GameCollector.StoreHandlers.EADesktop`: This package references `System.Management`, which is **not trimmable** due to COM interop issues. See [dotnet/runtime#78038](https://github.com/dotnet/runtime/issues/78038), [dotnet/runtime#75176](https://github.com/dotnet/runtime/pull/75176) and [dotnet/runtime#61960](https://github.com/dotnet/runtime/issues/61960) for more details. +- `GameCollector.StoreHandlers.EADesktop` and `GameCollector.PkgHandlers.Winget`: These packages reference `System.Management`, which is **not trimmable** due to COM interop issues. See [dotnet/runtime#78038](https://github.com/dotnet/runtime/issues/78038), [dotnet/runtime#75176](https://github.com/dotnet/runtime/pull/75176) and [dotnet/runtime#61960](https://github.com/dotnet/runtime/issues/61960) for more details. I recommend looking at the [project file](./other/GameFinder.Example/GameFinder.Example.csproj) of the example project, if you run into warnings or errors with trimming. diff --git a/codecov.yml b/codecov.yml index 95a801a0..db5c0e8a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -132,6 +132,10 @@ component_management: name: Wargaming.net Game Center paths: - "src/GameCollector.StoreHandlers.WargamingNet/**" + - component_id: pkg_handler_winget + name: Windows Package Manager + paths: + - "src/GameCollector.PkgHandlers.Winget/**" - component_id: emu_handler_dolphin name: Dolphin Emulator paths: diff --git a/other/GameFinder.Example/GameFinder.Example.csproj b/other/GameFinder.Example/GameFinder.Example.csproj index fbb55ba2..b24db6ab 100644 --- a/other/GameFinder.Example/GameFinder.Example.csproj +++ b/other/GameFinder.Example/GameFinder.Example.csproj @@ -51,6 +51,10 @@ + + + + diff --git a/other/GameFinder.Example/Options.cs b/other/GameFinder.Example/Options.cs index 0d9cdc45..773f3d05 100644 --- a/other/GameFinder.Example/Options.cs +++ b/other/GameFinder.Example/Options.cs @@ -112,6 +112,9 @@ public class Options [Option("wargaming", HelpText = "Search for Wargaming.Net Game Center games", Hidden = true, Group = "Handlers")] public bool Wargaming { get; set; } = false; + [Option("winget", HelpText = "Search for Windows Package Manager packages", Hidden = true, Group = "Handlers")] + public bool Winget { get; set; } = false; + [Option("xbox", HelpText = "Search for Xbox Games Pass games", Hidden = true, Group = "Handlers")] public bool Xbox { get; set; } = false; diff --git a/other/GameFinder.Example/Program.cs b/other/GameFinder.Example/Program.cs index 7ed8f6ee..945dc969 100644 --- a/other/GameFinder.Example/Program.cs +++ b/other/GameFinder.Example/Program.cs @@ -36,6 +36,7 @@ using GameCollector.StoreHandlers.Rockstar; using GameCollector.StoreHandlers.Ubisoft; using GameCollector.StoreHandlers.WargamingNet; +using GameCollector.PkgHandlers.Winget; using GameCollector.EmuHandlers.Dolphin; using GameCollector.EmuHandlers.MAME; //using GameCollector.DataHandlers.TheGamesDb; @@ -114,13 +115,13 @@ private async static void Run(Options options, ILogger logger) var settings = new Settings(options.Installed, options.Parent || options.Base, options.Owned, options.Games); - if (options.All) // Enable all emulator and handlers + if (options.All) // Enable all handlers { options.Amazon = true; options.Arc = true; options.BattleNet = true; options.BigFish = true; - //options.Dolphin ??= ""; + //options.Dolphin ??= ""; // needs path options.EA = true; options.Epic = true; options.GameJolt = true; @@ -129,7 +130,7 @@ private async static void Run(Options options, ILogger logger) options.IG = true; options.Itch = true; options.Legacy = true; - //options.MAME ??= ""; + //options.MAME ??= ""; // needs path options.Oculus = true; options.Origin = true; options.Paradox = true; @@ -138,9 +139,10 @@ private async static void Run(Options options, ILogger logger) options.RobotCache = true; options.Rockstar = true; options.Steam = true; - //options.TheGamesDB = true; + //options.TheGamesDB = true; // WIP options.Ubisoft = true; options.Wargaming = true; + options.Winget = true; options.Xbox = true; } @@ -185,6 +187,8 @@ private async static void Run(Options options, ILogger logger) if (options.Ubisoft || options.Uplay) tasks.Add(Task.Run(() => RunUbisoftHandler(settings, windowsRegistry, realFileSystem), cancelToken)); if (options.Wargaming || options.WargamingNet) tasks.Add(Task.Run(() => RunWargamingNetHandler(settings, windowsRegistry, realFileSystem), cancelToken)); + if (options.Winget) tasks.Add(Task.Run(() => RunWingetHandler(settings, windowsRegistry, realFileSystem), cancelToken)); + if (options.Dolphin is not null) { tasks.Add(Task.Run(() => @@ -445,6 +449,12 @@ private static void RunWargamingNetHandler(Settings settings, IRegistry registry var handler = new WargamingNetHandler(registry, fileSystem); LogGamesAndErrors(handler.FindAllGames(settings), logger); } + private static void RunWingetHandler(Settings settings, IRegistry registry, IFileSystem fileSystem) + { + var logger = _provider.CreateLogger(nameof(WingetHandler)); + var handler = new WingetHandler(registry, fileSystem); + LogGamesAndErrors(handler.FindAllGames(settings), logger); + } private static void RunDolphinHandler(Settings settings, IRegistry registry, IFileSystem fileSystem, AbsolutePath path) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af241bef..dea2dd2d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - 4.2.4.1 + 4.2.4.2 Nutzzz, erri120 docs\README.md diff --git a/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj b/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj new file mode 100644 index 00000000..68e27a54 --- /dev/null +++ b/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj @@ -0,0 +1,19 @@ + + + Library for finding installed apps and available packages via Windows Package Manager. + winget windows-package-manager games + + + + + + + + + + + + + + + diff --git a/src/GameCollector.PkgHandlers.Winget/WingetGame.cs b/src/GameCollector.PkgHandlers.Winget/WingetGame.cs new file mode 100644 index 00000000..95356f66 --- /dev/null +++ b/src/GameCollector.PkgHandlers.Winget/WingetGame.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using GameFinder.Common; +using JetBrains.Annotations; +using NexusMods.Paths; + +namespace GameCollector.PkgHandlers.Winget; + +/// +/// Represents an installed app or available package via Windows Package Manager. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// + +[PublicAPI] +public record WingetGame(WingetGameId Id, + string? Name, + AbsolutePath InstallDirectory, + AbsolutePath Launch = new(), + string? InstallerUrl = "", + AbsolutePath Uninstall = new(), + bool IsInstalled = true, + DateTime? InstallDate = null, + string? Description = "", + string? Publisher = "", + List? PackageTags = default, + string? PublisherUrl = "", + string? SupportUrl = "", + string? Homepage = "", + string? Author = "", + string? PackageName = "", + string? Moniker = "", + string? Source = "", + string? LicenseType = "", + string? InstalledVersion = "", + string? DefaultVersion = "", + string? AgeRating = "") : + GameData(Handler: Handler.PkgHandler_Winget, + GameId: Id.ToString(), + GameName: Name ?? "", + GamePath: InstallDirectory, + Launch: Launch, + LaunchUrl: InstallerUrl ?? "", + Icon: Launch, + Uninstall: Uninstall, + IsInstalled: IsInstalled, + InstallDate: InstallDate, + Metadata: new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["Description"] = new() { Description ?? "", }, + ["Developers"] = new() { Author ?? "", }, + ["Publishers"] = new() { Publisher ?? "", }, + ["Genres"] = PackageTags ?? new(), + ["WebInfo"] = new() { Homepage ?? PublisherUrl ?? "" }, + ["WebSupport"] = new() { SupportUrl ?? "" }, + ["PackageName"] = new() { PackageName ?? "" }, + ["Moniker"] = new() { Moniker ?? "", }, + ["Source"] = new() { Source ?? "", }, + ["License"] = new() { LicenseType ?? "", }, + ["InstalledVersion"] = new() { InstalledVersion ?? "", }, + ["DefaultVersion"] = new() { DefaultVersion ?? "", }, + ["AgeRating"] = new() { AgeRating ?? "", }, + }); diff --git a/src/GameCollector.PkgHandlers.Winget/WingetGameId.cs b/src/GameCollector.PkgHandlers.Winget/WingetGameId.cs new file mode 100644 index 00000000..315f8258 --- /dev/null +++ b/src/GameCollector.PkgHandlers.Winget/WingetGameId.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using TransparentValueObjects; + +namespace GameCollector.PkgHandlers.Winget; + +/// +/// Represents an id for installed apps and available packages via Windows Package Manager. +/// +[ValueObject] +public readonly partial struct WingetGameId : IAugmentWith +{ + /// + public static IEqualityComparer InnerValueDefaultEqualityComparer { get; } = StringComparer.OrdinalIgnoreCase; +} + +/// +[PublicAPI] +public class WingetGameIdComparer : IEqualityComparer +{ + private static WingetGameIdComparer? _default; + + /// + /// Default equality comparer that uses . + /// + public static WingetGameIdComparer Default => _default ??= new(); + + private readonly StringComparison _stringComparison; + + /// + /// Default constructor that uses . + /// + public WingetGameIdComparer() : this(StringComparison.OrdinalIgnoreCase) { } + + /// + /// Constructor. + /// + /// + public WingetGameIdComparer(StringComparison stringComparison) + { + _stringComparison = stringComparison; + } + + /// + public bool Equals(WingetGameId x, WingetGameId y) => string.Equals(x.Value, y.Value, _stringComparison); + + /// + public int GetHashCode(WingetGameId obj) => obj.Value.GetHashCode(_stringComparison); +} diff --git a/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs b/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs new file mode 100644 index 00000000..6f01a01a --- /dev/null +++ b/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs @@ -0,0 +1,774 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using GameFinder.Common; +using GameFinder.RegistryUtils; +using JetBrains.Annotations; +using NexusMods.Paths; +using OneOf; + +namespace GameCollector.PkgHandlers.Winget; + +/// +/// Handler for finding installed apps via Windows Package Manager. +/// +/// +/// Leverages winget, which inspects registry and WindowsApps (Microsoft Store) for installed programs. +/// +[PublicAPI] +public class WingetHandler : AHandler +{ + internal const string TagQuery = "game"; + internal const string UninstallRegKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + + private readonly IRegistry _registry; + private readonly IFileSystem _fileSystem; + + //private WindowsPackageManagerFactory? _winGetFactory = null; + + private ProcessStartInfo _startInfo = new() + { + UseShellExecute = false, + RedirectStandardOutput = true, + StandardOutputEncoding = Encoding.UTF8, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + }; + + /// + /// Constructor. + /// + /// + /// 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. + /// + /// + /// The implementation of to use. For a shared instance use + /// . For tests either use , + /// a custom implementation or just a mock of the interface. + /// + public WingetHandler(IRegistry registry, IFileSystem fileSystem) + { + _registry = registry; + _fileSystem = fileSystem; + + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) + { + // If the user is an administrator, use the elevated factory. Otherwhise COM will crash + /* + if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) + _winGetFactory = new WindowsPackageManagerElevatedFactory(); + else + _winGetFactory = new WindowsPackageManagerStandardFactory(); + */ + } + } + + /// + public override IEqualityComparer? IdEqualityComparer => WingetGameIdComparer.Default; + + /// + public override Func IdSelector => game => game.Id; + + /// + public override AbsolutePath FindClient() + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var wingetExe = _fileSystem.FromUnsanitizedFullPath(Path.Combine(localAppData, "Microsoft", "WindowsApps", "winget.exe")); + if (Path.IsPathRooted(localAppData) && _fileSystem.FileExists(wingetExe)) + return wingetExe; + + return default; + } + + /// + public override IEnumerable> FindAllGames(Settings? settings = null) + { + return FindAllGames(settings?.InstalledOnly ?? false); + } + + /// + /// Finds all apps supported by this package handler. The return type + /// will always be a non-null game or a non-null error message. + /// + /// + /// Get additional information about winget/msstore packages + /// + public IEnumerable> FindAllGames(bool? installedOnly = false, bool? expandPackage = false) + { + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) + { + yield return new ErrorMessage("Only supported on Windows 10.0.19041.0 or later."); + yield break; + } + /* + if (_winGetFactory is null) + { + yield return new ErrorMessage("Could not access Windows Package Manager."); + yield break; + } + */ + + Dictionary> games = new(); + + // Create Package Manager and get available catalogs + //var manager = _winGetFactory.CreatePackageManager(); + //var availableCatalogs = manager.GetPackageCatalogs(); + + /* + foreach (var catalog in availableCatalogs.ToArray()) + { + // Create a filter to search for packages with "game" tag + //var filterList = _winGetFactory.CreateFindPackagesOptions(); + + // Add the query to the filter + var tagFilter = _winGetFactory.CreatePackageMatchFilter(); + tagFilter.Field = PackageMatchField.Tag; + tagFilter.Value = TagQuery; + filterList.Filters.Add(tagFilter); + + // Find the packages with the filters + //var searchResults = await catalog.Connect().PackageCatalog.FindPackagesAsync(filterList); + var searchResults = catalog.Connect().PackageCatalog.FindPackages(filterList); + foreach (var match in searchResults.Matches.ToArray()) + { + var pkg = match.CatalogPackage; + var id = WingetGameId.From(pkg.Id); + + games.Add(id, new WingetGame( + Id: id, + Name: pkg.Name, + InstallDirectory: default, + //PkgTags: pkg.Tags, + //Source: pkg.Source, + InstalledVersion: pkg.InstalledVersion.DisplayName, + DefaultVersion: pkg.DefaultInstallVersion.DisplayName + )); + } + } + */ + + // Get all installed items + //var installedCatalogs = manager.GetLocalPackageCatalog; + + foreach (var item in GetInstalled(expandPackage)) + { + if (item.IsT0) + { + if (!games.ContainsKey(item.AsT0.Id)) + continue; + + yield return new WingetGame( + Id: item.AsT0.Id, + Name: item.AsT0.Name, + InstallDirectory: default, + IsInstalled: true, + //PkgTags: item.CatalogPackage.Tags, + //Source: item.CatalogPackage.Source, + InstalledVersion: item.AsT0.InstalledVersion, + DefaultVersion: item.AsT0.DefaultVersion + ); + } + else yield return item.AsT1; + } + + //var freeGames = SearchFreeGames(); + + } + + private IEnumerable> GetInstalled() + { + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) + { + yield return new ErrorMessage("Only supported on Windows 10.0.19041.0 or later."); + yield break; + } + /* + if (_winGetFactory is null) + { + yield return new ErrorMessage("Could not access Windows Package Manager."); + yield break; + } + */ + + // Fetching installed packages + //var winGetManager = _winGetFactory.CreatePackageManager(); + + // CHANGE THIS INDEX TO CHANGE THE SOURCE + var selectedIndex = 0; // does this refer to winget vs. msstore? + + /* + PackageCatalogReference installedSearchCatalogRef; + if (selectedIndex < 0) + { + installedSearchCatalogRef = winGetManager.GetLocalPackageCatalog(LocalPackageCatalog.InstalledPackages); + } + else + { + var selectedRemoteCatalogRef = winGetManager.GetPackageCatalogs().ToArray()[selectedIndex]; + + //Console.WriteLine($"Searching on package catalog {selectedRemoteCatalogRef.Info.Name} "); + + var createCompositePackageCatalogOptions = _winGetFactory.CreateCreateCompositePackageCatalogOptions(); + createCompositePackageCatalogOptions.Catalogs.Add(selectedRemoteCatalogRef); + createCompositePackageCatalogOptions.CompositeSearchBehavior = CompositeSearchBehavior.LocalCatalogs; + installedSearchCatalogRef = winGetManager.CreateCompositePackageCatalog(createCompositePackageCatalogOptions); + } + + var connectResult = installedSearchCatalogRef.Connect(); + if (connectResult.Status != ConnectResultStatus.Ok) + { + yield return new ErrorMessage("Failed to connect to local catalog."); + yield break; + } + + var findPackagesOptions = _winGetFactory.CreateFindPackagesOptions(); + var filter = _winGetFactory.CreatePackageMatchFilter(); + filter.Field = PackageMatchField.Id; + filter.Option = PackageFieldMatchOption.ContainsCaseInsensitive; + filter.Value = ""; + findPackagesOptions.Filters.Add(filter); + + var taskResult = connectResult.PackageCatalog.FindPackages(findPackagesOptions); + */ + + // Begin enumeration + /* + foreach (var match in taskResult.Matches.ToArray()) + { + var pkg = match.CatalogPackage; + //Console.WriteLine($"Package {pkg.Name} is available Online: " + pkg.DefaultInstallVersion.PackageCatalog.Info.Name); + yield return new WingetGame( + Id: WingetGameId.From(pkg.Id), + Name: pkg.Name, + InstallDirectory: default, + IsInstalled: pkg.DefaultInstallVersion != null, + //PackageTags: pkg.Tags???, + Source: pkg.DefaultInstallVersion?.Channel, // is this winget vs. msstore? + PackageName: pkg.DefaultInstallVersion?.PackageCatalog.Info.Name, + ProductCodes: pkg.DefaultInstallVersion?.ProductCodes.ToList(), + Publisher: pkg.InstalledVersion.Publisher, + InstalledVersion: pkg.InstalledVersion.DisplayName, + DefaultVersion: pkg.DefaultInstallVersion?.DisplayName); + } + */ + + // End enumeration + } + + private IEnumerable> GetInstalled(bool? expandPackage) + { + //************************************* + Console.OutputEncoding = Encoding.UTF8; + //************************************* + + var wingetExe = FindClient(); + if (wingetExe == default) + { + yield return new ErrorMessage("Winget not installed"); + yield break; + } + + using var process = new Process(); + process.StartInfo = _startInfo; + process.StartInfo.FileName = wingetExe.GetFullPath(); + process.StartInfo.Arguments = "list --nowarn --disable-interactivity"; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (string.IsNullOrEmpty(output)) + { + yield return new ErrorMessage("No output from winget"); + yield break; + } + + List colPos = new(); + var i = 0; + foreach (var line in output.Split('\n')) + { + if (i == 0) + { + if (!line.Contains("Name ", StringComparison.Ordinal)) + continue; + + var headLine = line[line.IndexOf("Name ", StringComparison.Ordinal)..]; + foreach (var col in headLine.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + var c = headLine.IndexOf(col, StringComparison.Ordinal); + if (c > -1) + colPos.Add(c); + } + if (colPos.Count < 5) + break; + } + else if (i > 1 && !string.IsNullOrWhiteSpace(line)) // skip separator line + { + var name = line[colPos[0]..colPos[1]].Trim(); + var strId = line[colPos[1]..colPos[2]].Trim(); + var id = WingetGameId.From(strId); + var version = line[colPos[2]..colPos[3]].Trim(); + var available = line[colPos[3]..colPos[4]].Trim(); + var source = line[colPos[4]..].Trim(); + + if (string.IsNullOrWhiteSpace(source)) // non-package install + { + if (expandPackage != true) + { + Console.WriteLine(" " + name); + yield return new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + IsInstalled: true, + InstalledVersion: version); + i++; + continue; + } + + if (name.EndsWith('…') || strId.EndsWith('…') || version.EndsWith('…')) + { + (name, strId, version) = Relist(name, strId, version); + id = WingetGameId.From(strId); + } + + if (!string.IsNullOrEmpty(strId) && !strId.EndsWith('…')) + { + var game = ParseRegistry(strId); + if (game.IsT0) + { + Console.WriteLine("@ " + name); + yield return new WingetGame( + Id: id, + Name: name, + InstallDirectory: game.AsT0.InstallDirectory, + Launch: game.AsT0.Launch, + Uninstall: game.AsT0.Uninstall, + IsInstalled: true, + InstallDate: game.AsT0.InstallDate, + Publisher: game.AsT0.Publisher, + SupportUrl: game.AsT0.SupportUrl, + Homepage: game.AsT0.Homepage, + InstalledVersion: version); + i++; + continue; + } + + yield return game.AsT1; + } + + Console.WriteLine("? " + name); + yield return new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + IsInstalled: true, + InstalledVersion: version); + } + else // package install + { + if (expandPackage != true) + { + if (name.EndsWith('…') || strId.EndsWith('…') || version.EndsWith('…')) + { + (name, strId, version) = Relist(name, strId, version); + id = WingetGameId.From(strId); + } + Console.WriteLine(" " + name); + yield return new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + IsInstalled: true, + Source: source, + InstalledVersion: version, + DefaultVersion: available); + i++; + continue; + } + + if (strId.EndsWith('…')) + { + (name, strId, version) = Relist(name, strId, version); + id = WingetGameId.From(strId); + } + yield return GetPackageInfo(colPos, line, search: false); + } + } + i++; + } + } + + private (string, string, string) Relist(string name, string id, string version) + { + var subName = name; + if (name.EndsWith('…')) + subName = name[..^1]; + var subId = id; + if (id.EndsWith('…')) + subId = id[..^1]; + + var wingetExe = FindClient(); + + using var process = new Process(); + process.StartInfo = _startInfo; + process.StartInfo.FileName = wingetExe.GetFullPath(); + //process.StartInfo.Arguments = $"list --name \"{subName}\" --nowarn --disable-interactivity"; + process.StartInfo.Arguments = $"list --id \"{subId}\" --nowarn --disable-interactivity"; + process.Start(); + var listOutput = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + List listCols = new(); + var i = 0; + foreach (var listLine in listOutput.Split('\n')) + { + if (i == 0) + { + if (!listLine.Contains("Name", StringComparison.Ordinal)) + continue; + + var headLine = listLine[listLine.IndexOf("Name ", StringComparison.Ordinal)..]; + foreach (var col in headLine.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + var c = headLine.IndexOf(col, StringComparison.Ordinal); + if (c > -1) + listCols.Add(c); + } + if (listCols.Count < 3) + break; + } + else if (i > 1 && !string.IsNullOrWhiteSpace(listLine)) // skip separator line + { + name = listLine[listCols[0]..listCols[1]].Trim(); + id = listLine[listCols[1]..listCols[2]].Trim(); + version = listLine[listCols[2]..]; + } + i++; + } + + return (name, id, version); + } + + private Dictionary> SearchFreeGames(bool expandPackage = false) + { + Dictionary> freeGames = new(); + + var wingetExe = FindClient(); + + using var process = new Process(); + process.StartInfo = _startInfo; + process.StartInfo.FileName = wingetExe.GetFullPath(); + process.StartInfo.Arguments = "search --tag game --nowarn --disable-interactivity"; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (string.IsNullOrEmpty(output)) + { + return new() { [WingetGameId.From("")] = new ErrorMessage("No output from winget") }; + } + + List colPos = new(); + var i = 0; + foreach (var line in output.Split('\n')) + { + if (i == 0) + { + if (!line.Contains("Name ", StringComparison.Ordinal)) + continue; + + var headLine = line[line.IndexOf("Name ", StringComparison.Ordinal)..]; + foreach (var col in headLine.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + var c = headLine.IndexOf(col, StringComparison.Ordinal); + if (c > -1) + colPos.Add(c); + } + if (colPos.Count < 5) + break; + } + else if (i > 1 && !string.IsNullOrWhiteSpace(line)) // skip separator line + { + var name = line[colPos[0]..colPos[1]].Trim(); + var strId = line[colPos[1]..colPos[2]].Trim(); + var id = WingetGameId.From(strId); + var available = line[colPos[2]..colPos[3]].Trim(); + //var match = line[colPos[3]..colPos[4]].Trim(); + var source = line[colPos[4]..].Trim(); + + if (!expandPackage) + { + freeGames.Add(id, new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + IsInstalled: false, + DefaultVersion: available)); + i++; + continue; + } + + freeGames.Add(id, GetPackageInfo(colPos, line, search: true)); + } + i++; + } + + return freeGames; + } + + private OneOf ParseRegistry(string id) + { + // id syntax = "ARP\[Machine|User]\[X64|X86]\ + + var regKeyName = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"; + var baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine); + if (id.StartsWith(@"ARP\Machine\X64\", StringComparison.Ordinal)) + { + regKeyName += id[@"ARP\Machine\X64\".Length..]; + baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + } + else if (id.StartsWith(@"ARP\Machine\X86\", StringComparison.Ordinal)) + { + regKeyName += id[@"ARP\Machine\X86\".Length..]; + baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + } + else if (id.StartsWith(@"ARP\User\X64\", StringComparison.Ordinal)) + { + regKeyName += id[@"ARP\User\X64\".Length..]; + baseKey = _registry.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); + } + else if (id.StartsWith(@"ARP\User\X86\", StringComparison.Ordinal)) + { + regKeyName += id[@"ARP\User\X86\".Length..]; + baseKey = _registry.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32); + } + + try + { + using var regKey = baseKey.OpenSubKey(regKeyName); + if (regKey is null) + { + return new ErrorMessage($"Unable to open {regKeyName}"); + } + + if (!regKey.TryGetString("DisplayName", out var name)) + name = ""; + + if (!regKey.TryGetString("HelpLink", out var help)) + help = ""; + + DateTime installDate = default; + if (regKey.TryGetString("InstallDate", out var date)) + DateTime.TryParseExact(date, "yyyyMMdd", new CultureInfo("en-US"), DateTimeStyles.None, out installDate); + + if (!regKey.TryGetString("InstallLocation", out var path)) + path = ""; + + if (!regKey.TryGetString("DisplayIcon", out var launch)) + launch = ""; + + if (!regKey.TryGetString("Publisher", out var pub)) + pub = ""; + + if (!regKey.TryGetString("UninstallString", out var uninst)) + uninst = ""; + + if (!regKey.TryGetString("URLInfoAbout", out var url)) + url = ""; + + return new WingetGame( + Id: WingetGameId.From(id), + Name: name, + InstallDirectory: Path.IsPathRooted(path) ? _fileSystem.FromUnsanitizedFullPath(path) : new(), + InstallDate: installDate == default ? null : installDate, + Launch: Path.IsPathRooted(launch) ? _fileSystem.FromUnsanitizedFullPath(launch) : new(), + Uninstall: Path.IsPathRooted(uninst) ? _fileSystem.FromUnsanitizedFullPath(uninst) : new(), + Publisher: pub, + SupportUrl: help, + Homepage: url + ); + } + catch (Exception e) + { + return new ErrorMessage(e, $"Exception while parsing registry key {regKeyName}"); + } + } + + private OneOf GetPackageInfo(List colPos, string listInfo, bool search = false) + { + //list header = "Name,Id,Version,Available,Source" + //search header = "Name,Id,Version,Match,Source" + + var wingetExe = FindClient(); + + var isDescription = false; + //var isReleaseNotes = false; + var isTags = false; + + var name = listInfo[colPos[0]..colPos[1]].Trim(); + var strId = listInfo[colPos[1]..colPos[2]].Trim(); + var id = WingetGameId.From(strId); + var version = ""; + string? available; + //string? match; + if (search) + { + available = listInfo[colPos[2]..colPos[3]].Trim(); + //match = listInfo[colPos[3]..colPos[4]].Trim(); + } + else + { + version = listInfo[colPos[2]..colPos[3]].Trim(); + available = listInfo[colPos[3]..colPos[4]].Trim(); + } + var source = listInfo[colPos[4]..].Trim(); + var pkgName = ""; + var publisher = ""; + //var publisherUrl = ""; + var publisherSupportUrl = ""; + var author = ""; + var moniker = ""; + var description = ""; + var homepage = ""; + var license = ""; + //var licenseUrl = ""; + //var privacyUrl = ""; + //var copyright = ""; + //var releaseNotes = ""; + //var releaseNotesUrl = ""; + //var purchaseUrl = ""; + //var documentationTutorials = ""; + //var documentationManual = ""; + List tags = new(); + //var agreementsCategory = ""; + //var agreementsPricing = ""; + //string agreementsFreeTrial = ""; + var agreementsAgeRatings = ""; + //string agreementsTermsOfTransaction = ""; + //string agreementsSeizureWarning = ""; + //string agreementsStoreLicenseTerms = ""; + //var installerType = ""; + //var installerLocale = ""; + var installerUrl = ""; + //var installerSha256 = ""; + //var installerStoreProductId = ""; + //var installerReleaseDate = ""; + //var installerOfflineDistributionSupported = false; + + using var process = new Process(); + process.StartInfo = _startInfo; + process.StartInfo.FileName = wingetExe.GetFullPath(); + process.StartInfo.Arguments = $"show {strId} --nowarn --disable-interactivity"; + process.Start(); + var pkgOutput = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + var i = 0; + foreach (var pkgLine in pkgOutput.Split('\n')) + { + if (isDescription) + { + if (pkgLine.StartsWith(' ')) + { + description += pkgLine.TrimStart(); + continue; + } + isDescription = false; + } + /* + if (isReleaseNotes) + { + if (pkgLine.StartsWith(' ')) + { + releaseNotes += pkgLine.TrimStart(); + continue; + } + isReleaseNotes = false; + } + */ + if (isTags) + { + if (pkgLine.StartsWith(' ')) + { + tags.Add(pkgLine.TrimStart()); + continue; + } + isTags = false; + } + + if (i == 0) + { + if (pkgLine.Contains("Found ", StringComparison.Ordinal)) + { + var headLine = pkgLine[pkgLine.IndexOf("Found ", StringComparison.Ordinal)..]; + if (headLine.LastIndexOf('[') > 7) + { + strId = headLine[(headLine.LastIndexOf('[') + 1)..^1]; + id = WingetGameId.From(strId); + pkgName = headLine["Found ".Length..headLine.LastIndexOf('[')]; + } + else + pkgName = headLine["Found ".Length..]; + } + else + continue; + } + else if (pkgLine.StartsWith("Version: ", StringComparison.Ordinal)) + version = pkgLine["Version: ".Length..]; + else if (pkgLine.StartsWith("Publisher: ", StringComparison.Ordinal)) + publisher = pkgLine["Publisher: ".Length..]; + else if (pkgLine.StartsWith("Publisher Support Url: ", StringComparison.Ordinal)) + publisherSupportUrl = pkgLine["Publisher Support Url: ".Length..]; + else if (pkgLine.StartsWith("Moniker: ", StringComparison.Ordinal)) + moniker = pkgLine["Moniker: ".Length..]; + else if (pkgLine.StartsWith("Description:", StringComparison.Ordinal)) + { + description = pkgLine["Description:".Length..].TrimStart(); + isDescription = true; + } + else if (pkgLine.StartsWith("Homepage: ", StringComparison.Ordinal)) + homepage = pkgLine["Homepage: ".Length..]; + else if (pkgLine.StartsWith("License: ", StringComparison.Ordinal)) + license = pkgLine["License: ".Length..]; + //else if (pkgLine.StartsWith("Release Notes:", StringComparison.Ordinal)) + //{ + // releaseNotes = pkgLine["Release Notes:".Length..].TrimStart(); + // isReleaseNotes = true; + //} + else if (pkgLine.StartsWith(" Age Ratings: ", StringComparison.Ordinal)) + agreementsAgeRatings = pkgLine[" Age Ratings: ".Length..]; + else if (pkgLine.StartsWith("Tags:", StringComparison.Ordinal)) + isTags = true; + else if (pkgLine.StartsWith(" Installer Url: ", StringComparison.Ordinal)) + installerUrl = pkgLine[" Installer Url: ".Length..]; + i++; + } + + Console.WriteLine("> " + name); + return new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + InstallerUrl: installerUrl, + IsInstalled: true, + Description: description, + Publisher: publisher, + Author: author, + PackageTags: tags, + Homepage: homepage, + SupportUrl: publisherSupportUrl, + PackageName: pkgName, + Moniker: moniker, + Source: source, + LicenseType: license, + InstalledVersion: version, + DefaultVersion: available, + AgeRating: agreementsAgeRatings); + } +} diff --git a/src/GameFinder.Common/Handler.cs b/src/GameFinder.Common/Handler.cs index e53e8ad2..45bd95ec 100644 --- a/src/GameFinder.Common/Handler.cs +++ b/src/GameFinder.Common/Handler.cs @@ -139,6 +139,11 @@ public enum Handler [Description("GameCollector.EmuHandlers.MAME")] EmuHandler_MAME, /// + /// Windows Package Manager package handler + /// + [Description("GameCollector.PkgHandlers.Winget")] + PkgHandler_Winget, + /// /// TheGamesDb.net data handler /// [Description("GameCollector.DataHandlers.TheGamesDb")] diff --git a/src/GameFinder/GameFinder.csproj b/src/GameFinder/GameFinder.csproj index 10b9f037..eb7891e8 100644 --- a/src/GameFinder/GameFinder.csproj +++ b/src/GameFinder/GameFinder.csproj @@ -45,6 +45,7 @@ + From 581ebf887fdbaf2d1e76bee5281e702c6720910c Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Tue, 1 Oct 2024 12:05:50 -0700 Subject: [PATCH 2/5] v4.3.1-1 --- CHANGELOG.md | 12 +++++++++++- Directory.Build.props | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d7fec9..4bf7ac19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com). Though upstream [GameFinder](https://github.com/erri120/GameFinder) adheres to [Semantic Versioning](https://semver.org), for the moment this project simply uses GameFinder's version number and adds a new digit to the end. Where normally a fourth digit would be used for hotfixes or pre-release versions, because GameCollector is still in its infancy, breaking changes may occur between point releases. Because the scope of this project is so much larger than upstream, eventually a different versioning scheme will need to be used. -## [Unreleased](https://github.com/Nutzzz/GameCollector/compare/v4.2.3.1...HEAD) +## [Unreleased](https://github.com/Nutzzz/GameCollector/compare/v4.3.1.1...HEAD) **TODO**: @@ -20,6 +20,16 @@ Though upstream [GameFinder](https://github.com/erri120/GameFinder) adheres to [ ## [Released](https://github.com/Nutzzz/GameCollector/releases) +## [4.3.1-1](https://github.com/Nutzzz/GameCollector/compare/v4.2.4.1...v4.3.1.1) - 2024-10-01 + +### Added + +- Upstream: Adds support for Heroic + +### Changed + +- Upstream: Heroic: pass along the wine prefix + ## [4.2.4-1](https://github.com/Nutzzz/GameCollector/compare/v4.2.3.2...v4.2.4.1) - 2024-07-08 ### Added diff --git a/Directory.Build.props b/Directory.Build.props index 9c04d808..93d14f12 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net8.0 true - 4.2.4 + 4.3.1 Nutzzz, erri120 https://github.com/Nutzzz/GameCollector git diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af241bef..f71bfdcf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - 4.2.4.1 + 4.3.1.1 Nutzzz, erri120 docs\README.md From 926ca087820203a085616076f35afc2c7f8eb0ca Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Wed, 2 Oct 2024 09:03:39 -0700 Subject: [PATCH 3/5] v4.3.1-2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e662407..afdd6d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Though upstream [GameFinder](https://github.com/erri120/GameFinder) adheres to [ - Add [DataHandlers](https://github.com/Nutzzz/GameCollector/pull/13) to download metadata and images from online sources -## [4.2.4-2](https://github.com/Nutzzz/GameCollector/compare/v4.2.4.1...v4.2.4.2) - UNRELEASED +## [4.3.1-2](https://github.com/Nutzzz/GameCollector/compare/v4.3.1.1...v4.3.1.2) - UNRELEASED ### Added From 7dd6fcadd1c162edb049fd0cdad609827b16c58d Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Mon, 14 Oct 2024 12:22:38 -0700 Subject: [PATCH 4/5] Revert "v4.3.1-2" This reverts commit 926ca087820203a085616076f35afc2c7f8eb0ca. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afdd6d82..6e662407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Though upstream [GameFinder](https://github.com/erri120/GameFinder) adheres to [ - Add [DataHandlers](https://github.com/Nutzzz/GameCollector/pull/13) to download metadata and images from online sources -## [4.3.1-2](https://github.com/Nutzzz/GameCollector/compare/v4.3.1.1...v4.3.1.2) - UNRELEASED +## [4.2.4-2](https://github.com/Nutzzz/GameCollector/compare/v4.2.4.1...v4.2.4.2) - UNRELEASED ### Added From 5e0d51fc59f9d18c4606e05c0ad8da9ca472b8fc Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Fri, 18 Oct 2024 15:37:56 -0700 Subject: [PATCH 5/5] Some tweaks to winget experiment --- Directory.Packages.props | 14 +- other/GameFinder.Example/Program.cs | 2 +- src/Directory.Build.props | 2 +- .../GameCollector.PkgHandlers.Winget.csproj | 2 +- .../WingetGame.cs | 3 + .../WingetHandler.cs | 338 ++++++++++++------ 6 files changed, 242 insertions(+), 119 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 12176566..77dba488 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,9 +15,9 @@ - + - + @@ -27,13 +27,13 @@ - + - - - + + + - + diff --git a/other/GameFinder.Example/Program.cs b/other/GameFinder.Example/Program.cs index 51e1c2a2..ef052ca0 100644 --- a/other/GameFinder.Example/Program.cs +++ b/other/GameFinder.Example/Program.cs @@ -462,7 +462,7 @@ private static void RunWargamingNetHandler(Settings settings, IRegistry registry private static void RunWingetHandler(Settings settings, IRegistry registry, IFileSystem fileSystem) { var logger = _provider.CreateLogger(nameof(WingetHandler)); - var handler = new WingetHandler(registry, fileSystem); + var handler = new WingetHandler(registry, fileSystem, logger); //, logger); LogGamesAndErrors(handler.FindAllGames(settings), logger); } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 15998280..a3209bf4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - 4.3.2.1 + 4.3.2.2 Nutzzz, erri120 docs\README.md diff --git a/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj b/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj index 68e27a54..fe57ff16 100644 --- a/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj +++ b/src/GameCollector.PkgHandlers.Winget/GameCollector.PkgHandlers.Winget.csproj @@ -1,6 +1,6 @@ - Library for finding installed apps and available packages via Windows Package Manager. + Library for finding installed apps and available game packages via Windows Package Manager. winget windows-package-manager games diff --git a/src/GameCollector.PkgHandlers.Winget/WingetGame.cs b/src/GameCollector.PkgHandlers.Winget/WingetGame.cs index 95356f66..39ff5a8d 100644 --- a/src/GameCollector.PkgHandlers.Winget/WingetGame.cs +++ b/src/GameCollector.PkgHandlers.Winget/WingetGame.cs @@ -16,6 +16,7 @@ namespace GameCollector.PkgHandlers.Winget; /// /// /// +/// /// /// /// @@ -40,6 +41,7 @@ public record WingetGame(WingetGameId Id, string? InstallerUrl = "", AbsolutePath Uninstall = new(), bool IsInstalled = true, + bool IsOwned = true, DateTime? InstallDate = null, string? Description = "", string? Publisher = "", @@ -64,6 +66,7 @@ public record WingetGame(WingetGameId Id, Icon: Launch, Uninstall: Uninstall, IsInstalled: IsInstalled, + IsOwned: IsOwned, InstallDate: InstallDate, Metadata: new Dictionary>(StringComparer.OrdinalIgnoreCase) { diff --git a/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs b/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs index 6f01a01a..6325e05f 100644 --- a/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs +++ b/src/GameCollector.PkgHandlers.Winget/WingetHandler.cs @@ -3,10 +3,12 @@ using System.Diagnostics; using System.Globalization; using System.IO; +//using System.Security.Principal; using System.Text; using GameFinder.Common; using GameFinder.RegistryUtils; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using NexusMods.Paths; using OneOf; @@ -21,11 +23,12 @@ namespace GameCollector.PkgHandlers.Winget; [PublicAPI] public class WingetHandler : AHandler { - internal const string TagQuery = "game"; + internal const string DefaultQuery = "game"; internal const string UninstallRegKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; private readonly IRegistry _registry; private readonly IFileSystem _fileSystem; + private ILogger? _logger; //private WindowsPackageManagerFactory? _winGetFactory = null; @@ -34,6 +37,7 @@ public class WingetHandler : AHandler UseShellExecute = false, RedirectStandardOutput = true, StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, }; @@ -52,10 +56,11 @@ public class WingetHandler : AHandler /// . For tests either use , /// a custom implementation or just a mock of the interface. /// - public WingetHandler(IRegistry registry, IFileSystem fileSystem) + public WingetHandler(IRegistry registry, IFileSystem fileSystem, ILogger? logger = null) { _registry = registry; _fileSystem = fileSystem; + _logger = logger; if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) { @@ -89,7 +94,7 @@ public override AbsolutePath FindClient() /// public override IEnumerable> FindAllGames(Settings? settings = null) { - return FindAllGames(settings?.InstalledOnly ?? false); + return FindAllGames(settings?.InstalledOnly ?? false, settings?.OwnedOnly ?? false, settings?.GamesOnly ?? false); } /// @@ -97,9 +102,17 @@ public override IEnumerable> FindAllGames(Settin /// will always be a non-null game or a non-null error message. /// /// + /// + /// + /// /// Get additional information about winget/msstore packages /// - public IEnumerable> FindAllGames(bool? installedOnly = false, bool? expandPackage = false) + public IEnumerable> FindAllGames( + bool installedOnly = false, + bool ownedOnly = false, + bool gamesOnly = false, + string? query = DefaultQuery, + bool expandPackage = false) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) { @@ -114,7 +127,7 @@ public IEnumerable> FindAllGames(bool? installed } */ - Dictionary> games = new(); + Dictionary> installed = new(); // Create Package Manager and get available catalogs //var manager = _winGetFactory.CreatePackageManager(); @@ -123,13 +136,13 @@ public IEnumerable> FindAllGames(bool? installed /* foreach (var catalog in availableCatalogs.ToArray()) { - // Create a filter to search for packages with "game" tag //var filterList = _winGetFactory.CreateFindPackagesOptions(); + // Create a filter to search for packages with query ["game" default] // Add the query to the filter var tagFilter = _winGetFactory.CreatePackageMatchFilter(); tagFilter.Field = PackageMatchField.Tag; - tagFilter.Value = TagQuery; + tagFilter.Value = query; filterList.Filters.Add(tagFilter); // Find the packages with the filters @@ -140,7 +153,7 @@ public IEnumerable> FindAllGames(bool? installed var pkg = match.CatalogPackage; var id = WingetGameId.From(pkg.Id); - games.Add(id, new WingetGame( + installed.Add(id, new WingetGame( Id: id, Name: pkg.Name, InstallDirectory: default, @@ -156,43 +169,67 @@ public IEnumerable> FindAllGames(bool? installed // Get all installed items //var installedCatalogs = manager.GetLocalPackageCatalog; - foreach (var item in GetInstalled(expandPackage)) + if (!gamesOnly) { - if (item.IsT0) + //installed = GetInstalled(); + installed = GetInstalledParse(expandPackage); + foreach (var item in installed) { - if (!games.ContainsKey(item.AsT0.Id)) - continue; - - yield return new WingetGame( - Id: item.AsT0.Id, - Name: item.AsT0.Name, - InstallDirectory: default, - IsInstalled: true, - //PkgTags: item.CatalogPackage.Tags, - //Source: item.CatalogPackage.Source, - InstalledVersion: item.AsT0.InstalledVersion, - DefaultVersion: item.AsT0.DefaultVersion - ); + if (item.Value.IsT0) + { + yield return new WingetGame( + Id: item.Key, + Name: item.Value.AsT0.Name, + InstallDirectory: default, + IsInstalled: true, + IsOwned: true, + //PkgTags: item.CatalogPackage.Tags, + //Source: item.CatalogPackage.Source, + InstalledVersion: item.Value.AsT0.InstalledVersion, + DefaultVersion: item.Value.AsT0.DefaultVersion + ); + } + else yield return item.Value.AsT1; } - else yield return item.AsT1; } //var freeGames = SearchFreeGames(); + if (!installedOnly && !string.IsNullOrEmpty(query)) + { + foreach (var game in SearchFreeGamesParse(query, expandPackage)) + { + if (!installed.ContainsKey(game.Key)) + { + if (game.Value.IsT0) + { + yield return new WingetGame( + Id: game.Key, + Name: game.Value.AsT0.Name, + InstallDirectory: default, + IsInstalled: false, + IsOwned: false, + DefaultVersion: game.Value.AsT0.DefaultVersion + ); + } + else yield return game.Value.AsT1; + } + } + } } - private IEnumerable> GetInstalled() + private Dictionary> GetInstalled() { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041, 0)) { - yield return new ErrorMessage("Only supported on Windows 10.0.19041.0 or later."); - yield break; + _logger?.LogDebug("***Only supported on Windows 10.0.19041.0 or later."); + return new() { [WingetGameId.From("")] = new ErrorMessage("Only supported on Windows 10.0.19041.0 or later.") }; } /* if (_winGetFactory is null) { - yield return new ErrorMessage("Could not access Windows Package Manager."); - yield break; + _logger?.LogDebug("***Could not access Windows Package Manager."); + return new() { [WingetGameId.From("")] = new ErrorMessage("Could not access Windows Package Manager.") }; } */ @@ -212,7 +249,7 @@ private IEnumerable> GetInstalled() { var selectedRemoteCatalogRef = winGetManager.GetPackageCatalogs().ToArray()[selectedIndex]; - //Console.WriteLine($"Searching on package catalog {selectedRemoteCatalogRef.Info.Name} "); + //_logger?.LogDebug($"Searching on package catalog {selectedRemoteCatalogRef.Info.Name} "); var createCompositePackageCatalogOptions = _winGetFactory.CreateCreateCompositePackageCatalogOptions(); createCompositePackageCatalogOptions.Catalogs.Add(selectedRemoteCatalogRef); @@ -242,16 +279,17 @@ private IEnumerable> GetInstalled() foreach (var match in taskResult.Matches.ToArray()) { var pkg = match.CatalogPackage; - //Console.WriteLine($"Package {pkg.Name} is available Online: " + pkg.DefaultInstallVersion.PackageCatalog.Info.Name); + //_logger?.LogDebug($"Package {pkg.Name} is available Online: " + pkg.DefaultInstallVersion.PackageCatalog.Info.Name); yield return new WingetGame( Id: WingetGameId.From(pkg.Id), Name: pkg.Name, InstallDirectory: default, IsInstalled: pkg.DefaultInstallVersion != null, + IsOwned: pkg.DefaultInstallVersion != null, //PackageTags: pkg.Tags???, Source: pkg.DefaultInstallVersion?.Channel, // is this winget vs. msstore? PackageName: pkg.DefaultInstallVersion?.PackageCatalog.Info.Name, - ProductCodes: pkg.DefaultInstallVersion?.ProductCodes.ToList(), + //ProductCodes: pkg.DefaultInstallVersion?.ProductCodes.ToList(), Publisher: pkg.InstalledVersion.Publisher, InstalledVersion: pkg.InstalledVersion.DisplayName, DefaultVersion: pkg.DefaultInstallVersion?.DisplayName); @@ -259,33 +297,46 @@ private IEnumerable> GetInstalled() */ // End enumeration + return new(); } - private IEnumerable> GetInstalled(bool? expandPackage) + private Dictionary> GetInstalled(bool? expandPackage) + { + Dictionary> installed = new(); + return installed; + } + + private Dictionary> GetInstalledParse(bool expandPackage) { //************************************* Console.OutputEncoding = Encoding.UTF8; //************************************* + Dictionary> installed = new(); + var wingetExe = FindClient(); if (wingetExe == default) { - yield return new ErrorMessage("Winget not installed"); - yield break; + _logger?.LogDebug("***Winget not installed"); + return new() { [WingetGameId.From("")] = new ErrorMessage("Winget not installed") }; } using var process = new Process(); process.StartInfo = _startInfo; process.StartInfo.FileName = wingetExe.GetFullPath(); process.StartInfo.Arguments = "list --nowarn --disable-interactivity"; + process.StartInfo.StandardOutputEncoding = Encoding.UTF8; + process.StartInfo.StandardErrorEncoding = Encoding.UTF8; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (string.IsNullOrEmpty(output)) { - yield return new ErrorMessage("No output from winget"); - yield break; + _logger?.LogDebug("***No output from winget"); + return new() { [WingetGameId.From("")] = new ErrorMessage("No output from winget") }; } List colPos = new(); @@ -309,31 +360,47 @@ private IEnumerable> GetInstalled(bool? expandPa } else if (i > 1 && !string.IsNullOrWhiteSpace(line)) // skip separator line { - var name = line[colPos[0]..colPos[1]].Trim(); - var strId = line[colPos[1]..colPos[2]].Trim(); - var id = WingetGameId.From(strId); - var version = line[colPos[2]..colPos[3]].Trim(); - var available = line[colPos[3]..colPos[4]].Trim(); - var source = line[colPos[4]..].Trim(); + string name = "", strId = "", version = "", available = ""; + string? source = null; + WingetGameId id = default; + // TODO: This doesn't work if there are surrogate pairs in the string + if (line.Length > colPos[1]) + name = line[colPos[0]..colPos[1]].Trim(); + if (line.Length > colPos[2]) + { + strId = line[colPos[1]..colPos[2]].Trim(); + id = WingetGameId.From(strId); + } + if (line.Length > colPos[3]) + version = line[colPos[2]..colPos[3]].Trim(); + if (line.Length > colPos[4]) + { + available = line[colPos[3]..colPos[4]].Trim(); + source = line[colPos[4]..].Trim(); + } if (string.IsNullOrWhiteSpace(source)) // non-package install { - if (expandPackage != true) + if (!expandPackage) { - Console.WriteLine(" " + name); - yield return new WingetGame( + if (installed.TryAdd(id, new WingetGame( Id: id, Name: name, InstallDirectory: default, IsInstalled: true, - InstalledVersion: version); + IsOwned: true, + InstalledVersion: version))) + { + _logger?.LogDebug(" " + name); + } i++; continue; } if (name.EndsWith('…') || strId.EndsWith('…') || version.EndsWith('…')) { - (name, strId, version) = Relist(name, strId, version); + (name, strId, version) = Relist(name, strId, version, source); + strId = strId.TrimEnd('…'); id = WingetGameId.From(strId); } @@ -342,84 +409,105 @@ private IEnumerable> GetInstalled(bool? expandPa var game = ParseRegistry(strId); if (game.IsT0) { - Console.WriteLine("@ " + name); - yield return new WingetGame( + if (installed.TryAdd(id, new WingetGame( Id: id, Name: name, InstallDirectory: game.AsT0.InstallDirectory, Launch: game.AsT0.Launch, Uninstall: game.AsT0.Uninstall, IsInstalled: true, + IsOwned: true, InstallDate: game.AsT0.InstallDate, Publisher: game.AsT0.Publisher, SupportUrl: game.AsT0.SupportUrl, Homepage: game.AsT0.Homepage, - InstalledVersion: version); + InstalledVersion: version))) + { + _logger?.LogDebug("@ " + name); + } i++; continue; } - yield return game.AsT1; + if (installed.TryAdd(id, game.AsT1)) + { + _logger?.LogDebug("***" + game.AsT1.Message); + } + i++; } - Console.WriteLine("? " + name); - yield return new WingetGame( + if (installed.TryAdd(id, new WingetGame( Id: id, Name: name, InstallDirectory: default, IsInstalled: true, - InstalledVersion: version); + IsOwned: true, + InstalledVersion: version))) + { + _logger?.LogDebug("? " + name); + } + i++; + continue; } - else // package install + + //else [package install] + + if (!expandPackage) { - if (expandPackage != true) + if (name.EndsWith('…') || strId.EndsWith('…') || version.EndsWith('…')) { - if (name.EndsWith('…') || strId.EndsWith('…') || version.EndsWith('…')) - { - (name, strId, version) = Relist(name, strId, version); - id = WingetGameId.From(strId); - } - Console.WriteLine(" " + name); - yield return new WingetGame( - Id: id, - Name: name, - InstallDirectory: default, - IsInstalled: true, - Source: source, - InstalledVersion: version, - DefaultVersion: available); - i++; - continue; + (name, strId, version) = Relist(name, strId, version, source); + strId = strId.TrimEnd('…'); + id = WingetGameId.From(strId); } - if (strId.EndsWith('…')) + if (installed.TryAdd(id, new WingetGame( + Id: id, + Name: name, + InstallDirectory: default, + IsInstalled: true, + IsOwned: true, + Source: source, + InstalledVersion: version, + DefaultVersion: available))) { - (name, strId, version) = Relist(name, strId, version); - id = WingetGameId.From(strId); + _logger?.LogDebug(" " + name); } - yield return GetPackageInfo(colPos, line, search: false); + i++; + continue; + } + + if (strId.EndsWith('…')) + { + (name, strId, version) = Relist(name, strId, version, source); + strId = strId.TrimEnd('…'); + id = WingetGameId.From(strId); + } + if (installed.TryAdd(id, GetPackageInfo(colPos, line, search: false))) + { + _logger?.LogDebug(" " + name); } } + i++; } + _logger?.LogDebug("GetInstalledParse(): " + i + " apps"); + + return installed; } - private (string, string, string) Relist(string name, string id, string version) + private (string, string, string) Relist(string name, string id, string version, string? source) { - var subName = name; - if (name.EndsWith('…')) - subName = name[..^1]; - var subId = id; - if (id.EndsWith('…')) - subId = id[..^1]; - var wingetExe = FindClient(); + if (string.IsNullOrWhiteSpace(source)) + source = "winget"; + using var process = new Process(); process.StartInfo = _startInfo; process.StartInfo.FileName = wingetExe.GetFullPath(); - //process.StartInfo.Arguments = $"list --name \"{subName}\" --nowarn --disable-interactivity"; - process.StartInfo.Arguments = $"list --id \"{subId}\" --nowarn --disable-interactivity"; + //process.StartInfo.Arguments = $"list --name \"{name.TrimEnd('…')}\" --source {source} --nowarn --disable-interactivity"; + process.StartInfo.Arguments = $"list --id \"{id.TrimEnd('…')}\" --source {source} --nowarn --disable-interactivity"; process.Start(); var listOutput = process.StandardOutput.ReadToEnd(); process.WaitForExit(); @@ -445,17 +533,23 @@ private IEnumerable> GetInstalled(bool? expandPa } else if (i > 1 && !string.IsNullOrWhiteSpace(listLine)) // skip separator line { - name = listLine[listCols[0]..listCols[1]].Trim(); - id = listLine[listCols[1]..listCols[2]].Trim(); - version = listLine[listCols[2]..]; + // TODO: This doesn't work if there are surrogate pairs in the string + if (listLine.Length > listCols[1]) + name = listLine[listCols[0]..listCols[1]].Trim(); + if (listLine.Length > listCols[2]) + { + id = listLine[listCols[1]..listCols[2]].Trim(); + version = listLine[listCols[2]..].Trim(); + } } + i++; } return (name, id, version); } - private Dictionary> SearchFreeGames(bool expandPackage = false) + private Dictionary> SearchFreeGamesParse(string query = DefaultQuery, bool expandPackage = false) { Dictionary> freeGames = new(); @@ -464,13 +558,14 @@ private Dictionary> SearchFreeGame using var process = new Process(); process.StartInfo = _startInfo; process.StartInfo.FileName = wingetExe.GetFullPath(); - process.StartInfo.Arguments = "search --tag game --nowarn --disable-interactivity"; + process.StartInfo.Arguments = $"search --tag {query} --source winget --nowarn --disable-interactivity"; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (string.IsNullOrEmpty(output)) { + _logger?.LogDebug("***No output from winget"); return new() { [WingetGameId.From("")] = new ErrorMessage("No output from winget") }; } @@ -495,29 +590,50 @@ private Dictionary> SearchFreeGame } else if (i > 1 && !string.IsNullOrWhiteSpace(line)) // skip separator line { - var name = line[colPos[0]..colPos[1]].Trim(); - var strId = line[colPos[1]..colPos[2]].Trim(); - var id = WingetGameId.From(strId); - var available = line[colPos[2]..colPos[3]].Trim(); - //var match = line[colPos[3]..colPos[4]].Trim(); - var source = line[colPos[4]..].Trim(); + string name = "", strId = "", available = "", match = ""; + string? source = null; + WingetGameId id = default; + // TODO: This doesn't work if there are surrogate pairs in the string + if (line.Length > colPos[1]) + name = line[colPos[0]..colPos[1]].Trim(); + if (line.Length > colPos[2]) + { + strId = line[colPos[1]..colPos[2]].Trim(); + id = WingetGameId.From(strId); + } + if (line.Length > colPos[3]) + available = line[colPos[2]..colPos[3]].Trim(); + if (line.Length > colPos[4]) + { + match = line[colPos[3]..colPos[4]].Trim(); + source = line[colPos[4]..].Trim(); + } if (!expandPackage) { - freeGames.Add(id, new WingetGame( + if (freeGames.TryAdd(id, new WingetGame( Id: id, Name: name, InstallDirectory: default, IsInstalled: false, - DefaultVersion: available)); - i++; + IsOwned: false, + DefaultVersion: available))) + { + _logger?.LogDebug("* " + name); + i++; + } continue; } - freeGames.Add(id, GetPackageInfo(colPos, line, search: true)); + if (freeGames.TryAdd(id, GetPackageInfo(colPos, line, search: true))) + { + _logger?.LogDebug("*@" + name); + } } + i++; } + _logger?.LogDebug("SearchFreeGamesParse(): " + i + " games"); return freeGames; } @@ -526,28 +642,30 @@ private OneOf ParseRegistry(string id) { // id syntax = "ARP\[Machine|User]\[X64|X86]\ - var regKeyName = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"; - var baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine); + string? regKeyName; // = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"; + IRegistryKey baseKey; // = _registry.OpenBaseKey(RegistryHive.LocalMachine); if (id.StartsWith(@"ARP\Machine\X64\", StringComparison.Ordinal)) { - regKeyName += id[@"ARP\Machine\X64\".Length..]; + regKeyName = UninstallRegKey + id[@"ARP\Machine\X64\".Length..]; baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); } else if (id.StartsWith(@"ARP\Machine\X86\", StringComparison.Ordinal)) { - regKeyName += id[@"ARP\Machine\X86\".Length..]; + regKeyName = UninstallRegKey + id[@"ARP\Machine\X86\".Length..]; baseKey = _registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); } else if (id.StartsWith(@"ARP\User\X64\", StringComparison.Ordinal)) { - regKeyName += id[@"ARP\User\X64\".Length..]; + regKeyName = UninstallRegKey + id[@"ARP\User\X64\".Length..]; baseKey = _registry.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); } else if (id.StartsWith(@"ARP\User\X86\", StringComparison.Ordinal)) { - regKeyName += id[@"ARP\User\X86\".Length..]; + regKeyName = UninstallRegKey + id[@"ARP\User\X86\".Length..]; baseKey = _registry.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32); } + else + return new ErrorMessage("Did not find expected \"ARP\\[Machine|User]\\[X64|X86]\\\" registry key prefix in id " + id); try { @@ -747,16 +865,18 @@ private OneOf GetPackageInfo(List colPos, string isTags = true; else if (pkgLine.StartsWith(" Installer Url: ", StringComparison.Ordinal)) installerUrl = pkgLine[" Installer Url: ".Length..]; + i++; } - Console.WriteLine("> " + name); + _logger?.LogDebug("> " + name); return new WingetGame( Id: id, Name: name, InstallDirectory: default, InstallerUrl: installerUrl, IsInstalled: true, + IsOwned: true, Description: description, Publisher: publisher, Author: author,