first commit
This commit is contained in:
357
NitroxModel/Platforms/Store/Steam.cs
Normal file
357
NitroxModel/Platforms/Store/Steam.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
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"));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user