first commit
This commit is contained in:
28
NitroxModel/Discovery/InstallationFinders/ConfigFinder.cs
Normal file
28
NitroxModel/Discovery/InstallationFinders/ConfigFinder.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using NitroxModel.Helper;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read a local config value that contains the installation directory.
|
||||
/// </summary>
|
||||
public sealed class ConfigFinder : IGameFinder
|
||||
{
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string path = NitroxUser.PreferredGamePath;
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return Error("Configured game path was found empty. Please enter the path to the game installation");
|
||||
}
|
||||
|
||||
if (!GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Error($"Game installation directory config '{path}' is invalid. Please enter the path to the '{gameInfo.Name}' installation");
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
extern alias JB;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JB::JetBrains.Annotations;
|
||||
using NitroxModel.Discovery.Models;
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders.Core;
|
||||
|
||||
public sealed record GameFinderResult
|
||||
{
|
||||
public string ErrorMessage { get; init; }
|
||||
public GameLibraries Origin { get; init; }
|
||||
public string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of type that made the result.
|
||||
/// </summary>
|
||||
public string FinderName { get; init; } = "";
|
||||
|
||||
public bool IsOk => string.IsNullOrWhiteSpace(ErrorMessage) && !string.IsNullOrWhiteSpace(Path);
|
||||
|
||||
private GameFinderResult()
|
||||
{
|
||||
}
|
||||
|
||||
public static GameFinderResult Error(string message, [CallerFilePath] string callerCodeFile = "")
|
||||
{
|
||||
return new GameFinderResult
|
||||
{
|
||||
FinderName = callerCodeFile[(callerCodeFile.LastIndexOf("\\", StringComparison.Ordinal) + 1)..^3],
|
||||
ErrorMessage = message
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returned when game libraries were found but the game appears to not be installed.
|
||||
/// </summary>
|
||||
public static GameFinderResult NotFound([CallerFilePath] string callerCodeFile = "")
|
||||
{
|
||||
return new GameFinderResult
|
||||
{
|
||||
FinderName = callerCodeFile[(callerCodeFile.LastIndexOf("\\", StringComparison.Ordinal) + 1)..^3]
|
||||
};
|
||||
}
|
||||
|
||||
public static GameFinderResult Ok([NotNull] string path, [CallerFilePath] string callerCodeFile = "")
|
||||
{
|
||||
Validate.NotNull(path);
|
||||
return new GameFinderResult
|
||||
{
|
||||
FinderName = callerCodeFile[(callerCodeFile.LastIndexOf("\\", StringComparison.Ordinal) + 1)..^3],
|
||||
Path = path
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
extern alias JB;
|
||||
using JB::JetBrains.Annotations;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders.Core;
|
||||
|
||||
public interface IGameFinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches for game installation directory.
|
||||
/// </summary>
|
||||
/// <param name="gameInfo">Game to search for.</param>
|
||||
/// <returns>Nullable game installation</returns>
|
||||
[NotNull] GameFinderResult FindGame(GameInfo gameInfo);
|
||||
}
|
32
NitroxModel/Discovery/InstallationFinders/DiscordFinder.cs
Normal file
32
NitroxModel/Discovery/InstallationFinders/DiscordFinder.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// Trying to find install either in appdata or in C:. So for now we just check these 2 paths until we have a better way.
|
||||
/// Discord stores game files in a subfolder called "content" while the parent folder is used to store Discord related files instead.
|
||||
/// </summary>
|
||||
public sealed class DiscordFinder : IGameFinder
|
||||
{
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string localAppdataDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
|
||||
string path = Path.Combine(localAppdataDirectory, "DiscordGames", gameInfo.Name, "content");
|
||||
if (GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
path = Path.Combine("C:", "Games", gameInfo.Name, "content");
|
||||
if (GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// Trying to find the path in environment variables by the key {GAMEINFO FULLNAME}_INSTALLATION_PATH that contains the installation directory.
|
||||
/// <list>
|
||||
/// <item>SUBNAUTICA_INSTALLATION_PATH</item>
|
||||
/// <item>SUBNAUTICAZERO_INSTALLATION_PATH</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class EnvironmentFinder : IGameFinder
|
||||
{
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable($"{gameInfo.Name.ToUpper()}_INSTALLATION_PATH");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return Error($"Configured game path with environment variable {gameInfo.Name.ToUpper()}_INSTALLATION_PATH was found empty");
|
||||
}
|
||||
|
||||
if (!GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Error($"Game installation directory '{path}' is invalid. Please enter the path to the '{gameInfo.Name}' installation");
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
45
NitroxModel/Discovery/InstallationFinders/EpicGamesFinder.cs
Normal file
45
NitroxModel/Discovery/InstallationFinders/EpicGamesFinder.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// Trying to find the path in the Epic Games installation records.
|
||||
/// </summary>
|
||||
public sealed class EpicGamesFinder : IGameFinder
|
||||
{
|
||||
private static readonly Regex installLocationRegex = new("\"InstallLocation\"[^\"]*\"(.*)\"");
|
||||
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string commonAppFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
string epicGamesManifestsDir = Path.Combine(commonAppFolder, "Epic", "EpicGamesLauncher", "Data", "Manifests");
|
||||
|
||||
if (!Directory.Exists(epicGamesManifestsDir))
|
||||
{
|
||||
return Error("Epic games manifest directory does not exist. Verify that Epic Games Store has been installed");
|
||||
}
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(epicGamesManifestsDir, "*.item"))
|
||||
{
|
||||
string fileText = File.ReadAllText(file);
|
||||
Match match = installLocationRegex.Match(fileText);
|
||||
|
||||
if (match.Success && match.Value.Contains(gameInfo.Name))
|
||||
{
|
||||
string matchedPath = Path.GetFullPath(match.Groups[1].Value);
|
||||
if (!GameInstallationHelper.HasValidGameFolder(matchedPath, gameInfo))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(matchedPath);
|
||||
}
|
||||
}
|
||||
|
||||
return Error("Could not find game installation directory from Epic Games installation records. Verify that game has been installed with Epic Games Store");
|
||||
}
|
||||
}
|
23
NitroxModel/Discovery/InstallationFinders/MicrosoftFinder.cs
Normal file
23
NitroxModel/Discovery/InstallationFinders/MicrosoftFinder.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.IO;
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// MS Store games are stored under <c>C:\XboxGames\[GAME]\Content\</c> by default.
|
||||
/// It's likely we could read the choosen path from <c>C:\Program Files\WindowsApps</c> but we're unable to read store settings from those folders.
|
||||
/// </summary>
|
||||
public sealed class MicrosoftFinder : IGameFinder
|
||||
{
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string path = Path.Combine("C:", "XboxGames", gameInfo.Name, "Content");
|
||||
if (!GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Error($"Game installation directory '{path}' is invalid. Please enter the path to the '{gameInfo.Name}' installation");
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
164
NitroxModel/Discovery/InstallationFinders/SteamFinder.cs
Normal file
164
NitroxModel/Discovery/InstallationFinders/SteamFinder.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using NitroxModel.Discovery.InstallationFinders.Core;
|
||||
using NitroxModel.Platforms.OS.Windows;
|
||||
using static NitroxModel.Discovery.InstallationFinders.Core.GameFinderResult;
|
||||
|
||||
namespace NitroxModel.Discovery.InstallationFinders;
|
||||
|
||||
/// <summary>
|
||||
/// Trying to find the path in the Steam installation directory by the appid that contains the game installation directory.
|
||||
/// By default each game will have a corresponding appmanifest_{appid}.acf file in the steamapps folder.
|
||||
/// Except for some games that are installed on a different diskdrive, in those case 'libraryfolders.vdf' will give us the real location of the appid folder.
|
||||
/// </summary>
|
||||
public sealed class SteamFinder : IGameFinder
|
||||
{
|
||||
public GameFinderResult FindGame(GameInfo gameInfo)
|
||||
{
|
||||
string steamPath = GetSteamPath();
|
||||
if (string.IsNullOrEmpty(steamPath))
|
||||
{
|
||||
return Error("Steam isn't installed");
|
||||
}
|
||||
|
||||
string path;
|
||||
string appsPath = Path.Combine(steamPath, "steamapps");
|
||||
if (File.Exists(Path.Combine(appsPath, $"appmanifest_{gameInfo.SteamAppId}.acf")))
|
||||
{
|
||||
path = Path.Combine(appsPath, "common", gameInfo.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
path = SearchAllInstallations(Path.Combine(appsPath, "libraryfolders.vdf"), gameInfo.SteamAppId, gameInfo.Name);
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
path = Path.Combine(path, $"{gameInfo.Name}.app", "Contents");
|
||||
}
|
||||
if (!GameInstallationHelper.HasValidGameFolder(path, gameInfo))
|
||||
{
|
||||
return Error($"Path '{path}' known by Steam for '{gameInfo.FullName}' does not point to a valid game file structure");
|
||||
}
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
private static string GetSteamPath()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
string steamPath = RegistryEx.Read<string>(@"Software\Valve\Steam\SteamPath");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(steamPath))
|
||||
{
|
||||
steamPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||||
"Steam"
|
||||
);
|
||||
}
|
||||
|
||||
return Directory.Exists(steamPath) ? steamPath : null;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(homePath))
|
||||
{
|
||||
homePath = Environment.GetEnvironmentVariable("HOME");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(homePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] commonSteamPath =
|
||||
[
|
||||
// Default install location
|
||||
// https://github.com/ValveSoftware/steam-for-linux
|
||||
Path.Combine(homePath, ".local", "share", "Steam"),
|
||||
// Those symlinks are often use as a backward-compatibility (Debian, Ubuntu, Fedora, ArchLinux)
|
||||
// https://wiki.archlinux.org/title/steam, https://askubuntu.com/questions/227502/where-are-steam-games-installed
|
||||
Path.Combine(homePath, ".steam", "steam"),
|
||||
Path.Combine(homePath, ".steam", "root"),
|
||||
// Flatpack install
|
||||
// https://github.com/flathub/com.valvesoftware.Steam/wiki, https://flathub.org/apps/com.valvesoftware.Steam
|
||||
Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"),
|
||||
Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".steam", "steam"),
|
||||
];
|
||||
|
||||
foreach (string path in commonSteamPath)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(homePath))
|
||||
{
|
||||
homePath = Environment.GetEnvironmentVariable("HOME");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(homePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Steam should always be here
|
||||
string steamPath = Path.Combine(homePath, "Library", "Application Support", "Steam");
|
||||
if (Directory.Exists(steamPath))
|
||||
{
|
||||
return steamPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds game install directory by iterating through all the steam game libraries configured, matching the given appid.
|
||||
/// </summary>
|
||||
private static string SearchAllInstallations(string libraryFolders, int appid, string gameName)
|
||||
{
|
||||
if (!File.Exists(libraryFolders))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StreamReader file = new(libraryFolders);
|
||||
char[] trimChars = [' ', '\t'];
|
||||
|
||||
while (file.ReadLine() is { } line)
|
||||
{
|
||||
line = Regex.Unescape(line.Trim(trimChars));
|
||||
Match regMatch = Regex.Match(line, "\"(.*)\"\t*\"(.*)\"");
|
||||
string key = regMatch.Groups[1].Value;
|
||||
|
||||
// New format (about 2021-07-16) uses "path" key instead of steam-library-index as key. If either, it could be steam game path.
|
||||
if (!key.Equals("path", StringComparison.OrdinalIgnoreCase) && !int.TryParse(key, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string value = regMatch.Groups[2].Value;
|
||||
|
||||
if (File.Exists(Path.Combine(value, "steamapps", $"appmanifest_{appid}.acf")))
|
||||
{
|
||||
return Path.Combine(value, "steamapps", "common", gameName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user