mirror of
https://github.com/CommonLoon102/NBloodServerSupervisor.git
synced 2025-01-05 09:32:35 +01:00
fix port ranges (#6)
This commit is contained in:
parent
4f96ad89db
commit
c1b385c41d
12
Common/CommandLineUtils.cs
Normal file
12
Common/CommandLineUtils.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Common
|
||||||
|
{
|
||||||
|
public static class CommandLineUtils
|
||||||
|
{
|
||||||
|
public static string GetClientLaunchCommand(string host, int port, string modCommandLine) =>
|
||||||
|
$"nblood -client {host} -port {port} {modCommandLine}";
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ namespace Common
|
|||||||
{
|
{
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public const string NBloodExecutable = "nblood_server";
|
|
||||||
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
|
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
|
||||||
{
|
{
|
||||||
{ "BLOOD", new Mod("BLOOD", "Blood", "") },
|
{ "BLOOD", new Mod("BLOOD", "Blood", "") },
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
using Model;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Common
|
|
||||||
{
|
|
||||||
public static class NBloodServerStartInfo
|
|
||||||
{
|
|
||||||
private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
|
|
||||||
|
|
||||||
public static ProcessStartInfo Get(int maxPlayers, int port, Mod mod)
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo(GetExecutable(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}")
|
|
||||||
{
|
|
||||||
UseShellExecute = true,
|
|
||||||
WorkingDirectory = workingDir
|
|
||||||
};
|
|
||||||
|
|
||||||
return psi;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetExecutable()
|
|
||||||
{
|
|
||||||
string nbloodServer = Constants.NBloodExecutable;
|
|
||||||
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
||||||
if (isWindows)
|
|
||||||
nbloodServer += ".exe";
|
|
||||||
|
|
||||||
return nbloodServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.NetworkInformation;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Common
|
|
||||||
{
|
|
||||||
public class PortUtils
|
|
||||||
{
|
|
||||||
public static int GetPort()
|
|
||||||
{
|
|
||||||
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
|
|
||||||
var usedPorts = ipGlobalProperties.GetActiveTcpConnections().Select(c => c.LocalEndPoint.Port).ToList();
|
|
||||||
usedPorts.AddRange(ipGlobalProperties.GetActiveTcpListeners().Select(c => c.Port).ToList());
|
|
||||||
usedPorts.AddRange(ipGlobalProperties.GetActiveUdpListeners().Select(c => c.Port).ToList());
|
|
||||||
|
|
||||||
var availablePorts = Enumerable.Range(1025, ushort.MaxValue).ToList().Except(usedPorts).ToList();
|
|
||||||
|
|
||||||
Random rnd = new Random();
|
|
||||||
int index = rnd.Next(0, availablePorts.Count() - 1);
|
|
||||||
int port = availablePorts[index];
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
86
Common/ProcessSpawner.cs
Normal file
86
Common/ProcessSpawner.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using Model;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
namespace Common
|
||||||
|
{
|
||||||
|
public static class ProcessSpawner
|
||||||
|
{
|
||||||
|
private const int minPort = 23581;
|
||||||
|
private const int maxPort = 23700;
|
||||||
|
private const int maximumServers = 80;
|
||||||
|
private const string nBloodExecutable = "nblood_server";
|
||||||
|
|
||||||
|
private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
|
||||||
|
private static readonly Random rnd = new Random();
|
||||||
|
|
||||||
|
public static SpawnedServerInfo SpawnServer(int players, string modName)
|
||||||
|
{
|
||||||
|
int serversRunning = Process.GetProcessesByName(nBloodExecutable).Count();
|
||||||
|
if (serversRunning >= maximumServers)
|
||||||
|
throw new Exception("The maximum number of servers are already running.");
|
||||||
|
|
||||||
|
Mod mod = GetModByName(modName);
|
||||||
|
int port = GetPort();
|
||||||
|
|
||||||
|
var process = Process.Start(GetProcessStartInfo(players, port, mod));
|
||||||
|
return new SpawnedServerInfo(process, port, mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mod GetModByName(string modName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(modName))
|
||||||
|
return Constants.SupportedMods["BLOOD"];
|
||||||
|
|
||||||
|
if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
|
||||||
|
throw new Exception("This mod is not supported: " + modName);
|
||||||
|
|
||||||
|
return Constants.SupportedMods[modName.ToUpper()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetPort()
|
||||||
|
{
|
||||||
|
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
|
||||||
|
var usedPorts = ipGlobalProperties.GetActiveTcpConnections().Select(c => c.LocalEndPoint.Port).ToList();
|
||||||
|
usedPorts.AddRange(ipGlobalProperties.GetActiveTcpListeners().Select(c => c.Port).ToList());
|
||||||
|
usedPorts.AddRange(ipGlobalProperties.GetActiveUdpListeners().Select(c => c.Port).ToList());
|
||||||
|
|
||||||
|
var availablePorts = Enumerable.Range(minPort, maxPort - minPort + 1).ToList().Except(usedPorts).ToList();
|
||||||
|
|
||||||
|
if (availablePorts.Count == 0)
|
||||||
|
throw new Exception($"Cannot obtain free port in range {minPort}-{maxPort}.");
|
||||||
|
|
||||||
|
int index = rnd.Next(0, availablePorts.Count() - 1);
|
||||||
|
int port = availablePorts[index];
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProcessStartInfo GetProcessStartInfo(int maxPlayers, int port, Mod mod)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(GetExecutableName(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}")
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = workingDir
|
||||||
|
};
|
||||||
|
|
||||||
|
return psi;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetExecutableName()
|
||||||
|
{
|
||||||
|
string nbloodServer = nBloodExecutable;
|
||||||
|
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
|
if (isWindows)
|
||||||
|
nbloodServer += ".exe";
|
||||||
|
|
||||||
|
return nbloodServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
Common/SpawnedServerInfo.cs
Normal file
22
Common/SpawnedServerInfo.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using Model;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Common
|
||||||
|
{
|
||||||
|
public class SpawnedServerInfo
|
||||||
|
{
|
||||||
|
public Process Process { get; }
|
||||||
|
public int Port { get; }
|
||||||
|
public Mod Mod { get; }
|
||||||
|
|
||||||
|
public SpawnedServerInfo(Process process, int port, Mod mod)
|
||||||
|
{
|
||||||
|
Process = process;
|
||||||
|
Port = port;
|
||||||
|
Mod = mod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -89,3 +89,5 @@ http://your.ip.goes.here:23580/nblood/api/startserver?players=3&modName=cryptic&
|
|||||||
The number of players must be at least 3 and maximum 8. The servers started with this URL won't be visible publicly via the `listservers` URL.
|
The number of players must be at least 3 and maximum 8. The servers started with this URL won't be visible publicly via the `listservers` URL.
|
||||||
The modName parameter can be `cryptic`, `dw`, `fo`, `twoira` or it can be missing.
|
The modName parameter can be `cryptic`, `dw`, `fo`, `twoira` or it can be missing.
|
||||||
You can see the port and the command line command to join in the response.
|
You can see the port and the command line command to join in the response.
|
||||||
|
|
||||||
|
Port range used: 23580-23700
|
||||||
|
@ -26,23 +26,30 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void ProcessPacket(byte[] buffer)
|
private static void ProcessPacket(byte[] buffer)
|
||||||
{
|
{
|
||||||
string message = Encoding.ASCII.GetString(buffer);
|
try
|
||||||
switch (message[0])
|
|
||||||
{
|
{
|
||||||
case 'A':
|
string message = Encoding.ASCII.GetString(buffer);
|
||||||
ProcessPlayerCountsPacket(message);
|
switch (message[0])
|
||||||
break;
|
{
|
||||||
case 'B':
|
case 'A':
|
||||||
ProcessPlayerNamesPacket(message);
|
ProcessPlayerCountsPacket(message);
|
||||||
break;
|
break;
|
||||||
case 'C':
|
case 'B':
|
||||||
ProcessFragsPacket(buffer);
|
ProcessPlayerNamesPacket(message);
|
||||||
break;
|
break;
|
||||||
case 'D':
|
case 'C':
|
||||||
ProcessRemovePacket(message);
|
ProcessFragsPacket(buffer);
|
||||||
break;
|
break;
|
||||||
default:
|
case 'D':
|
||||||
break;
|
ProcessRemovePacket(message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Log...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,15 +24,22 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void KillUnusedServers()
|
private static void KillUnusedServers()
|
||||||
{
|
{
|
||||||
var killables = Program.State.Servers.Values.Where(s =>
|
try
|
||||||
s.IsPrivate
|
|
||||||
&& !s.IsStarted
|
|
||||||
&& s.CurrentPlayers < 2
|
|
||||||
&& (DateTime.UtcNow - s.SpawnedAtUtc) > TimeSpan.FromMinutes(10));
|
|
||||||
|
|
||||||
foreach (var server in killables)
|
|
||||||
{
|
{
|
||||||
Process.GetProcessById(server.ProcessId).Kill();
|
var killables = Program.State.Servers.Values.Where(s =>
|
||||||
|
s.IsPrivate
|
||||||
|
&& !s.IsStarted
|
||||||
|
&& s.CurrentPlayers < 2
|
||||||
|
&& (DateTime.UtcNow - s.SpawnedAtUtc) > TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
|
foreach (var server in killables)
|
||||||
|
{
|
||||||
|
Process.GetProcessById(server.ProcessId).Kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//Log...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,20 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void RemoveCrashedServers()
|
private static void RemoveCrashedServers()
|
||||||
{
|
{
|
||||||
var crashedServers = State.Servers.Values
|
try
|
||||||
.Where(s => s.IsStarted && DateTime.UtcNow - s.LastHeartBeatUtc < TimeSpan.FromMinutes(15))
|
|
||||||
.Select(s => s.Port);
|
|
||||||
|
|
||||||
foreach (var port in crashedServers)
|
|
||||||
{
|
{
|
||||||
State.Servers.TryRemove(port, out _);
|
var crashedServers = State.Servers.Values
|
||||||
|
.Where(s => s.IsStarted && DateTime.UtcNow - s.LastHeartBeatUtc < TimeSpan.FromMinutes(15))
|
||||||
|
.Select(s => s.Port);
|
||||||
|
|
||||||
|
foreach (var port in crashedServers)
|
||||||
|
{
|
||||||
|
State.Servers.TryRemove(port, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Log...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,24 +46,33 @@ namespace Supervisor
|
|||||||
{
|
{
|
||||||
if (IsNewServerNeeded(i, mod))
|
if (IsNewServerNeeded(i, mod))
|
||||||
{
|
{
|
||||||
int port = PortUtils.GetPort();
|
try
|
||||||
var process = Process.Start(NBloodServerStartInfo.Get(i, port, mod));
|
|
||||||
Program.State.Servers.AddOrUpdate(port, new Server()
|
|
||||||
{
|
{
|
||||||
Port = port,
|
var spawnedServer = ProcessSpawner.SpawnServer(i, mod.Name);
|
||||||
ProcessId = process.Id,
|
int port = spawnedServer.Port;
|
||||||
MaximumPlayers = i,
|
int processId = spawnedServer.Process.Id;
|
||||||
CurrentPlayers = 1,
|
Program.State.Servers.AddOrUpdate(port, new Server()
|
||||||
Mod = mod,
|
{
|
||||||
},
|
Port = port,
|
||||||
(prt, server) =>
|
ProcessId = processId,
|
||||||
|
MaximumPlayers = i,
|
||||||
|
CurrentPlayers = 1,
|
||||||
|
Mod = mod,
|
||||||
|
},
|
||||||
|
(prt, server) =>
|
||||||
|
{
|
||||||
|
server.ProcessId = processId;
|
||||||
|
server.MaximumPlayers = i;
|
||||||
|
server.CurrentPlayers = 1;
|
||||||
|
server.Mod = mod;
|
||||||
|
return server;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
{
|
{
|
||||||
server.ProcessId = process.Id;
|
// No free ports, cannot create process
|
||||||
server.MaximumPlayers = i;
|
// Log...
|
||||||
server.CurrentPlayers = 1;
|
}
|
||||||
server.Mod = mod;
|
|
||||||
return server;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,17 +31,24 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void ProcessWebApiMessage(byte[] buffer)
|
private static void ProcessWebApiMessage(byte[] buffer)
|
||||||
{
|
{
|
||||||
string message = Encoding.ASCII.GetString(buffer);
|
try
|
||||||
switch (message[0])
|
|
||||||
{
|
{
|
||||||
case 'A':
|
string message = Encoding.ASCII.GetString(buffer);
|
||||||
ProcessGetCurrentStateRequest();
|
switch (message[0])
|
||||||
break;
|
{
|
||||||
case 'B':
|
case 'A':
|
||||||
StorePrivateServerInfo(message);
|
ProcessGetCurrentStateRequest();
|
||||||
break;
|
break;
|
||||||
default:
|
case 'B':
|
||||||
break;
|
StorePrivateServerInfo(message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Log...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Model;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace WebInterface
|
|
||||||
{
|
|
||||||
public static class CommandLineUtils
|
|
||||||
{
|
|
||||||
public static string GetLaunchCommand(string host, int port, Mod mod)
|
|
||||||
{
|
|
||||||
return $"nblood -client {host} -port {port} {mod.CommandLine}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -58,23 +58,20 @@ namespace WebInterface.Controllers
|
|||||||
if (parameters.ApiKey != _config.GetValue<string>("ApiKey"))
|
if (parameters.ApiKey != _config.GetValue<string>("ApiKey"))
|
||||||
return new StartServerResponse("Invalid ApiKey.");
|
return new StartServerResponse("Invalid ApiKey.");
|
||||||
|
|
||||||
string processName = Constants.NBloodExecutable;
|
SpawnedServerInfo serverProcess = ProcessSpawner.SpawnServer(parameters.Players, parameters.ModName);
|
||||||
int serversRunning = Process.GetProcessesByName(processName).Count();
|
byte[] payload = Encoding.ASCII.GetBytes($"B{serverProcess.Port}\t{serverProcess.Process.Id}\0");
|
||||||
if (serversRunning >= _config.GetValue<int>("MaximumServers"))
|
|
||||||
return new StartServerResponse("The maximum number of servers are already running.");
|
|
||||||
|
|
||||||
Mod mod = GetMod(parameters.ModName);
|
|
||||||
int port = PortUtils.GetPort();
|
|
||||||
|
|
||||||
var process = Process.Start(NBloodServerStartInfo.Get(parameters.Players, port, mod));
|
|
||||||
byte[] payload = Encoding.ASCII.GetBytes($"B{port}\t{process.Id}\0");
|
|
||||||
socket.SendTo(payload, webApiListenerEndPoint);
|
socket.SendTo(payload, webApiListenerEndPoint);
|
||||||
|
|
||||||
_logger.LogInformation("Server started waiting for {0} players on port {1}.",
|
_logger.LogInformation("Server started waiting for {0} players on port {1}.",
|
||||||
parameters.Players, port);
|
parameters.Players, serverProcess.Port);
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||||
return new StartServerResponse(port) { CommandLine = CommandLineUtils.GetLaunchCommand(HttpContext.Request.Host.Host, port, mod) };
|
return new StartServerResponse(serverProcess.Port)
|
||||||
|
{
|
||||||
|
CommandLine = CommandLineUtils.GetClientLaunchCommand(HttpContext.Request.Host.Host,
|
||||||
|
serverProcess.Port,
|
||||||
|
serverProcess.Mod.CommandLine)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -108,16 +105,5 @@ namespace WebInterface.Controllers
|
|||||||
return new ListServersResponse("Unhandled exception has been occured. Check the logs for details.");
|
return new ListServersResponse("Unhandled exception has been occured. Check the logs for details.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mod GetMod(string modName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(modName))
|
|
||||||
return Constants.SupportedMods["BLOOD"];
|
|
||||||
|
|
||||||
if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
|
|
||||||
throw new Exception("This mod is not supported: " + modName);
|
|
||||||
|
|
||||||
return Constants.SupportedMods[modName.ToUpper()];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Model;
|
using Common;
|
||||||
|
using Model;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -28,7 +29,7 @@ namespace WebInterface.Services
|
|||||||
{
|
{
|
||||||
Port = s.Port,
|
Port = s.Port,
|
||||||
IsStarted = s.IsStarted,
|
IsStarted = s.IsStarted,
|
||||||
CommandLine = s.CurrentPlayers == s.MaximumPlayers ? "Sorry, the game is already started." : CommandLineUtils.GetLaunchCommand(host, s.Port, s.Mod),
|
CommandLine = GetCommandLine(s, host),
|
||||||
GameType = s.GameType,
|
GameType = s.GameType,
|
||||||
Mod = s.Mod.FriendlyName,
|
Mod = s.Mod.FriendlyName,
|
||||||
CurrentPlayers = s.CurrentPlayers,
|
CurrentPlayers = s.CurrentPlayers,
|
||||||
@ -41,6 +42,14 @@ namespace WebInterface.Services
|
|||||||
return serversResponse;
|
return serversResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetCommandLine(Model.Server server, string host)
|
||||||
|
{
|
||||||
|
if (server.CurrentPlayers == server.MaximumPlayers)
|
||||||
|
return "Sorry, the game is already started.";
|
||||||
|
|
||||||
|
return CommandLineUtils.GetClientLaunchCommand(host, server.Port, server.Mod.CommandLine);
|
||||||
|
}
|
||||||
|
|
||||||
public GetStatisticsResponse GetStatistics()
|
public GetStatisticsResponse GetStatistics()
|
||||||
{
|
{
|
||||||
StateResponse stateResponse = RequestState();
|
StateResponse stateResponse = RequestState();
|
||||||
|
@ -159,7 +159,7 @@
|
|||||||
</details>
|
</details>
|
||||||
<br />
|
<br />
|
||||||
<hr />
|
<hr />
|
||||||
<p>Do you think this page is ugly? You are right! If you want to make it better, PRs are welcomed (but please, don't use any JavaScript, thanks): <a href="https://github.com/CommonLoon102/NBloodServerSupervisor">https://github.com/CommonLoon102/NBloodServerSupervisor</a></p>
|
<p>Do you think this page is ugly? You are right! If you want to make it look better, PRs are welcomed (but please, don't use any JavaScript, thanks): <a href="https://github.com/CommonLoon102/NBloodServerSupervisor">https://github.com/CommonLoon102/NBloodServerSupervisor</a></p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -7,6 +7,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MaximumServers": 80,
|
|
||||||
"ApiKey": "CHANGEME"
|
"ApiKey": "CHANGEME"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user