From 93166ccf1ce17f0c71f87ae8fc74a077b03a0620 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 17 Feb 2025 15:59:35 +0100 Subject: [PATCH] Heroic: Support linux native games --- CHANGELOG.md | 4 ++ other/GameFinder.Example/Program.cs | 7 +-- src/GameFinder.Launcher.Heroic/DTOs/Json.cs | 4 +- .../HeroicGOGGame.cs | 22 +++++++-- .../HeroicGOGHandler.cs | 46 +++++++++++++++---- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4ba47a..9d0b46ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com) and this p ## [Released](https://github.com/erri120/GameFinder/releases) +## [4.5.0](https://github.com/erri120/GameFinder/compare/v4.4.0...v4.5.0) - 2025-02-17 + +- Heroic (GOG): Support linux native games + ## [4.4.0](https://github.com/erri120/GameFinder/compare/v4.3.4...v4.4.0) - 2024-12-10 - GOG & Heroic (GOG): Add `BuildId` diff --git a/other/GameFinder.Example/Program.cs b/other/GameFinder.Example/Program.cs index 69efe316..d6640f9c 100644 --- a/other/GameFinder.Example/Program.cs +++ b/other/GameFinder.Example/Program.cs @@ -109,10 +109,7 @@ private static void Run(Options options, ILogger logger) if (OperatingSystem.IsLinux()) { if (options.Steam) RunSteamHandler(realFileSystem, registry: null); - if (options.Heroic) - { - RunHeroicGOGHandler(realFileSystem); - } + if (options.Heroic) RunHeroicGOGHandler(realFileSystem); var winePrefixes = new List(); @@ -157,7 +154,7 @@ private static void RunGOGHandler(IRegistry registry, IFileSystem fileSystem) private static void RunHeroicGOGHandler(IFileSystem fileSystem) { var logger = _provider.CreateLogger(nameof(HeroicGOGHandler)); - var handler = new HeroicGOGHandler(fileSystem); + var handler = new HeroicGOGHandler(fileSystem, logger); LogGamesAndErrors(handler.FindAllGames(), logger); } diff --git a/src/GameFinder.Launcher.Heroic/DTOs/Json.cs b/src/GameFinder.Launcher.Heroic/DTOs/Json.cs index a4fc851e..c926e243 100644 --- a/src/GameFinder.Launcher.Heroic/DTOs/Json.cs +++ b/src/GameFinder.Launcher.Heroic/DTOs/Json.cs @@ -9,7 +9,7 @@ internal record Installed( [property: JsonPropertyName("install_path")] string InstallPath, [property: JsonPropertyName("install_size")] string InstallSize, [property: JsonPropertyName("is_dlc")] bool IsDlc, - [property: JsonPropertyName("version")] string Version, + [property: JsonPropertyName("version")] string? Version, [property: JsonPropertyName("appName")] string AppName, [property: JsonPropertyName("installedDLCs")] IReadOnlyList InstalledDLCs, [property: JsonPropertyName("language")] string Language, @@ -22,7 +22,7 @@ internal record Root( [property: JsonPropertyName("installed")] IReadOnlyList Installed ); -internal record GameConfig( +internal record LinuxGameConfig( [property: JsonPropertyName("autoInstallDxvk")] bool AutoInstallDxvk, [property: JsonPropertyName("autoInstallDxvkNvapi")] bool AutoInstallDxvkNvapi, [property: JsonPropertyName("autoInstallVkd3d")] bool AutoInstallVkd3d, diff --git a/src/GameFinder.Launcher.Heroic/HeroicGOGGame.cs b/src/GameFinder.Launcher.Heroic/HeroicGOGGame.cs index e1a7fa00..d54e90ba 100644 --- a/src/GameFinder.Launcher.Heroic/HeroicGOGGame.cs +++ b/src/GameFinder.Launcher.Heroic/HeroicGOGGame.cs @@ -1,23 +1,37 @@ +using System.Runtime.InteropServices; using GameFinder.StoreHandlers.GOG; using GameFinder.Wine; +using JetBrains.Annotations; using NexusMods.Paths; namespace GameFinder.Launcher.Heroic; +/// +/// Represents a GOG game installed via Heroic. +/// +[PublicAPI] public record HeroicGOGGame( GOGGameId Id, string Name, AbsolutePath Path, string BuildId, - AbsolutePath WinePrefixPath, - DTOs.WineVersion WineVersion) : GOGGame(Id, Name, Path, BuildId) + WineData? WineData, + OSPlatform Platform) : GOGGame(Id, Name, Path, BuildId) { - public WinePrefix GetWinePrefix() + /// + /// Gets the wine prefix, if any. + /// + public WinePrefix? GetWinePrefix() { + if (WineData is null) return null; + return new WinePrefix { - ConfigurationDirectory = WinePrefixPath.Combine("pfx"), + ConfigurationDirectory = WineData.WinePrefixPath.Combine("pfx"), UserName = "steamuser", }; } } + +[PublicAPI] +public record WineData(AbsolutePath WinePrefixPath, DTOs.WineVersion WineVersion); diff --git a/src/GameFinder.Launcher.Heroic/HeroicGOGHandler.cs b/src/GameFinder.Launcher.Heroic/HeroicGOGHandler.cs index 4a5c493c..497e6c94 100644 --- a/src/GameFinder.Launcher.Heroic/HeroicGOGHandler.cs +++ b/src/GameFinder.Launcher.Heroic/HeroicGOGHandler.cs @@ -4,10 +4,12 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text.Json; using GameFinder.Common; using GameFinder.StoreHandlers.GOG; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using NexusMods.Paths; using OneOf; @@ -17,6 +19,7 @@ namespace GameFinder.Launcher.Heroic; public class HeroicGOGHandler : AHandler { private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonSerializerOptions = new() { @@ -27,9 +30,10 @@ public class HeroicGOGHandler : AHandler /// /// Constructor. /// - public HeroicGOGHandler(IFileSystem fileSystem) + public HeroicGOGHandler(IFileSystem fileSystem, ILogger logger) { _fileSystem = fileSystem; + _logger = logger; } /// @@ -65,7 +69,7 @@ public override IEnumerable> FindAllGames() } } - internal static IEnumerable> ParseInstalledJsonFile(AbsolutePath path, AbsolutePath configPath) + internal IEnumerable> ParseInstalledJsonFile(AbsolutePath path, AbsolutePath configPath) { using var stream = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); var root = JsonSerializer.Deserialize(stream, JsonSerializerOptions); @@ -91,8 +95,7 @@ internal static IEnumerable> ParseInstalledJsonFile } } - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(JsonSerializerOptions)")] - internal static OneOf Parse( + internal OneOf Parse( DTOs.Installed installed, AbsolutePath configPath, IFileSystem fileSystem) @@ -102,7 +105,31 @@ internal static OneOf Parse( return new ErrorMessage($"The value \"appName\" is not a number: \"{installed.AppName}\""); } - var gamesConfigFile = GetGamesConfigJsonFile(configPath, id.ToString(CultureInfo.InvariantCulture)); + var installedPlatform = installed.Platform switch + { + "windows" => OSPlatform.Windows, + "linux" => OSPlatform.Linux, + _ => OSPlatform.Create(installed.Platform) + }; + + WineData? wineData = null; + if (installedPlatform == OSPlatform.Windows && OperatingSystem.IsLinux()) + { + var wineDataRes = GetWineData(installed, configPath, id); + if (wineDataRes.TryPickT1(out var errorMessage, out wineData)) + { + _logger.LogWarning("Unable to extract WineData for `{Id}`: {Message}", id, errorMessage.Message); + } + } + + var path = fileSystem.FromUnsanitizedFullPath(installed.InstallPath); + return new HeroicGOGGame(GOGGameId.From(id), installed.AppName, path, installed.BuildId, wineData, installedPlatform); + } + + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(JsonSerializerOptions)")] + internal OneOf GetWineData(DTOs.Installed installed, AbsolutePath configPath, long id) + { + var gamesConfigFile = GetLinuxGamesConfigJsonFile(configPath, id.ToString(CultureInfo.InvariantCulture)); if (!gamesConfigFile.FileExists) return new ErrorMessage($"File `{gamesConfigFile}` doesn't exist!"); using var stream = gamesConfigFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read); @@ -113,16 +140,15 @@ internal static OneOf Parse( }); var element = doc.RootElement.GetProperty(id.ToString(CultureInfo.InvariantCulture)); - var gameConfig = element.Deserialize(); + var gameConfig = element.Deserialize(); if (gameConfig is null) return new ErrorMessage($"Unable to deserialize `{gamesConfigFile}`"); - var path = fileSystem.FromUnsanitizedFullPath(installed.InstallPath); - var winePrefixPath = fileSystem.FromUnsanitizedFullPath(gameConfig.WinePrefix); + var winePrefixPath = _fileSystem.FromUnsanitizedFullPath(gameConfig.WinePrefix); - return new HeroicGOGGame(GOGGameId.From(id), installed.AppName, path, installed.BuildId, winePrefixPath, gameConfig.WineVersion); + return new WineData(winePrefixPath, gameConfig.WineVersion); } - internal static AbsolutePath GetGamesConfigJsonFile(AbsolutePath configPath, string name) + internal static AbsolutePath GetLinuxGamesConfigJsonFile(AbsolutePath configPath, string name) { return configPath.Combine("GamesConfig").Combine($"{name}.json"); }