Skip to content

Commit

Permalink
Heroic: Support linux native games
Browse files Browse the repository at this point in the history
  • Loading branch information
erri120 committed Feb 17, 2025
1 parent bb462fb commit 93166cc
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
7 changes: 2 additions & 5 deletions other/GameFinder.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AWinePrefix>();

Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions src/GameFinder.Launcher.Heroic/DTOs/Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> InstalledDLCs,
[property: JsonPropertyName("language")] string Language,
Expand All @@ -22,7 +22,7 @@ internal record Root(
[property: JsonPropertyName("installed")] IReadOnlyList<Installed> Installed
);

internal record GameConfig(
internal record LinuxGameConfig(
[property: JsonPropertyName("autoInstallDxvk")] bool AutoInstallDxvk,
[property: JsonPropertyName("autoInstallDxvkNvapi")] bool AutoInstallDxvkNvapi,
[property: JsonPropertyName("autoInstallVkd3d")] bool AutoInstallVkd3d,
Expand Down
22 changes: 18 additions & 4 deletions src/GameFinder.Launcher.Heroic/HeroicGOGGame.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a GOG game installed via Heroic.
/// </summary>
[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()
/// <summary>
/// Gets the wine prefix, if any.
/// </summary>
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);
46 changes: 36 additions & 10 deletions src/GameFinder.Launcher.Heroic/HeroicGOGHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,6 +19,7 @@ namespace GameFinder.Launcher.Heroic;
public class HeroicGOGHandler : AHandler<GOGGame, GOGGameId>
{
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;

private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
Expand All @@ -27,9 +30,10 @@ public class HeroicGOGHandler : AHandler<GOGGame, GOGGameId>
/// <summary>
/// Constructor.
/// </summary>
public HeroicGOGHandler(IFileSystem fileSystem)
public HeroicGOGHandler(IFileSystem fileSystem, ILogger logger)
{
_fileSystem = fileSystem;
_logger = logger;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -65,7 +69,7 @@ public override IEnumerable<OneOf<GOGGame, ErrorMessage>> FindAllGames()
}
}

internal static IEnumerable<OneOf<GOGGame, ErrorMessage>> ParseInstalledJsonFile(AbsolutePath path, AbsolutePath configPath)
internal IEnumerable<OneOf<GOGGame, ErrorMessage>> ParseInstalledJsonFile(AbsolutePath path, AbsolutePath configPath)
{
using var stream = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
var root = JsonSerializer.Deserialize<DTOs.Root>(stream, JsonSerializerOptions);
Expand All @@ -91,8 +95,7 @@ internal static IEnumerable<OneOf<GOGGame, ErrorMessage>> ParseInstalledJsonFile
}
}

[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize<TValue>(JsonSerializerOptions)")]
internal static OneOf<GOGGame, ErrorMessage> Parse(
internal OneOf<GOGGame, ErrorMessage> Parse(
DTOs.Installed installed,
AbsolutePath configPath,
IFileSystem fileSystem)
Expand All @@ -102,7 +105,31 @@ internal static OneOf<GOGGame, ErrorMessage> 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<TValue>(JsonSerializerOptions)")]
internal OneOf<WineData, ErrorMessage> 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);
Expand All @@ -113,16 +140,15 @@ internal static OneOf<GOGGame, ErrorMessage> Parse(
});

var element = doc.RootElement.GetProperty(id.ToString(CultureInfo.InvariantCulture));
var gameConfig = element.Deserialize<DTOs.GameConfig>();
var gameConfig = element.Deserialize<DTOs.LinuxGameConfig>();
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");
}
Expand Down

0 comments on commit 93166cc

Please sign in to comment.