Skip to content

Commit 6a33dc7

Browse files
authored
Added --data-path and --game-path options (SubnauticaNitrox#2522)
2 parents 100b70f + e4678f8 commit 6a33dc7

4 files changed

Lines changed: 81 additions & 36 deletions

File tree

Nitrox.Server.Subnautica/Program.cs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Nitrox.Server.Subnautica;
2626
[SuppressMessage("Usage", "DIMA001:Dependency Injection container is used directly")]
2727
public class Program
2828
{
29-
private static Lazy<string> gameInstallDir;
29+
private static Lazy<string>? gameInstallDir;
3030
private static readonly CircularBuffer<string> inputHistory = new(1000);
3131
private static int currentHistoryIndex;
3232
private static readonly CancellationTokenSource serverCts = new();
@@ -84,19 +84,7 @@ private static async Task StartServer(string[] args)
8484
Stopwatch watch = Stopwatch.StartNew();
8585

8686
// Allow game path to be given as command argument
87-
string gameDir;
88-
if (args.Length > 0 && Directory.Exists(args[0]) && File.Exists(Path.Combine(args[0], GameInfo.Subnautica.ExeName)))
89-
{
90-
gameDir = Path.GetFullPath(args[0]);
91-
gameInstallDir = new Lazy<string>(() => gameDir);
92-
}
93-
else
94-
{
95-
gameInstallDir = new Lazy<string>(() =>
96-
{
97-
return gameDir = NitroxUser.GamePath;
98-
});
99-
}
87+
gameInstallDir = new Lazy<string>(() => NitroxUser.GamePath);
10088
Log.Info($"Using game files from: \'{gameInstallDir.Value}\'");
10189

10290
// TODO: Fix DI to not be slow (should not use IO in type constructors). Instead, use Lazy<T> (et al). This way, cancellation can be faster.
@@ -107,7 +95,7 @@ private static async Task StartServer(string[] args)
10795
{
10896
_ = ipc.SendOutput($"{Ipc.Messages.PlayerCountMessage}:[{count}]");
10997
};
110-
string serverSaveName = NitroxServer.Server.GetSaveName(args, "My World");
98+
string serverSaveName = NitroxServer.Server.GetSaveName(args);
11199
Log.SaveName = serverSaveName;
112100

113101
using (CancellationTokenSource portWaitCts = CancellationTokenSource.CreateLinkedTokenSource(serverCts.Token))
@@ -525,11 +513,11 @@ private static void CurrentDomainOnUnhandledException(object sender, UnhandledEx
525513
private static class AssemblyResolver
526514
{
527515
private static string currentExecutableDirectory;
528-
private static readonly Dictionary<string, Assembly> resolvedAssemblyCache = [];
516+
private static readonly Dictionary<string, AssemblyCacheEntry> resolvedAssemblyCache = [];
529517

530-
public static Assembly Handler(object sender, ResolveEventArgs args)
518+
public static Assembly? Handler(object sender, ResolveEventArgs args)
531519
{
532-
static Assembly ResolveFromLib(ReadOnlySpan<char> dllName)
520+
static Assembly? ResolveFromLib(ReadOnlySpan<char> dllName)
533521
{
534522
dllName = dllName.Slice(0, Math.Max(dllName.IndexOf(','), 0));
535523
if (dllName.IsEmpty)
@@ -546,35 +534,48 @@ static Assembly ResolveFromLib(ReadOnlySpan<char> dllName)
546534
}
547535
string dllNameStr = dllName.ToString();
548536
// If available, return cached assembly
549-
if (resolvedAssemblyCache.TryGetValue(dllNameStr, out Assembly val))
537+
if (resolvedAssemblyCache.TryGetValue(dllNameStr, out AssemblyCacheEntry cacheEntry) && cacheEntry is { Assembly: { } cachedAssembly })
550538
{
551-
return val;
539+
return cachedAssembly;
540+
}
541+
if (cacheEntry == null)
542+
{
543+
cacheEntry = new AssemblyCacheEntry(0, null);
544+
resolvedAssemblyCache[dllNameStr] = cacheEntry;
552545
}
553546

554547
// Load DLLs where this program (exe) is located
555548
string dllPath = Path.Combine(GetExecutableDirectory(), "lib", dllNameStr);
556549
// Prefer to use Newtonsoft dll from game instead of our own due to protobuf issues. TODO: Remove when we do our own deserialization of game data instead of using the game's protobuf.
557550
if (dllPath.IndexOf("Newtonsoft.Json.dll", StringComparison.OrdinalIgnoreCase) >= 0 || !File.Exists(dllPath))
558551
{
559-
// Try find game managed libraries
560-
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
552+
if (gameInstallDir != null)
561553
{
562-
dllPath = Path.Combine(gameInstallDir.Value, "Resources", "Data", "Managed", dllNameStr);
563-
}
564-
else
565-
{
566-
dllPath = Path.Combine(gameInstallDir.Value, "Subnautica_Data", "Managed", dllNameStr);
554+
// Try find game managed libraries
555+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
556+
{
557+
dllPath = Path.Combine(gameInstallDir.Value, "Resources", "Data", "Managed", dllNameStr);
558+
}
559+
else
560+
{
561+
dllPath = Path.Combine(gameInstallDir.Value, "Subnautica_Data", "Managed", dllNameStr);
562+
}
567563
}
568564
}
569565

570566
try
571567
{
572568
// Read assemblies as bytes as to not lock the file so that Nitrox can patch assemblies while server is running.
573-
Assembly assembly = Assembly.Load(File.ReadAllBytes(dllPath));
574-
return resolvedAssemblyCache[dllNameStr] = assembly;
569+
cacheEntry.Assembly = Assembly.Load(File.ReadAllBytes(dllPath));
570+
return cacheEntry.Assembly;
575571
}
576572
catch
577573
{
574+
cacheEntry.Attempts++;
575+
if (cacheEntry.Attempts >= 5)
576+
{
577+
throw new FileNotFoundException($"Failed to load DLL '{dllName}' at: {dllPath}");
578+
}
578579
return null;
579580
}
580581
}
@@ -602,5 +603,11 @@ private static string GetExecutableDirectory()
602603
}
603604
return currentExecutableDirectory = new Uri(Path.GetDirectoryName(pathAttempt ?? ".") ?? Directory.GetCurrentDirectory()).LocalPath;
604605
}
606+
607+
private record AssemblyCacheEntry(int Attempts, Assembly? Assembly)
608+
{
609+
public int Attempts { get; set; } = Attempts;
610+
public Assembly? Assembly { get; set; } = Assembly;
611+
}
605612
}
606613
}

NitroxModel/Core/NitroxEnvironment.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Reflection;
5+
using System.Runtime.InteropServices;
46

57
namespace NitroxModel.Helper;
68

@@ -87,7 +89,24 @@ public static bool IsReleaseMode
8789
/// <summary>
8890
/// Gets the command line arguments as passed to the program on start.
8991
/// </summary>
90-
public static string[] CommandLineArgs => commandLineArgs ??= Environment.GetCommandLineArgs().Skip(1).ToArray();
92+
public static string[] CommandLineArgs
93+
{
94+
get
95+
{
96+
if (commandLineArgs != null)
97+
{
98+
return commandLineArgs;
99+
}
100+
101+
IEnumerable<string> args = Environment.GetCommandLineArgs().Skip(1);
102+
// Windows removes the ' character around an arg but other OSes do not.
103+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
104+
{
105+
args = args.Select(p => p.Trim('\''));
106+
}
107+
return commandLineArgs ??= args.ToArray();
108+
}
109+
}
91110

92111
public static string AppName => (Assembly.GetEntryAssembly()?.GetName().Name ?? Assembly.GetCallingAssembly().GetName().Name).Replace(".", " ");
93112

NitroxModel/Helper/NitroxUser.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ public static string AppDataPath
8383
}
8484
}
8585

86+
string? cliDataPath = NitroxEnvironment.CommandLineArgs.GetCommandArgs("--data-path").FirstOrDefault();
87+
if (!string.IsNullOrWhiteSpace(cliDataPath) && Path.IsPathRooted(cliDataPath))
88+
{
89+
Directory.CreateDirectory(cliDataPath);
90+
return appDataPath = cliDataPath;
91+
}
92+
8693
if (!Directory.Exists(applicationData))
8794
{
8895
applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
@@ -164,6 +171,17 @@ public static string GamePath
164171
return gamePath;
165172
}
166173

174+
string? cliGamePath = NitroxEnvironment.CommandLineArgs.GetCommandArgs("--game-path").FirstOrDefault();
175+
if (Directory.Exists(cliGamePath) && Path.IsPathRooted(cliGamePath))
176+
{
177+
GamePlatform = GamePlatforms.GetPlatformByGameDir(cliGamePath);
178+
return gamePath = cliGamePath;
179+
}
180+
if (cliGamePath != null)
181+
{
182+
throw new DirectoryNotFoundException($"Game directory not found at user-specified location: {cliGamePath}");
183+
}
184+
167185
List<GameFinderResult> finderResults = GameInstallationFinder.Instance.FindGame(GameInfo.Subnautica).TakeUntilInclusive(r => r is { IsOk: false }).ToList();
168186
GameFinderResult potentiallyValidResult = finderResults.LastOrDefault();
169187
if (potentiallyValidResult?.IsOk == true)

NitroxServer/Server.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.IO;
56
using System.Linq;
67
using System.Net;
@@ -135,7 +136,7 @@ public static SubnauticaServerConfig CreateOrLoadConfig()
135136
{
136137
// Create new save file
137138
Log.Debug("No save file was found, creating a new one...");
138-
saveDir = Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), "My World");
139+
saveDir = Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), GetSaveName(NitroxEnvironment.CommandLineArgs));
139140
Directory.CreateDirectory(saveDir);
140141
}
141142

@@ -368,13 +369,13 @@ private static List<ServerListing> GetSaves()
368369
/// Parses the save name from the given command line arguments or defaults to the standard save name.
369370
/// </summary>
370371
// TODO : Remove this method once server hosting/loading happens as a service (see '.NET Generic Host' on msdn)
371-
public static string GetSaveName(string[] args, string defaultValue = null)
372+
public static string GetSaveName(string[] args, string defaultValue = "My World")
372373
{
373-
string result = args.GetCommandArgs("--save").FirstOrDefault() ?? args.GetCommandArgs("--name").FirstOrDefault();
374+
string? result = args.GetCommandArgs("--save").FirstOrDefault() ?? args.GetCommandArgs("--name").FirstOrDefault();
374375
return IsValidSaveName(result) ? result : defaultValue;
375376
}
376377

377-
private static bool IsValidSaveName(string name)
378+
private static bool IsValidSaveName([NotNullWhen(true)] string? name)
378379
{
379380
if (string.IsNullOrWhiteSpace(name))
380381
{
@@ -384,11 +385,11 @@ private static bool IsValidSaveName(string name)
384385
{
385386
return false;
386387
}
387-
if (name.EndsWith("."))
388+
if (name.EndsWith('.'))
388389
{
389390
return false;
390391
}
391-
if (name.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1)
392+
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) > -1)
392393
{
393394
return false;
394395
}

0 commit comments

Comments
 (0)