Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/GameFinder.StoreHandlers.Steam/Models/RegistryEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Text;
using FluentResults;
using GameFinder.RegistryUtils;
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;

/// <summary>
/// Represents a parsed registry entry.
/// </summary>
[PublicAPI]
public sealed record RegistryEntry
{
/// <summary>
/// Gets the unique identifier of the app
/// that was parsed to produce this <see cref="RegistryEntry"/>.
/// </summary>
public required AppId AppId { get; init; }

#region Parsed Values

/// <summary>
/// Gets the <see cref="IRegistryKey"/> for the Uninstall registry subkey.
/// </summary>
public required IRegistryKey RegistryPath { get; init; }

/// <summary>
/// Gets the path to the icon for this app.
/// </summary>
public AbsolutePath? DisplayIcon { get; init; }

/// <summary>
/// Gets name of the app.
/// </summary>
public required string DisplayName { get; init; }

/// <summary>
/// Gets the help URL (invariably https://help.steampowered.com/)
/// </summary>
public required string HelpLink { get; init; }

/// <summary>
/// Gets the installation directory of the app.
/// </summary>
public required AbsolutePath? InstallLocation { get; init; }

/// <summary>
/// Gets the publisher name
/// </summary>
public required string Publisher { get; init; }

/// <summary>
/// Gets the uninstall executable
/// </summary>
/// <example><c>"C:\Program Files\Steam\steam.exe"</c></example>
public required AbsolutePath? UninstallExecutable { get; init; }

/// <summary>
/// Gets the uninstall parameters (note the steam:// URL by itself without the executable should be sufficient)
/// </summary>
/// <example><c>steam://uninstall/262060</c></example>
public required string UninstallParameters { get; init; }

/// <summary>
/// Gets the info URL
/// </summary>
public required string URLInfoAbout { get; init; }

#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();

/// <summary>
/// Parses the registry for <see cref="AppId"/> again and returns a new
/// instance of <see cref="RegistryEntry"/>.
/// </summary>
[Pure]
[System.Diagnostics.Contracts.Pure]
[MustUseReturnValue]
public Result<RegistryEntry> Reload(IFileSystem fileSystem, IRegistry? registry)
{
return RegistryEntryParser.ParseRegistryEntry(AppId, fileSystem, registry);
}

#endregion

#region Overwrites

/// <inheritdoc/>
public bool Equals(RegistryEntry? other)
{
if (other is null) return false;
if (AppId != other.AppId) return false;
if (DisplayIcon != other.DisplayIcon) return false;
if (!string.Equals(DisplayName, other.DisplayName, StringComparison.Ordinal)) return false;
if (!string.Equals(HelpLink, other.HelpLink, StringComparison.Ordinal)) return false;
if (InstallLocation != other.InstallLocation) return false;
if (!string.Equals(Publisher, other.Publisher, StringComparison.Ordinal)) return false;
if (UninstallExecutable != other.UninstallExecutable) return false;
if (!string.Equals(UninstallParameters, other.UninstallParameters, StringComparison.Ordinal)) return false;
if (!string.Equals(URLInfoAbout, other.URLInfoAbout, StringComparison.Ordinal)) return false;
return true;
}

/// <inheritdoc/>
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(AppId);
hashCode.Add(DisplayIcon);
hashCode.Add(DisplayName);
hashCode.Add(HelpLink);
hashCode.Add(InstallLocation);
hashCode.Add(Publisher);
hashCode.Add(UninstallExecutable);
hashCode.Add(UninstallParameters);
hashCode.Add(URLInfoAbout);
return hashCode.ToHashCode();
}

/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();

sb.Append("{ ");
sb.Append($"DisplayIcon = {DisplayIcon}, ");
sb.Append($"Uninstall = {UninstallExecutable} {UninstallParameters}");
sb.Append(" }");

return sb.ToString();
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.IO;
using FluentResults;
using GameFinder.RegistryUtils;
using GameFinder.StoreHandlers.Steam.Models;
using GameFinder.StoreHandlers.Steam.Models.ValueTypes;
using JetBrains.Annotations;
using NexusMods.Paths;

namespace GameFinder.StoreHandlers.Steam.Services;

/// <summary>
/// Parser for Steam Uninstall registry entries.
/// </summary>
/// <seealso cref="RegistryEntry"/>
[PublicAPI]
public static class RegistryEntryParser
{
internal const string UninstallRegKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";

/// <summary>
/// Parses the registry entry for the given Steam app ID.
/// </summary>
public static Result<RegistryEntry> ParseRegistryEntry(AppId appId, IFileSystem fileSystem, IRegistry? registry)
{
RegistryEntry regEntry;

if (fileSystem is null)
{
return Result.Ok();
return Result.Fail(new Error("Invalid filesystem parameter!"));
}
if (registry is null)
{
return Result.Ok();
return Result.Fail(new Error("Invalid registry parameter!"));
}

IRegistryKey? subKey = default;

try
{
// Entries are usually in HKLM64, but occasionally HKLM32 (or both)
var localMachine64 = registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
var localMachine32 = registry.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
using var subKey64 = localMachine64.OpenSubKey(Path.Combine(UninstallRegKey, "Steam App " + appId));
using var subKey32 = localMachine32.OpenSubKey(Path.Combine(UninstallRegKey, "Steam App " + appId));

subKey = subKey64;
if (subKey64 is null || string.IsNullOrEmpty(subKey64.GetString("InstallLocation")) &&
subKey32 is not null && !string.IsNullOrEmpty(subKey32.GetString("InstallLocation")))
{
subKey = subKey32;
}

if (subKey is null)
{
return Result.Ok();
return Result.Fail(
new Error("Invalid registry key!")
.WithMetadata("AppId", appId)
.WithMetadata("Key", subKey?.ToString())
);
}

var strIcon = subKey.GetString("DisplayIcon");
var strLoc = subKey.GetString("InstallLocation");
var strUninst = subKey.GetString("UninstallString");
var strUnExe = "";
var strUnParam = "";
if (strUninst is not null)
{
if (strUninst.StartsWith('"'))
{
strUnExe = strUninst[..strUninst.LastIndexOf('"')];
strUnParam = strUninst[(strUninst.LastIndexOf('"') + 1)..];
}
else if (strUninst.Contains(' ', StringComparison.Ordinal))
{
strUnExe = strUninst[..strUninst.IndexOf(' ', StringComparison.Ordinal)];
strUnParam = strUninst[(strUninst.IndexOf(' ', StringComparison.Ordinal) + 1)..];
}
else
{
strUnExe = strUninst;
}
}
regEntry = new()
{
AppId = appId,
RegistryPath = subKey,
DisplayIcon = Path.IsPathRooted(strIcon) ? fileSystem.FromUnsanitizedFullPath(strIcon) : null,
DisplayName = subKey.GetString("DisplayName") ?? "",
HelpLink = subKey.GetString("HelpLink") ?? "",
InstallLocation = Path.IsPathRooted(strLoc) ? fileSystem.FromUnsanitizedFullPath(strLoc) : null,
Publisher = subKey.GetString("Publisher") ?? "",
UninstallExecutable = Path.IsPathRooted(strUnExe) ? fileSystem.FromUnsanitizedFullPath(strUnExe) : null,
UninstallParameters = strUnParam,
URLInfoAbout = subKey.GetString("URLInfoAbout") ?? "",
};

return regEntry;
}
catch (Exception ex)
{
return Result.Ok();
return Result.Fail(
new ExceptionalError("Exception was thrown while parsing the registry!", ex)
.WithMetadata("Key", subKey?.ToString())
);
}
}
}
5 changes: 5 additions & 0 deletions src/GameFinder.StoreHandlers.Steam/SteamGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public sealed record SteamGame : IGame
/// </summary>
public required AppManifest AppManifest { get; init; }

/// <summary>
/// Gets the parsed <see cref="RegistryEntry"/> of this game.
/// </summary>
public RegistryEntry? RegistryEntry { get; init; }

/// <summary>
/// Gets the library folder that contains this game.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/GameFinder.StoreHandlers.Steam/SteamHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,19 @@ public override IEnumerable<OneOf<SteamGame, ErrorMessage>> FindAllGames()
continue;
}

var registryEntryResult = RegistryEntryParser.ParseRegistryEntry(appManifestResult.Value.AppId, _fileSystem, _registry);
/*
if (registryEntryResult.IsFailed)
{
yield return ConvertResultToErrorMessage(registryEntryResult);
}
*/

var steamGame = new SteamGame
{
SteamPath = steamPath,
AppManifest = appManifestResult.Value,
RegistryEntry = registryEntryResult.Value ?? default,
LibraryFolder = libraryFolder,
};

Expand Down