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,30 @@
using System.Globalization;
using System.Threading;
namespace NitroxModel.Helper;
public static class CultureManager
{
public static readonly CultureInfo CultureInfo = new("en-US");
/// <summary>
/// Internal Subnautica files are setup using US english number formats and dates. To ensure
/// that we parse all of these appropriately, we will set the default cultureInfo to en-US.
/// This must best done for any thread that is spun up and needs to read from files (unless
/// we were to migrate to 4.5.) Failure to set the context can result in very strange behaviour
/// throughout the entire application. This originally manifested itself as a duplicate spawning
/// issue for players in Europe. This was due to incorrect parsing of probability tables.
/// </summary>
public static void ConfigureCultureInfo()
{
// Although we loaded the en-US cultureInfo, let's make sure to set these in case the
// default was overriden by the user.
CultureInfo.NumberFormat.NumberDecimalSeparator = ".";
CultureInfo.NumberFormat.NumberGroupSeparator = ",";
Thread.CurrentThread.CurrentCulture = CultureInfo;
Thread.CurrentThread.CurrentUICulture = CultureInfo;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo;
}
}

View File

@@ -0,0 +1,34 @@
namespace NitroxModel.Helper;
public interface IKeyValueStore
{
/// <summary>
/// Gets a value for a key.
/// </summary>
/// <param name="key">Key to get value of.</param>
/// <param name="defaultValue">Default value to return if key does not exist or type conversion failed.</param>
/// <returns>The value or null if key was not found or conversion failed.</returns>
T GetValue<T>(string key, T defaultValue = default);
/// <summary>
/// Sets a value for a key.
/// </summary>
/// <param name="key">Key to set value of.</param>
/// <param name="value">Value to set for the key.</param>
/// <returns>True if the value was found.</returns>
bool SetValue<T>(string key, T value);
/// <summary>
/// Deletes a key along with its value.
/// </summary>
/// <param name="key">Key to delete.</param>
/// <returns>True if the key was deleted.</returns>
bool DeleteKey(string key);
/// <summary>
/// Check if a key exists.
/// </summary>
/// <param name="key">Key to check.</param>
/// <returns>True if the key exists.</returns>
bool KeyExists(string key);
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
namespace NitroxModel.Helper
{
public interface IMap
{
public int BatchSize { get; }
/// <summary>
/// AKA LargeWorldStreamer.blocksPerBatch
/// </summary>
public NitroxInt3 BatchDimensions { get; }
public NitroxInt3 DimensionsInMeters { get; }
public NitroxInt3 DimensionsInBatches { get; }
public NitroxInt3 BatchDimensionCenter { get; }
public List<NitroxTechType> GlobalRootTechTypes { get; }
public int ItemLevelOfDetail { get; }
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Runtime.InteropServices;
using NitroxModel.Platforms.OS.Shared;
using NitroxModel.Platforms.OS.Windows;
namespace NitroxModel.Helper;
/// <summary>
/// Simple Key-Value store that works cross-platform. <br />
/// </summary>
/// <remarks>
/// <para>
/// On <b>Windows</b>:<br />
/// Backend is <see cref="RegistryKeyValueStore" />, which uses the registry.
/// If you want to view/edit the KeyStore, open regedit and navigate to HKEY_CURRENT_USER\SOFTWARE\Nitrox\(keyname)
/// </para>
/// <para>
/// On <b>Linux</b>:<br />
/// Backend is <see cref="ConfigFileKeyValueStore" />, which uses a file.
/// If you want to view/edit the KeyStore, open $HOME/.config/Nitrox/nitrox.cfg in your favourite text editor.
/// </para>
/// </remarks>
public static class KeyValueStore
{
public static IKeyValueStore Instance { get; } = GetKeyValueStoreForPlatform();
private static IKeyValueStore GetKeyValueStoreForPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Use registry on Windows
return new RegistryKeyValueStore();
}
// if platform isn't Windows, it doesn't have a registry
// use a config file for storage this should work on most platforms
return new ConfigFileKeyValueStore();
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace NitroxModel.Helper;
public static class LinqExtensions
{
/// <summary>
/// Returns the items until the predicate stops matching, then includes the next non-matching result as well.
/// </summary>
/// <param name="source">Input enumerable.</param>
/// <param name="predicate">Predicate to match against the items in the enumerable.</param>
/// <typeparam name="T">Type of items to return.</typeparam>
/// <returns></returns>
public static IEnumerable<T> TakeUntilInclusive<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
{
if (predicate(item))
{
yield return item;
}
else
{
// Also self
yield return item;
yield break;
}
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
namespace NitroxModel.Helper
{
public class Mathf
{
public const float RAD2DEG = 57.29578f;
public const float PI = 3.14159274f;
public const float DEG2RAD = 0.0174532924f;
public static float Sqrt(float ls)
{
return (float)Math.Sqrt(ls);
}
public static float Atan2(float p1, float p2)
{
return (float)Math.Atan2(p1, p2);
}
public static float Asin(float p)
{
return (float)Math.Asin(p);
}
public static float Pow(float p1, float p2)
{
return (float)Math.Pow(p1, p2);
}
/// <summary>
/// Clamps the given value between 0 and 1.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static float Clamp01(float value)
{
// Not using Clamp as an optimization.
if (value < 0)
{
return 0;
}
if (value > 1)
{
return 1;
}
return value;
}
public static T Clamp<T>(T val, T min, T max) where T : IComparable<T>
{
if (val.CompareTo(min) < 0)
{
return min;
}
if (val.CompareTo(max) > 0)
{
return max;
}
return val;
}
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="digits" /> is less than 0 or greater than 15.</exception>
public static float Round(float value, int digits = 0)
{
return (float)Math.Round(value, digits);
}
public static float Lerp(float a, float b, float t)
{
return a + (b - a) * t;
}
/// <summary>
/// Reciprocal function of <see cref="Lerp"/>. Unlerp(a, b, Lerp(a, b, t)) = t
/// </summary>
public static float Unlerp(float a, float b, float lerpedResult)
{
return (lerpedResult - a) / (b - a);
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Numerics;
using NitroxModel.DataStructures.Unity;
namespace NitroxModel.Helper;
// ReSharper disable once InconsistentNaming
public static class Matrix4x4Extension
{
public static Matrix4x4 Compose(NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 localScale)
{
Matrix4x4 translationMatrix = Matrix4x4.CreateTranslation(localPosition.X, localPosition.Y, localPosition.Z);
Matrix4x4 rotationMatrix = Matrix4x4.CreateFromQuaternion((Quaternion)localRotation);
Matrix4x4 scaleMatrix = Matrix4x4.CreateScale(localScale.X, localScale.Y, localScale.Z);
return scaleMatrix * rotationMatrix * translationMatrix;
}
public static Matrix4x4 Invert(this Matrix4x4 m)
{
Matrix4x4.Invert(m, out Matrix4x4 result);
return result;
}
public static NitroxVector3 Transform(this Matrix4x4 m, NitroxVector3 v)
{
float x = v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31 + m.M41;
float y = v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32 + m.M42;
float z = v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33 + m.M43;
float w = v.X * m.M14 + v.Y * m.M24 + v.Z * m.M34 + m.M44;
if (w == 0)
{
throw new ArithmeticException($"Tried to divide by zero with Matrix {m} and vector {v}");
}
w = 1f / w;
return new NitroxVector3(x * w, y * w, z * w);
}
}

View File

@@ -0,0 +1,216 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Mono.Nat;
namespace NitroxModel.Helper;
public static class NatHelper
{
public static async Task<IPAddress> GetExternalIpAsync() => await MonoNatHelper.GetFirstAsync(static async device =>
{
try
{
return await device.GetExternalIPAsync().ConfigureAwait(false);
}
catch (Exception)
{
return null;
}
}).ConfigureAwait(false);
public static async Task<bool> DeletePortMappingAsync(ushort port, Protocol protocol, CancellationToken ct = default)
{
int tries = 3;
while (tries-- >= 0)
{
if (await TryRemoveAsync(port, protocol, ct))
{
return true;
}
await Task.Delay(250, ct);
}
return false;
static async Task<bool> TryRemoveAsync(ushort port, Protocol protocol, CancellationToken ct)
{
return await MonoNatHelper.GetFirstAsync(static async (device, mapping) =>
{
try
{
return await device.DeletePortMapAsync(mapping).ConfigureAwait(false) != null;
}
catch (MappingException)
{
return false;
}
}, new Mapping(protocol, port, port), ct).ConfigureAwait(false);
}
}
public static async Task<Mapping> GetPortMappingAsync(ushort port, Protocol protocol, CancellationToken ct = default)
{
return await MonoNatHelper.GetFirstAsync(static async (device, protocolAndPort) =>
{
try
{
return await device.GetSpecificMappingAsync(protocolAndPort.protocol, protocolAndPort.port).ConfigureAwait(false);
}
catch (Exception)
{
return null;
}
}, (port, protocol), ct).ConfigureAwait(false);
}
public static async Task<ResultCodes> AddPortMappingAsync(ushort port, Protocol protocol, CancellationToken ct = default)
{
Mapping mapping = new(protocol, port, port);
return await MonoNatHelper.GetFirstAsync(static async (device, mapping) =>
{
try
{
return await device.CreatePortMapAsync(mapping).ConfigureAwait(false) != null ? ResultCodes.SUCCESS : ResultCodes.UNKNOWN_ERROR;
}
catch (MappingException ex)
{
return ExceptionToCode(ex);
}
}, mapping, ct).ConfigureAwait(false);
}
public enum ResultCodes
{
SUCCESS,
CONFLICT_IN_MAPPING_ENTRY,
UNKNOWN_ERROR
}
private static ResultCodes ExceptionToCode(MappingException exception) => exception.ErrorCode switch
{
ErrorCode.ConflictInMappingEntry => ResultCodes.CONFLICT_IN_MAPPING_ENTRY,
_ => ResultCodes.UNKNOWN_ERROR
};
private static class MonoNatHelper
{
private static readonly ConcurrentDictionary<EndPoint, INatDevice> discoveredDevices = new();
private static readonly object discoverTaskLocker = new();
private static Task<IEnumerable<INatDevice>> discoverTaskCache;
public static Task<IEnumerable<INatDevice>> DiscoverAsync()
{
// Singleton discovery task. Same task is reused to cache the result of the discovery broadcast.
lock (discoverTaskLocker)
{
if (discoverTaskCache != null)
{
return discoverTaskCache;
}
return discoverTaskCache = DiscoveryUncachedAsync(60000, 5000);
}
}
private static DateTime lastFoundDeviceTime;
private static readonly object lastFoundDeviceTimeLock = new();
private static async Task<IEnumerable<INatDevice>> DiscoveryUncachedAsync(int timeoutInMs, int timeoutNoMoreDevicesMs)
{
void Handler(object sender, DeviceEventArgs args)
{
lock (lastFoundDeviceTimeLock)
{
lastFoundDeviceTime = DateTime.UtcNow;
}
discoveredDevices.TryAdd(args.Device.DeviceEndpoint, args.Device);
}
NatUtility.DeviceFound += Handler;
NatUtility.StartDiscovery();
try
{
CancellationTokenSource cancellation = new(timeoutInMs);
lock (lastFoundDeviceTimeLock)
{
lastFoundDeviceTime = DateTime.UtcNow;
}
bool hasFoundDeviceRecently = true;
while (!cancellation.IsCancellationRequested && hasFoundDeviceRecently)
{
lock (lastFoundDeviceTimeLock)
{
hasFoundDeviceRecently = (DateTime.UtcNow - lastFoundDeviceTime).TotalMilliseconds <= timeoutNoMoreDevicesMs;
}
await Task.Delay(10, cancellation.Token).ConfigureAwait(false);
}
}
finally
{
NatUtility.StopDiscovery();
NatUtility.DeviceFound -= Handler;
}
return discoveredDevices.Values;
}
public static async Task<TResult> GetFirstAsync<TResult>(Func<INatDevice, Task<TResult>> predicate) => await GetFirstAsync(static (device, p) => p(device), predicate);
public static async Task<TResult> GetFirstAsync<TResult, TExtraParam>(Func<INatDevice, TExtraParam, Task<TResult>> predicate, TExtraParam parameter, CancellationToken ct = default)
{
if (ct.IsCancellationRequested)
{
return default;
}
// Start NAT discovery (if it hasn't started yet).
Task<IEnumerable<INatDevice>> discoverTask = DiscoverAsync();
if (discoverTask.IsCompleted && discoveredDevices.IsEmpty)
{
return default;
}
// Progressively handle devices until first not-null/false result or when discovery times out.
ConcurrentDictionary<EndPoint, INatDevice> handledDevices = new();
do
{
IEnumerable<KeyValuePair<EndPoint, INatDevice>> unhandledDevices = discoveredDevices.Except(handledDevices).ToArray();
if (!unhandledDevices.Any())
{
try
{
await Task.Delay(10, ct);
}
catch (OperationCanceledException)
{
// ignored
}
continue;
}
foreach (KeyValuePair<EndPoint, INatDevice> pair in unhandledDevices)
{
if (ct.IsCancellationRequested)
{
return default;
}
if (handledDevices.TryAdd(pair.Key, pair.Value))
{
TResult result = await predicate(pair.Value, parameter);
if (result is true or not null)
{
return result;
}
}
}
} while (!ct.IsCancellationRequested && !discoverTask.IsCompleted);
return default;
}
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
#if RELEASE
using System.Net.Http;
using System.Text.RegularExpressions;
#endif
namespace NitroxModel.Helper
{
public static class NetHelper
{
private static readonly string[] privateNetworks =
{
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.0.0.0/24 ",
"192.168.0.0/16",
"198.18.0.0/15",
};
private static IPAddress wanIpCache;
private static IPAddress lanIpCache;
private static readonly object wanIpLock = new();
private static readonly object lanIpLock = new();
/// <summary>
/// Gets the network interfaces used for going onto the internet.
/// This is done by filtering for "Ethernet" and "Wi-Fi" network interfaces where "Ethernet" is returned earlier.
/// </summary>
/// <returns>Network interfaces used to go onto the internet.</returns>
public static IEnumerable<NetworkInterface> GetInternetInterfaces()
{
return NetworkInterface.GetAllNetworkInterfaces()
.Where(n => n.OperationalStatus is OperationalStatus.Up
&& n.NetworkInterfaceType is not (NetworkInterfaceType.Tunnel or NetworkInterfaceType.Loopback)
&& n.NetworkInterfaceType is (NetworkInterfaceType.Wireless80211 or NetworkInterfaceType.Ethernet))
.OrderBy(n => n.NetworkInterfaceType is NetworkInterfaceType.Ethernet ? 1 : 0)
.ThenBy(n => n.Name);
}
public static IPAddress GetLanIp()
{
lock (lanIpLock)
{
if (lanIpCache != null)
{
return lanIpCache;
}
}
foreach (NetworkInterface ni in GetInternetInterfaces())
{
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
lock (lanIpLock)
{
return lanIpCache = ip.Address;
}
}
}
}
return null;
}
public static async Task<IPAddress> GetWanIpAsync()
{
lock (wanIpLock)
{
if (wanIpCache != null)
{
return wanIpCache;
}
}
IPAddress ip = await NatHelper.GetExternalIpAsync();
#if RELEASE
if (ip == null || ip.IsPrivate())
{
Regex regex = new(@"(?:[0-2]??[0-9]{1,2}\.){3}[0-2]??[0-9]+");
string[] sites =
{
"https://ipv4.icanhazip.com/",
"https://checkip.amazonaws.com/",
"https://api.ipify.org/",
"https://api4.my-ip.io/ip",
"https://ifconfig.me/",
"https://showmyip.com/",
};
using HttpClient client = new();
foreach (string site in sites)
{
try
{
using HttpResponseMessage response = await client.GetAsync(site);
string content = await response.Content.ReadAsStringAsync();
ip = IPAddress.Parse(regex.Match(content).Value);
break;
}
catch
{
// ignore
}
}
}
#endif
lock (wanIpLock)
{
return wanIpCache = ip;
}
}
public static IPAddress GetHamachiIp()
{
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
{
if (ni.Name != "Hamachi")
{
continue;
}
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
return ip.Address;
}
}
}
return null;
}
public static async Task<bool> HasInternetConnectivityAsync()
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
return false;
}
using Ping ping = new();
PingReply reply = await ping.SendPingAsync(new IPAddress([8, 8, 8, 8]),2000);
return reply.Status == IPStatus.Success;
}
/// <summary>
/// Returns true if the given IP address is reserved for private networks.
/// </summary>
public static bool IsPrivate(this IPAddress address)
{
static bool IsInRange(IPAddress ipAddress, string mask)
{
string[] parts = mask.Split('/');
int ipNum = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);
int cidrAddress = BitConverter.ToInt32(IPAddress.Parse(parts[0]).GetAddressBytes(), 0);
int cidrMask = IPAddress.HostToNetworkOrder(-1 << (32 - int.Parse(parts[1])));
return (ipNum & cidrMask) == (cidrAddress & cidrMask);
}
foreach (string privateSubnet in privateNetworks)
{
if (IsInRange(address, privateSubnet))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns true if the IP address points to the executing machine.
/// </summary>
public static bool IsLocalhost(this IPAddress address)
{
if (address == null)
{
return false;
}
if (IPAddress.IsLoopback(address))
{
return true;
}
foreach (NetworkInterface ni in GetInternetInterfaces())
{
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
if (address.Equals(ip.Address))
{
return true;
}
}
}
return false;
}
}
}

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using NitroxModel.Discovery;
using NitroxModel.Discovery.InstallationFinders.Core;
using NitroxModel.Platforms.OS.Shared;
using NitroxModel.Platforms.Store;
using NitroxModel.Platforms.Store.Interfaces;
namespace NitroxModel.Helper;
public static class NitroxUser
{
public const string LAUNCHER_PATH_ENV_KEY = "NITROX_LAUNCHER_PATH";
private const string PREFERRED_GAMEPATH_KEY = "PreferredGamePath";
private static string appDataPath;
private static string launcherPath;
private static string gamePath;
private static string executableRootPath;
private static string executablePath;
private static string assetsPath;
private static readonly IEnumerable<Func<string>> launcherPathDataSources = new List<Func<string>>
{
() => Environment.GetEnvironmentVariable(LAUNCHER_PATH_ENV_KEY),
() =>
{
Assembly currentAsm = Assembly.GetEntryAssembly();
if (currentAsm?.GetName().Name.Equals("Nitrox.Launcher") ?? false)
{
return Path.GetDirectoryName(currentAsm.Location);
}
Assembly execAsm = Assembly.GetExecutingAssembly();
string execDir = string.IsNullOrEmpty(execAsm.Location) ? Directory.GetCurrentDirectory() : execAsm.Location;
DirectoryInfo execParentDir = Directory.GetParent(execDir);
// When running tests LanguageFiles is in same directory
if (execParentDir != null && Directory.Exists(Path.Combine(execParentDir.FullName, "LanguageFiles")))
{
return execParentDir.FullName;
}
// NitroxModel, NitroxServer and other assemblies are stored in Nitrox.Launcher/lib
if (execParentDir?.Parent != null && Directory.Exists(Path.Combine(execParentDir.Parent.FullName, "Resources", "LanguageFiles")))
{
return execParentDir.Parent.FullName;
}
return null;
},
() =>
{
using ProcessEx proc = ProcessEx.GetFirstProcess("Nitrox.Launcher");
string executable = proc?.MainModule?.FileName;
return !string.IsNullOrWhiteSpace(executable) ? Path.GetDirectoryName(executable) : null;
}
};
public static string AppDataPath
{
get
{
if (appDataPath != null)
{
return appDataPath;
}
string applicationData = null;
// On linux Environment.SpecialFolder.ApplicationData returns the Windows version inside wine, this bypasses that behaviour
string homeInWineEnv = Environment.GetEnvironmentVariable("WINEHOMEDIR");
if (homeInWineEnv is { Length: > 4 })
{
string homeInWine = homeInWineEnv[4..]; // WINEHOMEDIR is prefixed with \??\
if (Directory.Exists(homeInWine))
{
applicationData = Path.Combine(homeInWine, ".config");
Directory.CreateDirectory(applicationData); // Create it if it's not there (which should not happen in normal setups)
}
}
if (!Directory.Exists(applicationData))
{
applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
}
return appDataPath = Path.Combine(applicationData, "Nitrox");
}
}
/// <summary>
/// Tries to get the launcher path that was previously saved by other Nitrox code.
/// </summary>
public static string LauncherPath
{
get
{
if (launcherPath != null)
{
return launcherPath;
}
foreach (Func<string> retriever in launcherPathDataSources)
{
string path = retriever();
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
{
return launcherPath = path;
}
}
return null;
}
}
public static string AssetBundlePath => Path.Combine(LauncherPath, "Resources", "AssetBundles");
public static string LanguageFilesPath => Path.Combine(LauncherPath, "Resources", "LanguageFiles");
public static string PreferredGamePath
{
get => KeyValueStore.Instance.GetValue<string>(PREFERRED_GAMEPATH_KEY);
set => KeyValueStore.Instance.SetValue(PREFERRED_GAMEPATH_KEY, value);
}
private static IGamePlatform gamePlatform;
public static event Action GamePlatformChanged;
public static IGamePlatform GamePlatform
{
get
{
if (gamePlatform == null)
{
_ = GamePath; // Ensure gamePath is set
}
return gamePlatform;
}
set
{
if (gamePlatform != value)
{
gamePlatform = value;
GamePlatformChanged?.Invoke();
}
}
}
public static string GamePath
{
get
{
if (!string.IsNullOrEmpty(gamePath))
{
return gamePath;
}
List<GameFinderResult> finderResults = GameInstallationFinder.Instance.FindGame(GameInfo.Subnautica).TakeUntilInclusive(r => r is { IsOk: false }).ToList();
GameFinderResult potentiallyValidResult = finderResults.LastOrDefault();
if (potentiallyValidResult?.IsOk == true)
{
Log.Debug($"Game installation was found by {potentiallyValidResult.FinderName} at '{potentiallyValidResult.Path}'");
gamePath = potentiallyValidResult.Path;
GamePlatform = GamePlatforms.GetPlatformByGameDir(gamePath);
return gamePath;
}
Log.Error($"Could not locate Subnautica installation directory: {Environment.NewLine}{string.Join(Environment.NewLine, finderResults.Select(i => $"{i.FinderName}: {i.ErrorMessage}"))}");
return string.Empty;
}
set
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
if (!Directory.Exists(value))
{
throw new ArgumentException("Given path is an invalid directory");
}
// Ensures the path looks alright (no mixed / and \ path separators)
gamePath = Path.GetFullPath(value);
GamePlatform = GamePlatforms.GetPlatformByGameDir(gamePath);
}
}
public static string ExecutableRootPath
{
get
{
if (!string.IsNullOrWhiteSpace(executableRootPath))
{
return executableRootPath;
}
string exePath = ExecutableFilePath;
if (exePath == null)
{
return null;
}
return executableRootPath = Path.GetDirectoryName(exePath);
}
}
public static string ExecutableFilePath
{
get
{
if (!string.IsNullOrWhiteSpace(executablePath))
{
return executablePath;
}
Assembly entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly == null)
{
return null;
}
string path = entryAssembly.Location;
// File URI works different on Linux/OSX, so only do uri parsing on Windows.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
path = new Uri(path).LocalPath;
}
return executablePath = path;
}
}
public static string AssetsPath
{
get
{
if (!string.IsNullOrWhiteSpace(assetsPath))
{
return assetsPath;
}
string nitroxAssets;
if (NitroxEnvironment.IsTesting)
{
nitroxAssets = Directory.GetCurrentDirectory();
while (nitroxAssets != null && Path.GetFileName(nitroxAssets) != "Nitrox.Test")
{
nitroxAssets = Directory.GetParent(nitroxAssets)?.FullName;
}
if (nitroxAssets != null)
{
nitroxAssets = Path.Combine(Directory.GetParent(nitroxAssets)?.FullName ?? throw new Exception("Failed to get Nitrox assets during tests"), "Nitrox.Assets.Subnautica");
}
}
else
{
nitroxAssets = LauncherPath ?? ExecutableRootPath;
}
return assetsPath = nitroxAssets;
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.IO;
using NitroxModel.Platforms.OS.Shared;
namespace NitroxModel.Helper
{
public static class PirateDetection
{
public static bool HasTriggered { get; private set; }
/// <summary>
/// Event that calls subscribers if the pirate detection triggered successfully.
/// New subscribers are immediately invoked if the pirate flag has been set at the time of subscription.
/// </summary>
public static event EventHandler PirateDetected
{
add
{
pirateDetected += value;
// Invoke new subscriber immediately if pirate has already been detected.
if (HasTriggered)
{
value?.Invoke(null, EventArgs.Empty);
}
}
remove => pirateDetected -= value;
}
public static bool TriggerOnDirectory(string subnauticaRoot)
{
if (!IsPirateByDirectory(subnauticaRoot))
{
return false;
}
OnPirateDetected();
return true;
}
private static event EventHandler pirateDetected;
private static bool IsPirateByDirectory(string subnauticaRoot)
{
string subdirDll = Path.Combine(subnauticaRoot, GameInfo.Subnautica.DataFolder, "Plugins", "x86_64", "steam_api64.dll");
if (File.Exists(subdirDll) && !FileSystem.Instance.IsTrustedFile(subdirDll))
{
return true;
}
// Dlls might be in root if cracked game (to override DLLs in sub directories).
string rootDll = Path.Combine(subnauticaRoot, "steam_api64.dll");
if (File.Exists(rootDll) && !FileSystem.Instance.IsTrustedFile(rootDll))
{
return true;
}
return false;
}
private static void OnPirateDetected()
{
pirateDetected?.Invoke(null, EventArgs.Empty);
HasTriggered = true;
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
namespace NitroxModel.Helper;
/// <summary>
/// Utility class for reflection API.
/// </summary>
/// <remarks>
/// This class should be used when requiring <see cref="MethodInfo" /> or <see cref="MemberInfo" /> like information from code. This will ensure that compilation only succeeds
/// when reflection is used properly.
/// </remarks>
public static class Reflect
{
private static readonly BindingFlags BINDING_FLAGS_ALL = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
public static ConstructorInfo Constructor(Expression<Action> expression)
{
return (ConstructorInfo)GetMemberInfo(expression);
}
/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo Method(Expression<Action> expression)
{
return (MethodInfo)GetMemberInfo(expression);
}
/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo Method<T>(Expression<Action<T>> expression)
{
return (MethodInfo)GetMemberInfo(expression, typeof(T));
}
/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo Method<T, TResult>(Expression<Func<T, TResult>> expression)
{
return (MethodInfo)GetMemberInfo(expression, typeof(T));
}
public static FieldInfo Field<T>(Expression<Func<T>> expression)
{
return (FieldInfo)GetMemberInfo(expression);
}
public static FieldInfo Field<T>(Expression<Func<T, object>> expression)
{
return (FieldInfo)GetMemberInfo(expression);
}
public static PropertyInfo Property<T>(Expression<Func<T>> expression)
{
return (PropertyInfo)GetMemberInfo(expression);
}
public static PropertyInfo Property<T>(Expression<Func<T, object>> expression)
{
return (PropertyInfo)GetMemberInfo(expression);
}
private static MemberInfo GetMemberInfo(LambdaExpression expression, Type implementingType = null)
{
Expression currentExpression = expression.Body;
while (true)
{
switch (currentExpression.NodeType)
{
case ExpressionType.MemberAccess:
// If it cannot be unwrapped further, return this member.
MemberExpression exp = (MemberExpression)currentExpression;
if (exp.Expression is null or ParameterExpression)
{
return exp.Member;
}
currentExpression = exp.Expression;
break;
case ExpressionType.UnaryPlus:
currentExpression = ((UnaryExpression)currentExpression).Operand;
break;
case ExpressionType.New:
return ((NewExpression)currentExpression).Constructor;
case ExpressionType.Call:
MethodInfo method = ((MethodCallExpression)currentExpression).Method;
if (implementingType == null)
{
return method;
}
if (implementingType == method.ReflectedType)
{
return method;
}
// If method target is an interface, lookup the implementation in the implementingType.
if (method.ReflectedType?.IsInterface ?? false)
{
InterfaceMapping interfaceMap = implementingType.GetInterfaceMap(method.ReflectedType);
int i = Array.IndexOf(interfaceMap.InterfaceMethods, method);
return interfaceMap.TargetMethods[i];
}
// Expression does not know which type the MethodInfo belongs to if it's virtual; MethodInfo of base type/interface is returned instead.
ParameterInfo[] parameters = method.GetParameters();
Type[] args = new Type[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
args[i] = parameters[i].ParameterType;
}
return implementingType.GetMethod(method.Name, BINDING_FLAGS_ALL, null, args, null) ?? throw new Exception($"Unable to find method {method} on type {implementingType.FullName}");
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
currentExpression = ((UnaryExpression)currentExpression).Operand;
break;
case ExpressionType.Invoke:
currentExpression = ((InvocationExpression)currentExpression).Expression;
break;
default:
throw new ArgumentException($"Lambda expression '{expression}' does not target a member");
}
}
}
/// <summary>
/// Used to supplement ref/out/in parameters when using the <see cref="Reflect" /> API.
/// </summary>
public struct Ref<T>
{
public static T Field;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Text;
namespace NitroxModel.Helper
{
public static class StringHelper
{
private static readonly Random random = new Random();
public static string GenerateRandomString(int size)
{
StringBuilder builder = new StringBuilder();
char ch;
for (int i = 0; i < size; i++)
{
ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65)));
builder.Append(ch);
}
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,57 @@
extern alias JB;
using System;
using System.Runtime.CompilerServices;
using JB::JetBrains.Annotations;
using NitroxModel.DataStructures.Util;
namespace NitroxModel.Helper;
public static class Validate
{
// "where T : class" prevents non-nullable valuetypes from getting boxed to objects.
// In other words: Error when trying to assert non-null on something that can't be null in the first place.
[ContractAnnotation("o:null => halt")]
public static void NotNull<T>(T o, [CallerArgumentExpression("o")] string argumentExpression = null) where T : class
{
if (o != null)
{
return;
}
throw new ArgumentNullException(argumentExpression);
}
public static void IsTrue(bool b, [CallerArgumentExpression("b")] string argumentExpression = null)
{
if (!b)
{
throw new ArgumentException(argumentExpression);
}
}
public static void IsFalse(bool b, [CallerArgumentExpression("b")] string argumentExpression = null)
{
if (b)
{
throw new ArgumentException(argumentExpression);
}
}
public static T IsPresent<T>(Optional<T> opt) where T : class
{
if (!opt.HasValue)
{
throw new OptionalEmptyException<T>();
}
return opt.Value;
}
public static T IsPresent<T>(Optional<T> opt, string message) where T : class
{
if (!opt.HasValue)
{
throw new OptionalEmptyException<T>(message);
}
return opt.Value;
}
}