first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View 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);
}
}

View File

@@ -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
};
}
}

View File

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

View 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();
}
}

View File

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

View 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");
}
}

View 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);
}
}

View 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;
}
}