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,85 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Avalonia.Platform;
namespace Nitrox.Launcher.Models.Utils;
public static class AssetHelper
{
private static readonly string assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? throw new Exception("Unable to get Assembly name");
private static readonly Dictionary<string, Uri> assetPathCache = [];
public static Uri GetFullAssetPath(string assetPath)
{
if (assetPathCache.TryGetValue(assetPath, out Uri fullPath))
{
return fullPath;
}
Uri uri = assetPath.StartsWith("avares://") ? new Uri(assetPath) : new Uri($"avares://{assemblyName}{assetPath}");
if (!AssetLoader.Exists(uri) && !Avalonia.Controls.Design.IsDesignMode)
{
return assetPathCache[assetPath] = default;
}
return assetPathCache[assetPath] = uri;
}
public static T GetAssetFromStream<T>(string assetPath, Func<Stream, T> streamToDataFactory) => AssetLoader<T>.GetFromStream(assetPath, streamToDataFactory);
private static class AssetLoader<T>
{
private static readonly Dictionary<string, T> assetCache = [];
private static readonly Lock assetCacheLock = new();
public static T GetFromStream(string rawUri, Func<Stream, T> streamToDataFactory)
{
T data;
lock (assetCacheLock)
{
if (assetCache.TryGetValue(rawUri, out data))
{
return data;
}
}
// In design mode, resource aren't yet embedded.
if (Avalonia.Controls.Design.IsDesignMode)
{
using Stream stream = File.OpenRead(TryGetPathFromLocalFileSystem(rawUri));
data = streamToDataFactory(stream);
}
if (data == null)
{
using Stream stream = AssetLoader.Open(GetFullAssetPath(rawUri));
data = streamToDataFactory(stream);
}
lock (assetCacheLock)
{
assetCache.Add(rawUri, data);
}
return data;
}
private static string TryGetPathFromLocalFileSystem(string fileUri)
{
string targetedProject = Path.GetDirectoryName(Environment.GetCommandLineArgs().FirstOrDefault(part => !part.Contains("Designer", StringComparison.Ordinal) && part.EndsWith("dll", StringComparison.OrdinalIgnoreCase) && File.Exists(part)));
while (targetedProject != null && !Directory.EnumerateFileSystemEntries(targetedProject, "*.csproj", SearchOption.TopDirectoryOnly).Any())
{
targetedProject = Path.GetDirectoryName(targetedProject);
}
if (targetedProject == null)
{
return null;
}
ReadOnlySpan<char> fileUriSpan = fileUri.AsSpan();
while (fileUriSpan.StartsWith("/") || fileUriSpan.StartsWith("\\"))
{
fileUriSpan = fileUriSpan[1..];
}
return Path.Combine(targetedProject, fileUriSpan.ToString());
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace Nitrox.Launcher.Models.Utils;
public class CacheFile
{
private DateTimeOffset? creationTime;
public string FileName { get; init; }
public string TempFilePath => Path.Combine(Path.GetTempPath(), FileName);
public DateTimeOffset? CreationTime
{
get
{
if (creationTime == null && File.Exists(TempFilePath))
{
using FileStream stream = File.OpenRead(TempFilePath);
if (stream.Length < 8)
{
return null;
}
Span<byte> buffer = stackalloc byte[8];
stream.ReadExactly(buffer);
creationTime = DateTimeOffset.FromUnixTimeSeconds(BitConverter.ToInt64(buffer));
}
return creationTime;
}
}
public CacheFile(string fileName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
FileName = $"nitrox_{fileName.Trim()}.cache";
}
/// <summary>
/// Gets the cached data if not old or refreshes the cache using the <see cref="refreshedValueFactory"/>.
/// </summary>
public static async Task<T> GetOrRefreshAsync<T>(string name, Func<ValueReader, T> reader, Action<BinaryWriter, T> writer, Func<Task<T>> refreshedValueFactory = null, TimeSpan age = default)
{
if (age == default)
{
age = TimeSpan.FromDays(1);
}
CacheFile file = new(name);
if (writer != null && (file.CreationTime == null || DateTimeOffset.UtcNow - file.CreationTime >= age))
{
await using BinaryWriter binaryWriter = new(File.Create(file.TempFilePath));
binaryWriter.Write(BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
T newValue = refreshedValueFactory == null ? default : await refreshedValueFactory();
writer(binaryWriter, newValue);
return newValue;
}
using ValueReader valueReader = new(file.GetStream());
T readerResult = reader(valueReader);
if (valueReader.ReachedEarlyEnd)
{
return refreshedValueFactory == null ? default : await refreshedValueFactory();
}
return readerResult;
}
private BinaryReader GetStream()
{
BinaryReader reader = new(File.OpenRead(TempFilePath));
reader.ReadInt64(); // file creation in unix time
return reader;
}
public class ValueReader : IDisposable
{
private readonly BinaryReader binaryReader;
public bool ReachedEarlyEnd { get; private set; }
public ValueReader(BinaryReader binaryReader)
{
this.binaryReader = binaryReader;
}
public T Read<T>(T defaultValue = default)
{
static T InnerRead<T2>(ValueReader reader, Func<BinaryReader, T2> read, T defaultValue = default)
{
try
{
return (T)(object)read(reader.binaryReader);
}
catch (EndOfStreamException)
{
reader.ReachedEarlyEnd = true;
return defaultValue;
}
catch
{
return defaultValue;
}
}
// Return default values for future reads when end of file (EOF).
if (ReachedEarlyEnd)
{
return defaultValue;
}
Type requestedType = typeof(T);
if (requestedType == typeof(int))
{
return InnerRead(this, reader => reader.ReadInt32(), defaultValue);
}
if (requestedType == typeof(string))
{
return InnerRead(this, reader => reader.ReadString(), defaultValue);
}
if (requestedType == typeof(byte[]))
{
return InnerRead(this, reader =>
{
int dataSize = reader.ReadInt32();
return reader.ReadBytes(dataSize);
}, defaultValue);
}
throw new NotSupportedException($"Type: '{requestedType}' is not yet supported to be read from cache files");
}
public void Dispose() => binaryReader?.Dispose();
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using LitJson;
using Nitrox.Launcher.Models.Design;
using NitroxModel.Logger;
namespace Nitrox.Launcher.Models.Utils;
public partial class Downloader
{
public const string BLOGS_URL = "https://nitroxblog.rux.gg/wp-json/wp/v2/posts?per_page=8&page=1";
public const string LATEST_VERSION_URL = "https://nitrox.rux.gg/api/version/latest";
public const string CHANGELOGS_URL = "https://nitrox.rux.gg/api/changelog/releases";
public const string RELEASES_URL = "https://nitrox.rux.gg/api/version/releases";
[GeneratedRegex(@"""version"":""([^""]*)""")]
private static partial Regex JsonVersionFieldRegex { get; }
public static async Task<IList<NitroxBlog>> GetBlogsAsync()
{
IList<NitroxBlog> blogs = new List<NitroxBlog>();
try
{
string jsonString = await CacheFile.GetOrRefreshAsync("blogs",
r => r.Read(""),
(w, v) => w.Write(v),
async () =>
{
using HttpResponseMessage response = await GetResponseFromCacheAsync(BLOGS_URL);
return await response.Content.ReadAsStringAsync();
});
JsonData data = JsonMapper.ToObject(jsonString);
// TODO : Add a json schema validator
for (int i = 0; i < data.Count; i++)
{
string released = (string)data[i]["date"];
string url = (string)data[i]["link"];
string title = WebUtility.HtmlDecode((string)data[i]["title"]["rendered"]);
string imageUrl = (string)data[i]["jetpack_featured_media_url"];
string imageCacheName = $"blogimage_{title.ReplaceInvalidFileNameCharacters().ToLowerInvariant()}";
if (!DateTimeOffset.TryParse(released, out DateTimeOffset dateTime))
{
dateTime = DateTimeOffset.UtcNow;
Log.Error($"Error while trying to parse release time ({released}) of blog {url}");
}
else
{
imageCacheName = $"blogimage_{dateTime.ToUnixTimeSeconds()}";
}
// Get image bitmap from image URL
byte[] imageData = await CacheFile.GetOrRefreshAsync(imageCacheName,
r => r.Read<byte[]>(),
(w, v) =>
{
w.Write(v.Length);
w.Write(v);
},
async () =>
{
HttpResponseMessage imageResponse = await GetResponseFromCacheAsync(imageUrl);
return await imageResponse.Content.ReadAsByteArrayAsync();
});
using MemoryStream imageMemoryStream = new(imageData);
Bitmap image = new(imageMemoryStream);
blogs.Add(new NitroxBlog(title, DateOnly.FromDateTime(dateTime.DateTime), url, image));
}
}
catch (Exception ex)
{
Log.Error(ex, $"{nameof(Downloader)} : Error while fetching Nitrox blogs from {BLOGS_URL}");
LauncherNotifier.Error("Unable to fetch Nitrox blogs");
}
return blogs;
}
public static async Task<IList<NitroxChangelog>> GetChangeLogsAsync()
{
IList<NitroxChangelog> changelogs = new List<NitroxChangelog>();
try
{
//https://developer.wordpress.org/rest-api/reference/posts/#arguments
string jsonString = await CacheFile.GetOrRefreshAsync("changelogs",
r => r.Read(""),
(w, v) => w.Write(v),
async () =>
{
using HttpResponseMessage response = await GetResponseFromCacheAsync(CHANGELOGS_URL);
return await response.Content.ReadAsStringAsync();
});
StringBuilder builder = new();
JsonData data = JsonMapper.ToObject(jsonString);
// TODO : Add a json schema validator
for (int i = 0; i < data.Count; i++)
{
string version = (string)data[i]["version"];
string released = (string)data[i]["released"];
JsonData patchnotes = data[i]["patchnotes"];
if (!DateTime.TryParse(released, out DateTime dateTime))
{
dateTime = DateTime.UtcNow;
Log.Error($"Error while trying to parse release time ({released}) of Nitrox v{version}");
}
builder.Clear();
for (int j = 0; j < patchnotes.Count; j++)
{
if (patchnotes[j].ToString().StartsWith('-'))
{
builder.AppendLine($"\n[b][u]{patchnotes[j].ToString().TrimStart('-', ' ')}[/u][/b]");
}
else
{
builder.AppendLine($"• {(string)patchnotes[j]}");
}
}
changelogs.Add(new NitroxChangelog(version, dateTime, builder.ToString()));
}
}
catch (Exception ex)
{
Log.Error(ex, $"{nameof(Downloader)} : Error while fetching Nitrox changelogs from {CHANGELOGS_URL}");
LauncherNotifier.Error("Unable to fetch Nitrox changelogs");
}
return changelogs;
}
public static async Task<Version> GetNitroxLatestVersionAsync()
{
try
{
string jsonString = await CacheFile.GetOrRefreshAsync("update",
r => r.Read(""),
(w, v) => w.Write(v),
async () =>
{
using HttpResponseMessage response = await GetResponseFromCacheAsync(LATEST_VERSION_URL);
return await response.Content.ReadAsStringAsync();
});
Match match = JsonVersionFieldRegex.Match(jsonString);
if (match.Success && match.Groups.Count > 1)
{
return new Version(match.Groups[1].Value);
}
}
catch (Exception ex)
{
Log.Error(ex, $"{nameof(Downloader)} : Error while fetching Nitrox version from {LATEST_VERSION_URL}");
LauncherNotifier.Error("Unable to check for Nitrox updates");
throw;
}
return new Version();
}
private static async Task<HttpResponseMessage> GetResponseFromCacheAsync(string url)
{
Log.Info($"Trying to request data from {url}");
using HttpClient client = new();
client.DefaultRequestHeaders.UserAgent.ParseAdd("Nitrox.Launcher");
client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromDays(1) };
client.Timeout = TimeSpan.FromSeconds(5);
try
{
return await client.GetAsync(url);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while requesting data from {url}");
}
return null;
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.IO;
using System.Threading.Tasks;
using HanumanInstitute.MvvmDialogs;
using Nitrox.Launcher.ViewModels;
using NitroxModel.Helper;
using NitroxModel.Logger;
using NitroxModel.Platforms.OS.Shared;
namespace Nitrox.Launcher.Models.Utils;
internal static class GameInspect
{
/// <summary>
/// Check to ensure the Subnautica is not in legacy.
/// </summary>
public static async Task<bool> IsOutdatedGameAndNotify(string gameInstallDir, IDialogService dialogService = null)
{
try
{
ArgumentException.ThrowIfNullOrWhiteSpace(gameInstallDir);
string gameVersionFile = Path.Combine(gameInstallDir, GameInfo.Subnautica.DataFolder, "StreamingAssets", "SNUnmanagedData", "plastic_status.ignore");
if (int.TryParse(await File.ReadAllTextAsync(gameVersionFile), out int gameVersion) && gameVersion <= 68598)
{
if (dialogService != null)
{
await dialogService.ShowAsync<DialogBoxViewModel>(model =>
{
model.Title = "Legacy Game Detected";
model.Description = $"Nitrox does not support the legacy version of {GameInfo.Subnautica.FullName}. Please update your game to the latest version to run {GameInfo.Subnautica.FullName} with Nitrox.{Environment.NewLine}{Environment.NewLine}Version file location:{Environment.NewLine}{gameVersionFile}";
model.ButtonOptions = ButtonOptions.Ok;
});
}
return true;
}
}
catch (Exception ex)
{
Log.Error(ex, "Error while checking game version:");
LauncherNotifier.Debug(ex.Message);
// On error: ignore and assume it's not outdated in case of unforeseen changes. We don't want to block users.
return false;
}
return false;
}
/// <summary>
/// Checks game is running and if it is, warns. Does nothing in development mode for debugging purposes.
/// </summary>
public static bool WarnIfGameProcessExists(GameInfo game)
{
if (!NitroxEnvironment.IsReleaseMode)
{
return false;
}
if (!ProcessEx.ProcessExists(game.Name))
{
return false;
}
LauncherNotifier.Warning($"An instance of {game.FullName} is already running");
return true;
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.Messaging;
using Nitrox.Launcher.Models.Design;
namespace Nitrox.Launcher.Models.Utils;
public static class LauncherNotifier
{
public static void Error(string message)
{
WeakReferenceMessenger.Default.Send(new NotificationAddMessage(new NotificationItem(message, NotificationType.Error)));
}
public static void Info(string message)
{
WeakReferenceMessenger.Default.Send(new NotificationAddMessage(new NotificationItem(message)));
}
public static void Warning(string message)
{
WeakReferenceMessenger.Default.Send(new NotificationAddMessage(new NotificationItem(message, NotificationType.Warning)));
}
public static void Success(string message)
{
WeakReferenceMessenger.Default.Send(new NotificationAddMessage(new NotificationItem(message, NotificationType.Success)));
}
[Conditional("DEBUG")]
public static void Debug(string message, [CallerMemberName] string memberName = "")
{
WeakReferenceMessenger.Default.Send(new NotificationAddMessage(new NotificationItem($"Error in '{memberName}':{Environment.NewLine}{message}", NotificationType.Success)));
}
}

View File

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using NitroxModel.Logger;
using NitroxModel.Platforms.OS.Shared;
namespace Nitrox.Launcher.Models.Utils;
public static class NitroxEntryPatch
{
public const string GAME_ASSEMBLY_NAME = "Assembly-CSharp.dll";
public const string NITROX_ASSEMBLY_NAME = "NitroxPatcher.dll";
public const string GAME_ASSEMBLY_MODIFIED_NAME = "Assembly-CSharp-Nitrox.dll";
private const string NITROX_ENTRY_TYPE_NAME = "Main";
private const string NITROX_ENTRY_METHOD_NAME = "Execute";
private const string GAME_INPUT_TYPE_NAME = "GameInput";
private const string GAME_INPUT_METHOD_NAME = "Awake";
private const string NITROX_EXECUTE_INSTRUCTION = "System.Void NitroxPatcher.Main::Execute()";
/// <summary>
/// Inject Nitrox entry point into Subnautica's Assembly-CSharp.dll
/// </summary>
public static void Apply(string subnauticaBasePath)
{
ArgumentException.ThrowIfNullOrEmpty(subnauticaBasePath, nameof(subnauticaBasePath));
Log.Debug("Adding Nitrox entry point to Subnautica");
string subnauticaManagedPath = Path.Combine(subnauticaBasePath, GameInfo.Subnautica.DataFolder, "Managed");
string assemblyCSharp = Path.Combine(subnauticaManagedPath, GAME_ASSEMBLY_NAME);
string nitroxPatcherPath = Path.Combine(subnauticaManagedPath, NITROX_ASSEMBLY_NAME);
string modifiedAssemblyCSharp = Path.Combine(subnauticaManagedPath, GAME_ASSEMBLY_MODIFIED_NAME);
if (File.Exists(modifiedAssemblyCSharp))
{
// Avoid the case where AssemblyCSharp.dll get wiped and the only file left is AssemblyCSharp-Nitrox.dll
if (!File.Exists(assemblyCSharp))
{
Log.Error($"Invalid state, {GAME_ASSEMBLY_NAME} not found, but {GAME_ASSEMBLY_MODIFIED_NAME} exists. Please verify your installation.");
FileSystem.Instance.ReplaceFile(modifiedAssemblyCSharp, assemblyCSharp);
}
else
{
Log.Debug($"{GAME_ASSEMBLY_MODIFIED_NAME} already exists, removing it");
Exception copyError = RetryWait(() => File.Delete(modifiedAssemblyCSharp), 100, 5);
if (copyError != null)
{
throw copyError;
}
}
}
/*
private void Awake()
{
NitroxPatcher.Main.Execute(); <----------- Insert this line inside subnautica's code
if (GameInput.instance != null)
{
global::UnityEngine.Object.Destroy(base.gameObject);
return;
}
GameInput.instance = this;
GameInput.instance.Initialize();
for (int i = 0; i < GameInput.numDevices; i++)
{
GameInput.SetupDefaultBindings((GameInput.Device)i);
}
DevConsole.RegisterConsoleCommand(this, "debuginput", false, false);
}
*/
// TODO: Find a better way to inject Nitrox entrypoint instead of using file swapping
using (ModuleDefMD module = ModuleDefMD.Load(assemblyCSharp))
using (ModuleDefMD nitroxPatcherAssembly = ModuleDefMD.Load(nitroxPatcherPath))
{
TypeDef nitroxMainDefinition = nitroxPatcherAssembly.GetTypes().FirstOrDefault(x => x.Name == NITROX_ENTRY_TYPE_NAME);
MethodDef executeMethodDefinition = nitroxMainDefinition.Methods.FirstOrDefault(x => x.Name == NITROX_ENTRY_METHOD_NAME);
MemberRef executeMethodReference = module.Import(executeMethodDefinition);
TypeDef gameInputType = module.GetTypes().First(x => x.FullName == GAME_INPUT_TYPE_NAME);
MethodDef awakeMethod = gameInputType.Methods.First(x => x.Name == GAME_INPUT_METHOD_NAME);
Instruction callNitroxExecuteInstruction = OpCodes.Call.ToInstruction(executeMethodReference);
if (awakeMethod.Body.Instructions[0].Operand == callNitroxExecuteInstruction.Operand)
{
Log.Warn("Nitrox entry point already patched.");
return;
}
awakeMethod.Body.Instructions.Insert(0, callNitroxExecuteInstruction);
module.Write(modifiedAssemblyCSharp);
Log.Debug($"Writing assembly to {GAME_ASSEMBLY_MODIFIED_NAME}");
File.SetAttributes(assemblyCSharp, System.IO.FileAttributes.Normal);
}
// The assembly might be used by other code or some other program might work in it. Retry to be on the safe side.
Log.Debug($"Deleting {GAME_ASSEMBLY_NAME}");
Exception error = RetryWait(() => File.Delete(assemblyCSharp), 100, 5);
if (error != null)
{
throw error;
}
FileSystem.Instance.ReplaceFile(modifiedAssemblyCSharp, assemblyCSharp);
Log.Debug("Added Nitrox entry point to Subnautica");
}
/// <summary>
/// Remote Nitrox entry point from Subnautica's Assembly-CSharp.dll
/// </summary>
public static void Remove(string subnauticaBasePath)
{
ArgumentException.ThrowIfNullOrEmpty(subnauticaBasePath, nameof(subnauticaBasePath));
Log.Debug("Removing Nitrox entry point from Subnautica");
string subnauticaManagedPath = Path.Combine(subnauticaBasePath, GameInfo.Subnautica.DataFolder, "Managed");
string assemblyCSharp = Path.Combine(subnauticaManagedPath, GAME_ASSEMBLY_NAME);
string modifiedAssemblyCSharp = Path.Combine(subnauticaManagedPath, GAME_ASSEMBLY_MODIFIED_NAME);
using (ModuleDefMD module = ModuleDefMD.Load(assemblyCSharp))
{
TypeDef gameInputType = module.GetTypes().First(x => x.FullName == GAME_INPUT_TYPE_NAME);
MethodDef awakeMethod = gameInputType.Methods.First(x => x.Name == GAME_INPUT_METHOD_NAME);
IList<Instruction> methodInstructions = awakeMethod.Body.Instructions;
int nitroxExecuteInstructionIndex = FindNitroxExecuteInstructionIndex(methodInstructions);
if (nitroxExecuteInstructionIndex == -1)
{
Log.Debug($"Nitrox entry point not found in {GAME_INPUT_TYPE_NAME}:{GAME_INPUT_METHOD_NAME}");
return;
}
methodInstructions.RemoveAt(nitroxExecuteInstructionIndex);
module.Write(modifiedAssemblyCSharp);
File.SetAttributes(assemblyCSharp, System.IO.FileAttributes.Normal);
}
FileSystem.Instance.ReplaceFile(modifiedAssemblyCSharp, assemblyCSharp);
Log.Debug("Removed Nitrox entry point from Subnautica");
}
private static int FindNitroxExecuteInstructionIndex(IList<Instruction> methodInstructions)
{
for (int instructionIndex = 0; instructionIndex < methodInstructions.Count; instructionIndex++)
{
string instruction = methodInstructions[instructionIndex].Operand?.ToString();
if (instruction == NITROX_EXECUTE_INSTRUCTION)
{
return instructionIndex;
}
}
return -1;
}
private static Exception RetryWait(Action action, int interval, int retries = 0)
{
Exception lastException = null;
while (retries >= 0)
{
try
{
retries--;
action();
return null;
}
catch (Exception ex)
{
lastException = ex;
Task.Delay(interval).Wait();
}
}
return lastException;
}
public static bool IsPatchApplied(string subnauticaBasePath)
{
string subnauticaManagedPath = Path.Combine(subnauticaBasePath, GameInfo.Subnautica.DataFolder, "Managed");
string gameInputPath = Path.Combine(subnauticaManagedPath, GAME_ASSEMBLY_NAME);
using (ModuleDefMD module = ModuleDefMD.Load(gameInputPath))
{
TypeDef gameInputType = module.GetTypes().First(x => x.FullName == GAME_INPUT_TYPE_NAME);
MethodDef awakeMethod = gameInputType.Methods.First(x => x.Name == GAME_INPUT_METHOD_NAME);
return awakeMethod.Body.Instructions[0]?.ToString() == NITROX_EXECUTE_INSTRUCTION;
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using NitroxModel.Helper;
namespace Nitrox.Launcher.Models.Utils;
public static class ProcessUtils
{
public static Process StartProcessDetached(ProcessStartInfo startInfo)
{
if (!string.IsNullOrWhiteSpace(startInfo.Arguments))
{
throw new NotSupportedException($"Arguments must be supplied via {startInfo.ArgumentList}");
}
// On Linux, processes are started as child by default. So we wrap as shell command to start detached from current process.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
List<string> newArgs = ["-c", string.Join(" ", ["nohup", $"'{startInfo.FileName}'", string.Join(" ", startInfo.ArgumentList), ">/dev/null 2>&1", "&"])];
startInfo.FileName = "/bin/sh";
startInfo.ArgumentList.Clear();
startInfo.ArgumentList.AddRange(newArgs);
}
return Process.Start(startInfo);
}
/// <summary>
/// Starts the current app as a new instance.
/// </summary>
public static void StartSelf(params string[] arguments)
{
string executableFilePath = NitroxUser.ExecutableFilePath ?? Environment.ProcessPath;
// On Linux, entry assembly is .dll file but real executable is without extension.
string temp = Path.ChangeExtension(executableFilePath, null);
if (File.Exists(temp))
{
executableFilePath = temp;
}
temp = Path.ChangeExtension(executableFilePath, ".exe");
if (File.Exists(temp))
{
executableFilePath = temp;
}
if (arguments.Contains("--allow-instances"))
{
arguments = [..arguments, "--allow-instances"];
}
using Process proc = StartProcessDetached(new ProcessStartInfo(executableFilePath!, arguments));
}
/// <summary>
/// Opens the Url in the default browser. Forces the Uri scheme as Https.
/// </summary>
public static void OpenUrl(string url)
{
UriBuilder urlBuilder = new(url) { Scheme = Uri.UriSchemeHttps, Port = -1 };
using Process proc = Process.Start(new ProcessStartInfo
{
FileName = urlBuilder.Uri.ToString(),
UseShellExecute = true,
Verb = "open"
});
}
}

View File

@@ -0,0 +1,12 @@
using System.IO;
namespace Nitrox.Launcher.Models.Utils;
internal static class QModHelper
{
internal static bool IsQModInstalled(string subnauticaBasePath)
{
string subnauticaQModManagerPath = Path.Combine(subnauticaBasePath, "Bepinex", "plugins", "QModManager");
return Directory.Exists(subnauticaQModManagerPath);
}
}