33using System . Diagnostics ;
44using System . Globalization ;
55using System . IO ;
6+ using System . Net ;
7+ using System . Net . Sockets ;
68using System . Reflection ;
79using System . Runtime . InteropServices ;
810using System . Threading ;
11+ using System . Threading . Tasks ;
912using NitroxModel . Core ;
1013using NitroxModel . DataStructures . GameLogic ;
1114using NitroxModel . DataStructures . Util ;
1215using NitroxModel . Discovery ;
16+ using NitroxModel . Helper ;
1317using NitroxModel . Logger ;
1418using NitroxModel_Subnautica . Helper ;
1519using NitroxServer ;
@@ -22,54 +26,126 @@ public class Program
2226 private static readonly Dictionary < string , Assembly > resolvedAssemblyCache = new Dictionary < string , Assembly > ( ) ;
2327 private static Lazy < string > gameInstallDir ;
2428
25- private static void Main ( string [ ] args )
29+ // Prevents Garbage Collection freeing this callback's memory. Causing an exception to occur for this handle.
30+ private static readonly ConsoleEventDelegate consoleCtrlCheckDelegate = ConsoleEventCallback ;
31+
32+ private static async Task Main ( string [ ] args )
2633 {
27- ConfigureCultureInfo ( ) ;
2834 Log . Setup ( ) ;
35+ AppDomain . CurrentDomain . UnhandledException += CurrentDomainOnUnhandledException ;
2936
37+ ConfigureCultureInfo ( ) ;
3038 ConfigureConsoleWindow ( ) ;
3139
32- // Allow game path to be given as command argument
33- if ( args . Length > 0 && Directory . Exists ( args [ 0 ] ) && File . Exists ( Path . Combine ( args [ 0 ] , "Subnautica.exe" ) ) )
40+ AppMutex . Hold ( ( ) =>
3441 {
35- string gameDir = Path . GetFullPath ( args [ 0 ] ) ;
36- Log . Info ( $ "Using game files from: { gameDir } ") ;
37- gameInstallDir = new Lazy < string > ( ( ) => gameDir ) ;
38- }
39- else
42+ Log . Info ( "Waiting for 30 seconds on other Nitrox servers to initialize before starting.." ) ;
43+ } , 30000 ) ;
44+ Server server ;
45+ try
4046 {
41- gameInstallDir = new Lazy < string > ( ( ) =>
47+ // Allow game path to be given as command argument
48+ if ( args . Length > 0 && Directory . Exists ( args [ 0 ] ) && File . Exists ( Path . Combine ( args [ 0 ] , "Subnautica.exe" ) ) )
4249 {
43- string gameDir = GameInstallationFinder . Instance . FindGame ( ) ;
50+ string gameDir = Path . GetFullPath ( args [ 0 ] ) ;
4451 Log . Info ( $ "Using game files from: { gameDir } ") ;
45- return gameDir ;
46- } ) ;
47- }
52+ gameInstallDir = new Lazy < string > ( ( ) => gameDir ) ;
53+ }
54+ else
55+ {
56+ gameInstallDir = new Lazy < string > ( ( ) =>
57+ {
58+ string gameDir = GameInstallationFinder . Instance . FindGame ( ) ;
59+ Log . Info ( $ "Using game files from: { gameDir } ") ;
60+ return gameDir ;
61+ } ) ;
62+ }
4863
49- AppDomain . CurrentDomain . UnhandledException += CurrentDomainOnUnhandledException ;
50- AppDomain . CurrentDomain . AssemblyResolve += CurrentDomainOnAssemblyResolve ;
51- AppDomain . CurrentDomain . ReflectionOnlyAssemblyResolve += CurrentDomainOnAssemblyResolve ;
64+ AppDomain . CurrentDomain . AssemblyResolve += CurrentDomainOnAssemblyResolve ;
65+ AppDomain . CurrentDomain . ReflectionOnlyAssemblyResolve += CurrentDomainOnAssemblyResolve ;
66+
67+ Map . Main = new SubnauticaMap ( ) ;
5268
53- NitroxModel . Helper . Map . Main = new SubnauticaMap ( ) ;
69+ NitroxServiceLocator . InitializeDependencyContainer ( new SubnauticaServerAutoFacRegistrar ( ) ) ;
70+ NitroxServiceLocator . BeginNewLifetimeScope ( ) ;
5471
55- NitroxServiceLocator . InitializeDependencyContainer ( new SubnauticaServerAutoFacRegistrar ( ) ) ;
56- NitroxServiceLocator . BeginNewLifetimeScope ( ) ;
72+ server = NitroxServiceLocator . LocateService < Server > ( ) ;
73+ await WaitForAvailablePortAsync ( server . Port ) ;
74+ if ( ! server . Start ( ) )
75+ {
76+ throw new Exception ( "Unable to start server." ) ;
77+ }
78+ Log . Info ( "Server is waiting for players!" ) ;
5779
58- Server server = NitroxServiceLocator . LocateService < Server > ( ) ;
59- if ( ! server . Start ( ) )
80+ CatchExitEvent ( ) ;
81+ }
82+ finally
6083 {
61- throw new Exception ( "Unable to start server." ) ;
84+ // Allow other servers to start initializing.
85+ AppMutex . Release ( ) ;
6286 }
6387
64- CatchExitEvent ( ) ;
65-
88+ Log . Info ( "To get help for commands, run help in console or /help in chatbox\n " ) ;
6689 ConsoleCommandProcessor cmdProcessor = NitroxServiceLocator . LocateService < ConsoleCommandProcessor > ( ) ;
6790 while ( server . IsRunning )
6891 {
6992 cmdProcessor . ProcessCommand ( Console . ReadLine ( ) , Optional . Empty , Perms . CONSOLE ) ;
7093 }
7194 }
7295
96+ private static async Task WaitForAvailablePortAsync ( int port , int timeoutInSeconds = 30 )
97+ {
98+ void PrintPortWarn ( int timeRemaining )
99+ {
100+ Log . Warn ( $ "Port { port } UDP is already in use. Retrying for { timeRemaining } seconds until it is available..") ;
101+ }
102+
103+ Validate . IsTrue ( timeoutInSeconds >= 5 , "Timeout must be at least 5 seconds." ) ;
104+
105+ DateTimeOffset time = DateTimeOffset . UtcNow ;
106+ bool first = true ;
107+ using CancellationTokenSource source = new CancellationTokenSource ( timeoutInSeconds * 1000 ) ;
108+ using Socket socket = new Socket ( AddressFamily . InterNetwork , SocketType . Dgram , ProtocolType . IP ) ;
109+
110+ try
111+ {
112+ while ( true )
113+ {
114+ source . Token . ThrowIfCancellationRequested ( ) ;
115+ try
116+ {
117+ socket . Bind ( new IPEndPoint ( IPAddress . Any , port ) ) ;
118+ break ;
119+ }
120+ catch ( SocketException ex )
121+ {
122+ if ( ex . SocketErrorCode != SocketError . AddressAlreadyInUse )
123+ {
124+ throw ;
125+ }
126+
127+ if ( first )
128+ {
129+ first = false ;
130+ PrintPortWarn ( timeoutInSeconds ) ;
131+ }
132+ else if ( Environment . UserInteractive )
133+ {
134+ Console . CursorTop -- ;
135+ Console . CursorLeft = 0 ;
136+ PrintPortWarn ( timeoutInSeconds - ( DateTimeOffset . UtcNow - time ) . Seconds ) ;
137+ }
138+ await Task . Delay ( 500 , source . Token ) ;
139+ }
140+ }
141+ }
142+ catch ( OperationCanceledException ex )
143+ {
144+ Log . Error ( ex , "Port availability timeout reached." ) ;
145+ throw ;
146+ }
147+ }
148+
73149 private static void CurrentDomainOnUnhandledException ( object sender , UnhandledExceptionEventArgs e )
74150 {
75151 if ( e . ExceptionObject is Exception ex )
@@ -80,7 +156,7 @@ private static void CurrentDomainOnUnhandledException(object sender, UnhandledEx
80156 {
81157 return ;
82158 }
83-
159+
84160 Console . WriteLine ( "Press L to open log file before closing. Press any other key to close . . ." ) ;
85161 ConsoleKeyInfo key = Console . ReadKey ( true ) ;
86162 if ( key . Key == ConsoleKey . L )
@@ -94,7 +170,7 @@ private static void CurrentDomainOnUnhandledException(object sender, UnhandledEx
94170 } ;
95171 Process . Start ( fileOpenerProgram , Log . FileName ) ;
96172 }
97-
173+
98174 Environment . Exit ( 1 ) ;
99175 }
100176
@@ -180,9 +256,6 @@ private static void CatchExitEvent()
180256 [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
181257 private static extern bool SetConsoleCtrlHandler ( ConsoleEventDelegate callback , bool add ) ;
182258
183- // Prevents Garbage Collection freeing this callback's memory. Causing an exception to occur for this handle.
184- private static readonly ConsoleEventDelegate consoleCtrlCheckDelegate = ConsoleEventCallback ;
185-
186259 private static bool ConsoleEventCallback ( int eventType )
187260 {
188261 if ( eventType == 2 ) // close
0 commit comments