358 lines
14 KiB
C#
358 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NitroxModel.Discovery.Models;
|
|
using NitroxModel.Helper;
|
|
using NitroxModel.Platforms.OS.Shared;
|
|
using NitroxModel.Platforms.OS.Windows;
|
|
using NitroxModel.Platforms.Store.Exceptions;
|
|
using NitroxModel.Platforms.Store.Interfaces;
|
|
|
|
namespace NitroxModel.Platforms.Store;
|
|
|
|
public sealed class Steam : IGamePlatform
|
|
{
|
|
public string Name => nameof(Steam);
|
|
public Platform Platform => Platform.STEAM;
|
|
|
|
private string SteamProcessName => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "steam_osx" : "steam";
|
|
|
|
public bool OwnsGame(string gameRootPath) =>
|
|
gameRootPath switch
|
|
{
|
|
not null when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => Directory.Exists(Path.Combine(gameRootPath, "Plugins", "steam_api.bundle")),
|
|
not null when File.Exists(Path.Combine(gameRootPath, GameInfo.Subnautica.DataFolder, "Plugins", "x86_64", "steam_api64.dll")) => true,
|
|
not null when File.Exists(Path.Combine(gameRootPath, GameInfo.Subnautica.DataFolder, "Plugins", "steam_api64.dll")) => true,
|
|
_ => false
|
|
};
|
|
|
|
public async Task<ProcessEx> StartPlatformAsync()
|
|
{
|
|
// If steam is already running, do not start it.
|
|
ProcessEx steam = ProcessEx.GetFirstProcess(SteamProcessName);
|
|
if (steam is not null)
|
|
{
|
|
return steam;
|
|
}
|
|
|
|
// Steam is not running, start it.
|
|
string exe = GetExeFile();
|
|
if (exe is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string launchExe = exe;
|
|
string launchArgs = "-silent"; // Don't show Steam window
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
launchExe = "/bin/sh";
|
|
launchArgs = $@"-c ""nohup '{exe}' {launchArgs}"" &";
|
|
}
|
|
Stopwatch steamReadyStopwatch = Stopwatch.StartNew();
|
|
Process process = Process.Start(new ProcessStartInfo
|
|
{
|
|
WorkingDirectory = Path.GetDirectoryName(exe) ?? Directory.GetCurrentDirectory(),
|
|
FileName = launchExe,
|
|
WindowStyle = ProcessWindowStyle.Minimized,
|
|
UseShellExecute = true,
|
|
Arguments = launchArgs
|
|
});
|
|
|
|
if (process is not { HasExited: false })
|
|
{
|
|
return null;
|
|
}
|
|
|
|
steam = new ProcessEx(process);
|
|
// Wait for steam to write to its log file, which indicates it's ready to start games.
|
|
using CancellationTokenSource steamReadyCts = new(TimeSpan.FromSeconds(30));
|
|
try
|
|
{
|
|
DateTime consoleLogFileLastWrite = GetSteamConsoleLogLastWrite(exe);
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
await RegistryEx.CompareWaitAsync<int>(@"SOFTWARE\Valve\Steam\ActiveProcess\ActiveUser",
|
|
v => v > 0,
|
|
steamReadyCts.Token);
|
|
}
|
|
Log.Debug("Waiting for Steam to get ready...");
|
|
while (consoleLogFileLastWrite == GetSteamConsoleLogLastWrite(exe) && !steamReadyCts.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(250, steamReadyCts.Token);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// ignored
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex);
|
|
}
|
|
Log.Debug($"Steam wait result: {(steamReadyCts.IsCancellationRequested ? "timed out" : $"startup successful and took about {steamReadyStopwatch.Elapsed.TotalSeconds}s")}");
|
|
|
|
return steam;
|
|
}
|
|
|
|
public string GetExeFile()
|
|
{
|
|
string steamExecutable = "";
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
steamExecutable = Path.Combine(RegistryEx.Read(@"SOFTWARE\Valve\Steam\SteamPath", steamExecutable), "steam.exe");
|
|
}
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
steamExecutable = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Steam", "Steam.AppBundle", "Steam", "Contents", "MacOS", "steam_osx");
|
|
}
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
string userHomePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
if (!Directory.Exists(userHomePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string steamPath = Path.Combine(userHomePath, ".steam", "steam");
|
|
// support flatpak
|
|
if (!Directory.Exists(steamPath))
|
|
{
|
|
steamPath = Path.Combine(userHomePath, ".var", "app", "com.valvesoftware.Steam", "data", "Steam");
|
|
}
|
|
|
|
steamExecutable = Path.Combine(steamPath, "steam.sh");
|
|
}
|
|
|
|
return File.Exists(steamExecutable) ? Path.GetFullPath(steamExecutable) : null;
|
|
}
|
|
|
|
public async Task<ProcessEx> StartGameAsync(string pathToGameExe, string launchArguments, int steamAppId)
|
|
{
|
|
try
|
|
{
|
|
using ProcessEx steam = await StartPlatformAsync();
|
|
if (steam == null)
|
|
{
|
|
throw new GamePlatformException(this, "Platform is not running and could not be found.");
|
|
}
|
|
}
|
|
catch (OperationCanceledException ex)
|
|
{
|
|
throw new GamePlatformException(this, "Timeout reached while waiting for platform to start. Try again once platform has finished loading.", ex);
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
#if DEBUG // Needed to start multiple SN instances, but Steam Overlay doesn't work this way so only active for devs
|
|
return ProcessEx.Start(
|
|
pathToGameExe,
|
|
[("SteamGameId", steamAppId.ToString()), ("SteamAppID", steamAppId.ToString()), (NitroxUser.LAUNCHER_PATH_ENV_KEY, NitroxUser.LauncherPath)],
|
|
Path.GetDirectoryName(pathToGameExe),
|
|
launchArguments
|
|
);
|
|
#else
|
|
return new ProcessEx(Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = GetExeFile(),
|
|
Arguments = $"""-applaunch {steamAppId} --nitrox "{NitroxUser.LauncherPath}" {launchArguments}"""
|
|
}));
|
|
#endif
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
string steamPath = Path.GetDirectoryName(GetExeFile());
|
|
return StartGameWithProton(steamPath, pathToGameExe, steamAppId, launchArguments);
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
return ProcessEx.Start(
|
|
pathToGameExe,
|
|
[("SteamGameId", steamAppId.ToString()), ("SteamAppID", steamAppId.ToString()), (NitroxUser.LAUNCHER_PATH_ENV_KEY, NitroxUser.LauncherPath)],
|
|
Path.GetDirectoryName(pathToGameExe),
|
|
launchArguments
|
|
);
|
|
}
|
|
|
|
throw new NotSupportedException("Your operating system is not supported by Nitrox");
|
|
}
|
|
|
|
private static ProcessEx StartGameWithProton(string steamPath, string pathToGameExe, int steamAppId, string launchArguments)
|
|
{
|
|
// function to get library path for given game id
|
|
static string GetLibraryPath(string steamPath, string gameId)
|
|
{
|
|
string libraryFoldersPath = Path.Combine(steamPath, "config", "libraryfolders.vdf");
|
|
string content = File.ReadAllText(libraryFoldersPath);
|
|
|
|
// Regex to match library folder entries
|
|
Regex folderRegex = new(@"""(\d+)""\s*\{[^}]*""path""\s*""([^""]+)""[^}]*""apps""\s*\{([^}]+)\}", RegexOptions.Singleline);
|
|
MatchCollection matches = folderRegex.Matches(content);
|
|
|
|
foreach (Match match in matches)
|
|
{
|
|
string path = match.Groups[2].Value;
|
|
string apps = match.Groups[3].Value;
|
|
|
|
// Check if the gameId exists in the apps section
|
|
if (Regex.IsMatch(apps, $@"""{gameId}""\s*""[^""]+"""))
|
|
{
|
|
return path;
|
|
}
|
|
}
|
|
|
|
return ""; // Return empty string if not found
|
|
}
|
|
|
|
static List<string> GetAllLibraryPaths(string steamPath)
|
|
{
|
|
string libraryFoldersPath = Path.Combine(steamPath, "config", "libraryfolders.vdf");
|
|
string content = File.ReadAllText(libraryFoldersPath);
|
|
|
|
// Regex to match library folder entries
|
|
Regex folderRegex = new(@"""(\d+)""\s*\{[^}]*""path""\s*""([^""]+)""", RegexOptions.Singleline);
|
|
MatchCollection matches = folderRegex.Matches(content);
|
|
|
|
List<string> libraryPaths = [];
|
|
foreach (Match match in matches)
|
|
{
|
|
string path = match.Groups[2].Value.Replace("\\\\", "\\");
|
|
if (Directory.Exists(Path.Combine(path, "steamapps", "common")))
|
|
{
|
|
libraryPaths.Add(path);
|
|
}
|
|
}
|
|
// Add the default Steam library path
|
|
string defaultLibraryPath = Path.Combine(steamPath);
|
|
if (!libraryPaths.Contains(defaultLibraryPath))
|
|
{
|
|
libraryPaths.Add(defaultLibraryPath);
|
|
}
|
|
|
|
return libraryPaths;
|
|
}
|
|
|
|
static string GetProtonVersionFromConfigVdf(string configVdfFile, string appId)
|
|
{
|
|
try
|
|
{
|
|
string fileContent = File.ReadAllText(configVdfFile);
|
|
Match compatToolMatch = Regex.Match(fileContent, @"""CompatToolMapping""\s*{((?:\s*""\d+""[^{]+[^}]+})*)\s*}");
|
|
|
|
if (compatToolMatch.Success)
|
|
{
|
|
string compatToolMapping = compatToolMatch.Groups[1].Value;
|
|
string appIdPattern = $@"""{appId}""[^{{]*\{{[^}}]*""name""\s*""([^""]+)""";
|
|
Match appIdMatch = Regex.Match(compatToolMapping, appIdPattern);
|
|
|
|
if (appIdMatch.Success)
|
|
{
|
|
return appIdMatch.Groups[1].Value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Debug(ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
string compatdataPath = "";
|
|
if (!string.IsNullOrEmpty(pathToGameExe))
|
|
{
|
|
string[] pathComponents = pathToGameExe.Split(Path.DirectorySeparatorChar);
|
|
int steamAppsIndex = pathComponents.GetIndex("steamapps");
|
|
if (steamAppsIndex != -1)
|
|
{
|
|
string steamAppsPath = string.Join(Path.DirectorySeparatorChar.ToString(), pathComponents, 0, steamAppsIndex + 1);
|
|
compatdataPath = Path.Combine(steamAppsPath, "compatdata", steamAppId.ToString());
|
|
}
|
|
}
|
|
|
|
string sniperappid = "1628350";
|
|
string sniperruntimepath = Path.Combine(GetLibraryPath(steamPath, sniperappid), "steamapps", "common", "SteamLinuxRuntime_sniper");
|
|
|
|
string protonPath = null;
|
|
string protonRoot = Path.Combine(steamPath, "compatibilitytools.d");
|
|
string protonVersion = GetProtonVersionFromConfigVdf(Path.Combine(steamPath, "config", "config.vdf"), steamAppId.ToString()) ?? "proton_9";
|
|
bool isValveProton = protonVersion.StartsWith("proton_", StringComparison.OrdinalIgnoreCase);
|
|
if (isValveProton)
|
|
{
|
|
int index = protonVersion.IndexOf("proton_", StringComparison.OrdinalIgnoreCase);
|
|
if (index != -1)
|
|
{
|
|
protonVersion = protonVersion[(index + "proton_".Length)..];
|
|
}
|
|
if (protonVersion == "experimental")
|
|
{
|
|
protonVersion = "-";
|
|
}
|
|
|
|
foreach (string path in GetAllLibraryPaths(steamPath))
|
|
{
|
|
foreach (string dir in Directory.EnumerateDirectories(Path.Combine(path, "steamapps", "common")))
|
|
{
|
|
if (dir.Contains($"Proton {protonVersion}"))
|
|
{
|
|
protonPath = dir;
|
|
break;
|
|
}
|
|
}
|
|
if (protonPath != null)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
protonPath = Path.Combine(protonRoot, protonVersion);
|
|
}
|
|
if (protonPath == null)
|
|
{
|
|
throw new Exception("Game is not using Proton. Please change game properties in Steam to use the Proton compatibility layer.");
|
|
}
|
|
|
|
ProcessStartInfo startInfo = new()
|
|
{
|
|
FileName = Path.Combine(sniperruntimepath, "_v2-entry-point"),
|
|
Arguments = $" --verb=run -- \"{Path.Combine(protonPath, "proton")}\" run \"{pathToGameExe}\" {launchArguments}",
|
|
WorkingDirectory = Path.GetDirectoryName(pathToGameExe) ?? "",
|
|
UseShellExecute = false,
|
|
Environment =
|
|
{
|
|
[NitroxUser.LAUNCHER_PATH_ENV_KEY] = NitroxUser.LauncherPath,
|
|
["SteamGameId"] = steamAppId.ToString(),
|
|
["SteamAppID"] = steamAppId.ToString(),
|
|
["STEAM_COMPAT_APP_ID"] = steamAppId.ToString(),
|
|
["WINEPREFIX"] = compatdataPath,
|
|
["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = steamPath,
|
|
["STEAM_COMPAT_DATA_PATH"] = compatdataPath,
|
|
}
|
|
};
|
|
return new ProcessEx(Process.Start(startInfo));
|
|
}
|
|
|
|
private static DateTime GetSteamConsoleLogLastWrite(string steamExePath)
|
|
{
|
|
string steamLogsPath = steamExePath switch
|
|
{
|
|
not null when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Steam", "logs"),
|
|
not null when Path.GetDirectoryName(steamExePath) is { } steamPath => Path.Combine(steamPath, "logs"),
|
|
_ => ""
|
|
};
|
|
return File.GetLastWriteTime(Path.Combine(steamLogsPath, "console_log.txt"));
|
|
}
|
|
}
|