mirror of
https://github.com/CommonLoon102/NBloodServerSupervisor.git
synced 2024-12-22 18:52:44 +01:00
Add project files.
This commit is contained in:
parent
6971176d7d
commit
0ac65ffd08
7
Common/Common.csproj
Normal file
7
Common/Common.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
26
Common/PortUtils.cs
Normal file
26
Common/PortUtils.cs
Normal file
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
7
Model/Model.csproj
Normal file
7
Model/Model.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
13
Model/Player.cs
Normal file
13
Model/Player.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Model
|
||||
{
|
||||
[Serializable]
|
||||
public class Player
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
20
Model/Server.cs
Normal file
20
Model/Server.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Model
|
||||
{
|
||||
[Serializable]
|
||||
public class Server
|
||||
{
|
||||
public DateTime SpawnedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public int ProcessId { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool IsStarted { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
public int CurrentPlayers { get; set; }
|
||||
public int MaximumPlayers { get; set; }
|
||||
public string GameType { get; set; }
|
||||
public IList<Player> Players { get; set; } = new List<Player>();
|
||||
}
|
||||
}
|
12
Model/State.cs
Normal file
12
Model/State.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Model
|
||||
{
|
||||
public class State
|
||||
{
|
||||
public ConcurrentDictionary<int, Server> Servers { get; } = new ConcurrentDictionary<int, Server>();
|
||||
}
|
||||
}
|
12
Model/StateResponse.cs
Normal file
12
Model/StateResponse.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Model
|
||||
{
|
||||
[Serializable]
|
||||
public class StateResponse
|
||||
{
|
||||
public IList<Server> Servers { get; set; }
|
||||
}
|
||||
}
|
43
NBloodServerSupervisor.sln
Normal file
43
NBloodServerSupervisor.sln
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29709.97
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Supervisor", "Supervisor\Supervisor.csproj", "{BBE1C869-D87B-4174-8EC2-0E095488E99A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Model", "Model\Model.csproj", "{940B1560-86F4-4233-B59E-B502B0B5C2AF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{0BD5EB76-F38C-4C22-9213-6846251D18B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebInterface", "WebInterface\WebInterface.csproj", "{7DC8478B-FF2E-4CA5-BF4B-23DBC3E80D09}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BBE1C869-D87B-4174-8EC2-0E095488E99A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BBE1C869-D87B-4174-8EC2-0E095488E99A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BBE1C869-D87B-4174-8EC2-0E095488E99A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BBE1C869-D87B-4174-8EC2-0E095488E99A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{940B1560-86F4-4233-B59E-B502B0B5C2AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{940B1560-86F4-4233-B59E-B502B0B5C2AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{940B1560-86F4-4233-B59E-B502B0B5C2AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{940B1560-86F4-4233-B59E-B502B0B5C2AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0BD5EB76-F38C-4C22-9213-6846251D18B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0BD5EB76-F38C-4C22-9213-6846251D18B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0BD5EB76-F38C-4C22-9213-6846251D18B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0BD5EB76-F38C-4C22-9213-6846251D18B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7DC8478B-FF2E-4CA5-BF4B-23DBC3E80D09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7DC8478B-FF2E-4CA5-BF4B-23DBC3E80D09}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7DC8478B-FF2E-4CA5-BF4B-23DBC3E80D09}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7DC8478B-FF2E-4CA5-BF4B-23DBC3E80D09}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A47D63A4-8A9E-49E2-8DC8-C96C042534D4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
17
Supervisor/Frags.cs
Normal file
17
Supervisor/Frags.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
class Frags : PacketData
|
||||
{
|
||||
public IList<int> Scores { get; set; }
|
||||
public int GameType { get; set; }
|
||||
|
||||
public Frags()
|
||||
{
|
||||
IsStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
166
Supervisor/NBloodServerListener.cs
Normal file
166
Supervisor/NBloodServerListener.cs
Normal file
@ -0,0 +1,166 @@
|
||||
using Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
static class NBloodServerListener
|
||||
{
|
||||
private const int listenPort = 11027;
|
||||
private static IPEndPoint remoteIP = new IPEndPoint(IPAddress.Loopback, listenPort);
|
||||
private static UdpClient udpClient = new UdpClient(remoteIP);
|
||||
|
||||
public static async void StartListening()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
UdpReceiveResult rst = await udpClient.ReceiveAsync();
|
||||
ProcessPacket(rst.Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessPacket(byte[] buffer)
|
||||
{
|
||||
string message = Encoding.ASCII.GetString(buffer);
|
||||
switch (message[0])
|
||||
{
|
||||
case 'A':
|
||||
ProcessPlayerCountsPacket(message);
|
||||
break;
|
||||
case 'B':
|
||||
ProcessPlayerNamesPacket(message);
|
||||
break;
|
||||
case 'C':
|
||||
ProcessFragsPacket(buffer);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessPlayerCountsPacket(string message)
|
||||
{
|
||||
string[] splitMessage = message.SplitMessage();
|
||||
var packetData = new PlayerCounts()
|
||||
{
|
||||
Port = int.Parse(splitMessage[0]),
|
||||
IsStarted = int.Parse(splitMessage[1]) != 0,
|
||||
CurrentPlayers = int.Parse(splitMessage[2]),
|
||||
MaximumPlayers = int.Parse(splitMessage[3])
|
||||
};
|
||||
|
||||
UpdateState(packetData);
|
||||
}
|
||||
|
||||
private static void UpdateState(PlayerCounts packetData)
|
||||
{
|
||||
Program.State.Servers.AddOrUpdate(packetData.Port,
|
||||
new Server()
|
||||
{
|
||||
Port = packetData.Port,
|
||||
IsStarted = packetData.IsStarted,
|
||||
IsPrivate = packetData.IsPrivate,
|
||||
CurrentPlayers = packetData.CurrentPlayers,
|
||||
MaximumPlayers = packetData.MaximumPlayers
|
||||
},
|
||||
(port, server) =>
|
||||
{
|
||||
server.IsStarted = packetData.IsStarted;
|
||||
server.IsPrivate = packetData.IsPrivate;
|
||||
server.CurrentPlayers = packetData.CurrentPlayers;
|
||||
server.MaximumPlayers = packetData.MaximumPlayers;
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
private static void ProcessPlayerNamesPacket(string message)
|
||||
{
|
||||
string[] splitMessage = message.SplitMessage();
|
||||
var packetData = new PlayerNames()
|
||||
{
|
||||
Port = int.Parse(splitMessage[0]),
|
||||
Names = splitMessage.Skip(1).ToList(),
|
||||
};
|
||||
|
||||
UpdateState(packetData);
|
||||
}
|
||||
|
||||
private static void UpdateState(PlayerNames packetData)
|
||||
{
|
||||
Program.State.Servers.AddOrUpdate(packetData.Port,
|
||||
new Server()
|
||||
{
|
||||
Port = packetData.Port,
|
||||
IsStarted = packetData.IsStarted,
|
||||
Players = packetData.Names.Select(name => new Player() { Name = name }).ToList()
|
||||
},
|
||||
(port, server) =>
|
||||
{
|
||||
server.IsStarted = packetData.IsStarted;
|
||||
if (server.Players.Count == 0 || server.CurrentPlayers != packetData.Names.Count)
|
||||
{
|
||||
server.CurrentPlayers = packetData.Names.Count;
|
||||
server.Players = packetData.Names.Select(name => new Player()
|
||||
{
|
||||
Name = name,
|
||||
Score = 0
|
||||
}).ToList();
|
||||
}
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
private static void ProcessFragsPacket(byte[] buffer)
|
||||
{
|
||||
int port = BitConverter.ToInt32(buffer, 4);
|
||||
int gameType = BitConverter.ToInt32(buffer, 8);
|
||||
const int maxPlayers = 8;
|
||||
int[] scores = new int[maxPlayers];
|
||||
for (int i = 0; i < scores.Length; i++)
|
||||
scores[i] = BitConverter.ToInt32(buffer, 12 + i * sizeof(int));
|
||||
|
||||
var packetData = new Frags()
|
||||
{
|
||||
Port = port,
|
||||
GameType = gameType,
|
||||
Scores = scores
|
||||
};
|
||||
|
||||
UpdateState(packetData);
|
||||
}
|
||||
|
||||
private static void UpdateState(Frags packetData)
|
||||
{
|
||||
Program.State.Servers.AddOrUpdate(packetData.Port,
|
||||
new Server()
|
||||
{
|
||||
Port = packetData.Port,
|
||||
IsStarted = packetData.IsStarted,
|
||||
GameType = GetGameType(packetData.GameType)
|
||||
},
|
||||
(port, server) =>
|
||||
{
|
||||
server.IsStarted = packetData.IsStarted;
|
||||
server.GameType = GetGameType(packetData.GameType);
|
||||
for (int i = 0; i < server.CurrentPlayers; i++)
|
||||
server.Players[i].Score = packetData.Scores[i];
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetGameType(int gameType)
|
||||
{
|
||||
return gameType switch
|
||||
{
|
||||
1 => "Cooperative",
|
||||
2 => "Bloodbath",
|
||||
3 => "Capture The Flag",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
13
Supervisor/PacketData.cs
Normal file
13
Supervisor/PacketData.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
abstract class PacketData
|
||||
{
|
||||
public int Port { get; set; }
|
||||
public bool IsStarted { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
}
|
17
Supervisor/PlayerCounts.cs
Normal file
17
Supervisor/PlayerCounts.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
class PlayerCounts : PacketData
|
||||
{
|
||||
public int CurrentPlayers { get; set; }
|
||||
public int MaximumPlayers { get; set; }
|
||||
|
||||
public PlayerCounts()
|
||||
{
|
||||
IsStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
16
Supervisor/PlayerNames.cs
Normal file
16
Supervisor/PlayerNames.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
class PlayerNames : PacketData
|
||||
{
|
||||
public IList<string> Names { get; set; }
|
||||
|
||||
public PlayerNames()
|
||||
{
|
||||
IsStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
39
Supervisor/PrivateServerManager.cs
Normal file
39
Supervisor/PrivateServerManager.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
static class PrivateServerManager
|
||||
{
|
||||
public static void Start()
|
||||
{
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
KillUnusedServers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void KillUnusedServers()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
Supervisor/Program.cs
Normal file
27
Supervisor/Program.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Model;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
class Program
|
||||
{
|
||||
public static readonly State State = new State();
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
NBloodServerListener.StartListening();
|
||||
WebApiListener.StartListening();
|
||||
|
||||
if (args.Length > 0)
|
||||
PublicServerManager.Start(args[0]);
|
||||
|
||||
PrivateServerManager.Start();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
Supervisor/Properties/launchSettings.json
Normal file
7
Supervisor/Properties/launchSettings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Supervisor": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
74
Supervisor/PublicServerManager.cs
Normal file
74
Supervisor/PublicServerManager.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using Common;
|
||||
using Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
class PublicServerManager
|
||||
{
|
||||
public static void Start(string nbloodPath)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
KillOrphanedServers();
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
LaunchNewServersWhenNeeded(nbloodPath);
|
||||
Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void KillOrphanedServers()
|
||||
{
|
||||
foreach (var process in Process.GetProcessesByName("nblood_server"))
|
||||
{
|
||||
if (!Program.State.Servers.Values.Any(s => s.ProcessId == process.Id))
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void LaunchNewServersWhenNeeded(string nbloodPath)
|
||||
{
|
||||
const int maxPlayers = 8;
|
||||
|
||||
for (int i = 3; i <= maxPlayers; i++)
|
||||
{
|
||||
if (IsNewServerNeeded(i))
|
||||
{
|
||||
int port = PortUtils.GetPort();
|
||||
var process = Process.Start(nbloodPath, $"-server {i} -port {port}");
|
||||
Program.State.Servers.AddOrUpdate(port, new Server()
|
||||
{
|
||||
Port = port,
|
||||
ProcessId = process.Id,
|
||||
MaximumPlayers = i,
|
||||
CurrentPlayers = 1,
|
||||
},
|
||||
(prt, server) =>
|
||||
{
|
||||
server.ProcessId = process.Id;
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNewServerNeeded(int i)
|
||||
{
|
||||
return !Program.State.Servers.Values.Any(s =>
|
||||
!s.IsPrivate && s.MaximumPlayers == i && s.CurrentPlayers < s.MaximumPlayers);
|
||||
}
|
||||
}
|
||||
}
|
19
Supervisor/StringUtils.cs
Normal file
19
Supervisor/StringUtils.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
static class StringUtils
|
||||
{
|
||||
public static string[] SplitMessage(this string message)
|
||||
{
|
||||
return message.Substring(1)
|
||||
.Split('\t', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => e.Trim('\0'))
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
13
Supervisor/Supervisor.csproj
Normal file
13
Supervisor/Supervisor.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
85
Supervisor/WebApiListener.cs
Normal file
85
Supervisor/WebApiListener.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Supervisor
|
||||
{
|
||||
static class WebApiListener
|
||||
{
|
||||
private const int listenPort = 11028;
|
||||
private static readonly IPEndPoint remoteIP = new IPEndPoint(IPAddress.Loopback, listenPort);
|
||||
private static readonly UdpClient udpClient = new UdpClient(remoteIP);
|
||||
|
||||
private static readonly Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
private static readonly IPEndPoint webApiEndPoint = new IPEndPoint(IPAddress.Loopback, 11029);
|
||||
|
||||
public static async void StartListening()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
UdpReceiveResult rst = await udpClient.ReceiveAsync();
|
||||
ProcessWebApiMessage(rst.Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessWebApiMessage(byte[] buffer)
|
||||
{
|
||||
string message = Encoding.ASCII.GetString(buffer);
|
||||
switch (message[0])
|
||||
{
|
||||
case 'A':
|
||||
ProcessGetCurrentStateRequest();
|
||||
break;
|
||||
case 'B':
|
||||
StorePrivateServerInfo(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessGetCurrentStateRequest()
|
||||
{
|
||||
var response = new StateResponse();
|
||||
response.Servers = Program.State.Servers.Values.ToList();
|
||||
|
||||
byte[] serializedResponse = ObjectToByteArray(response);
|
||||
socket.SendTo(serializedResponse, webApiEndPoint);
|
||||
}
|
||||
|
||||
private static void StorePrivateServerInfo(string message)
|
||||
{
|
||||
string[] split = message.SplitMessage();
|
||||
int port = int.Parse(split[0]);
|
||||
int processId = int.Parse(split[1]);
|
||||
Program.State.Servers.AddOrUpdate(port, new Server()
|
||||
{
|
||||
ProcessId = processId,
|
||||
IsPrivate = true
|
||||
},
|
||||
(prt, server) =>
|
||||
{
|
||||
server.ProcessId = processId;
|
||||
server.IsPrivate = true;
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] ObjectToByteArray(object obj)
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
bf.Serialize(ms, obj);
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
172
WebInterface/Controllers/NBloodController.cs
Normal file
172
WebInterface/Controllers/NBloodController.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Common;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Model;
|
||||
|
||||
namespace WebInterface.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class NBloodController : ControllerBase
|
||||
{
|
||||
private static bool _isBusy = false;
|
||||
private static DateTime _lastRefresh = DateTime.MinValue;
|
||||
private static ListServersResponse _lastServerList = null;
|
||||
private static readonly object _locker = new object();
|
||||
|
||||
private readonly ILogger<NBloodController> _logger;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
private const int listenPort = 11029;
|
||||
private static readonly IPEndPoint remoteIP = new IPEndPoint(IPAddress.Loopback, listenPort);
|
||||
private static readonly UdpClient udpClient = new UdpClient(remoteIP);
|
||||
|
||||
private static readonly Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
private static readonly IPEndPoint webApiListenerEndPoint = new IPEndPoint(IPAddress.Loopback, 11028);
|
||||
|
||||
public NBloodController(ILogger<NBloodController> logger, IConfiguration config)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("[controller]/api/startserver")]
|
||||
public StartServerResponse StartServer([FromQuery] ServerParameters parameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
while (_isBusy)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||
if (sw.Elapsed.TotalSeconds > 5)
|
||||
throw new Exception("Request timeout: the previous request hasn't finished yet.");
|
||||
}
|
||||
|
||||
_isBusy = true;
|
||||
|
||||
if (parameters.Players < 2)
|
||||
parameters.Players = 2;
|
||||
|
||||
if (parameters.ApiKey != _config.GetValue<string>("ApiKey"))
|
||||
return new StartServerResponse("Invalid ApiKey.");
|
||||
|
||||
string nbloodPath = _config.GetValue<string>("NBloodPath");
|
||||
if (!System.IO.File.Exists(nbloodPath))
|
||||
throw new Exception($"The configured path for the nblood executable is invalid.");
|
||||
|
||||
string processName = Path.GetFileNameWithoutExtension(nbloodPath);
|
||||
int serversRunning = Process.GetProcessesByName(processName).Count();
|
||||
if (serversRunning >= _config.GetValue<int>("MaximumServers"))
|
||||
return new StartServerResponse("The maximum number of servers are already running.");
|
||||
|
||||
int port = PortUtils.GetPort();
|
||||
|
||||
string args = BuildArgs(parameters, port);
|
||||
var process = Process.Start(nbloodPath, args);
|
||||
byte[] payload = Encoding.ASCII.GetBytes($"B{port}\t{process.Id}\0");
|
||||
socket.SendTo(payload, webApiListenerEndPoint);
|
||||
|
||||
_logger.LogInformation("Server started waiting for {0} players on port {1}.",
|
||||
parameters.Players, port);
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||
return new StartServerResponse(port) { CommandLine = GetCommandLine(port) };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.ToString());
|
||||
return new StartServerResponse("Unhandled exception has been occured. Check the logs for details.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildArgs(ServerParameters parameters, int port)
|
||||
{
|
||||
string args = $"-server {parameters.Players} -port {port}";
|
||||
if (parameters.IsBroadcast)
|
||||
{
|
||||
args += " -broadcast";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("[controller]/api/listservers")]
|
||||
public ListServersResponse ListServers()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (DateTime.UtcNow - _lastRefresh > TimeSpan.FromSeconds(5)
|
||||
|| _lastServerList == null)
|
||||
{
|
||||
byte[] payload = Encoding.ASCII.GetBytes($"A");
|
||||
byte[] response;
|
||||
lock (_locker)
|
||||
{
|
||||
socket.SendTo(payload, webApiListenerEndPoint);
|
||||
response = udpClient.ReceiveAsync().Result.Buffer;
|
||||
}
|
||||
|
||||
StateResponse stateResponse = (StateResponse)ByteArrayToObject(response);
|
||||
var webResponse = new ListServersResponse();
|
||||
webResponse.Servers = stateResponse.Servers.Where(s => !s.IsPrivate).Select(s => new Server()
|
||||
{
|
||||
Port = s.Port,
|
||||
IsStarted = s.IsStarted,
|
||||
CommandLine = s.CurrentPlayers == s.MaximumPlayers ? "Sorry, the game is already started." : GetCommandLine(s.Port),
|
||||
GameType = s.GameType,
|
||||
CurrentPlayers = s.CurrentPlayers,
|
||||
MaximumPlayers = s.MaximumPlayers,
|
||||
Players = s.Players.Select(p => new Player() { Name = p.Name, Score = p.Score }).ToList(),
|
||||
SpawnedAtUtc = s.SpawnedAtUtc
|
||||
}).OrderBy(s => s.MaximumPlayers).ToList();
|
||||
|
||||
_lastServerList = webResponse;
|
||||
_lastRefresh = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return _lastServerList;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.ToString());
|
||||
return new ListServersResponse("Unhandled exception has been occured. Check the logs for details.");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ByteArrayToObject(byte[] arrBytes)
|
||||
{
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
var binForm = new BinaryFormatter();
|
||||
memStream.Write(arrBytes, 0, arrBytes.Length);
|
||||
memStream.Seek(0, SeekOrigin.Begin);
|
||||
var obj = binForm.Deserialize(memStream);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCommandLine(int port)
|
||||
{
|
||||
return $"nblood -client {HttpContext.Request.Host.Host} -port {port}";
|
||||
}
|
||||
}
|
||||
}
|
26
WebInterface/Program.cs
Normal file
26
WebInterface/Program.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
20
WebInterface/Properties/launchSettings.json
Normal file
20
WebInterface/Properties/launchSettings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:61021",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"WebInterface": {
|
||||
"commandName": "Project",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
WebInterface/Startup.cs
Normal file
66
WebInterface/Startup.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
StartSupervisor();
|
||||
}
|
||||
|
||||
private void StartSupervisor()
|
||||
{
|
||||
string nbloodPath = Configuration.GetValue<string>("NBloodPath");
|
||||
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
string supervisor = "Supervisor";
|
||||
if (isWindows)
|
||||
supervisor += ".exe";
|
||||
if (!File.Exists(nbloodPath))
|
||||
throw new Exception($"Couldn't find nblood_server at {nbloodPath}");
|
||||
if (!File.Exists(supervisor))
|
||||
throw new Exception($"Couldn't find {supervisor} in {Directory.GetCurrentDirectory()}");
|
||||
Process.Start(supervisor, nbloodPath);
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
23
WebInterface/ViewModel/ListServersResponse.cs
Normal file
23
WebInterface/ViewModel/ListServersResponse.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class ListServersResponse
|
||||
{
|
||||
public IList<Server> Servers { get; set; }
|
||||
|
||||
string ErrorMessage { get; set; }
|
||||
|
||||
public ListServersResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public ListServersResponse(string errorMessage)
|
||||
{
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
13
WebInterface/ViewModel/Player.cs
Normal file
13
WebInterface/ViewModel/Player.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class Player
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
19
WebInterface/ViewModel/Server.cs
Normal file
19
WebInterface/ViewModel/Server.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
public DateTime SpawnedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public int Port { get; set; }
|
||||
public bool IsStarted { get; set; }
|
||||
public string CommandLine { get; set; }
|
||||
public int CurrentPlayers { get; set; }
|
||||
public int MaximumPlayers { get; set; }
|
||||
public string GameType { get; set; }
|
||||
public IList<Player> Players { get; set; } = new List<Player>();
|
||||
}
|
||||
}
|
14
WebInterface/ViewModel/ServerParameters.cs
Normal file
14
WebInterface/ViewModel/ServerParameters.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class ServerParameters
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public int Players { get; set; }
|
||||
public bool IsBroadcast { get; set; }
|
||||
}
|
||||
}
|
27
WebInterface/ViewModel/StartServerResponse.cs
Normal file
27
WebInterface/ViewModel/StartServerResponse.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebInterface
|
||||
{
|
||||
public class StartServerResponse
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string CommandLine { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public StartServerResponse(string errorMessage = "")
|
||||
{
|
||||
IsSuccess = false;
|
||||
ErrorMessage = errorMessage ?? "";
|
||||
}
|
||||
|
||||
public StartServerResponse(int port)
|
||||
{
|
||||
IsSuccess = true;
|
||||
Port = port;
|
||||
}
|
||||
}
|
||||
}
|
18
WebInterface/WebInterface.csproj
Normal file
18
WebInterface/WebInterface.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
9
WebInterface/appsettings.Development.json
Normal file
9
WebInterface/appsettings.Development.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
13
WebInterface/appsettings.json
Normal file
13
WebInterface/appsettings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"MaximumServers": 20,
|
||||
"NBloodPath": "\\path\\to\\nblood_server",
|
||||
"ApiKey": "!!! CHANGE ME !!!"
|
||||
}
|
Loading…
Reference in New Issue
Block a user