first commit
This commit is contained in:
85
Nitrox.Launcher/Models/Utils/AssetHelper.cs
Normal file
85
Nitrox.Launcher/Models/Utils/AssetHelper.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
131
Nitrox.Launcher/Models/Utils/CacheFile.cs
Normal file
131
Nitrox.Launcher/Models/Utils/CacheFile.cs
Normal 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();
|
||||
}
|
||||
}
|
194
Nitrox.Launcher/Models/Utils/Downloader.cs
Normal file
194
Nitrox.Launcher/Models/Utils/Downloader.cs
Normal 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;
|
||||
}
|
||||
}
|
67
Nitrox.Launcher/Models/Utils/GameInspect.cs
Normal file
67
Nitrox.Launcher/Models/Utils/GameInspect.cs
Normal 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;
|
||||
}
|
||||
}
|
37
Nitrox.Launcher/Models/Utils/LauncherNotifier.cs
Normal file
37
Nitrox.Launcher/Models/Utils/LauncherNotifier.cs
Normal 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)));
|
||||
}
|
||||
}
|
202
Nitrox.Launcher/Models/Utils/NitroxEntryPatch.cs
Normal file
202
Nitrox.Launcher/Models/Utils/NitroxEntryPatch.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
70
Nitrox.Launcher/Models/Utils/ProcessUtils.cs
Normal file
70
Nitrox.Launcher/Models/Utils/ProcessUtils.cs
Normal 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"
|
||||
});
|
||||
}
|
||||
}
|
12
Nitrox.Launcher/Models/Utils/QModHelper.cs
Normal file
12
Nitrox.Launcher/Models/Utils/QModHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user