mirror of
https://github.com/CommonLoon102/NBloodServerSupervisor.git
synced 2024-12-22 10:42:54 +01:00
Mods and home page (#3)
* add some mods * refactor * user friendly home page added * refactor * update readme * update readme
This commit is contained in:
parent
74b9946969
commit
a6dea8efbc
5
.gitignore
vendored
5
.gitignore
vendored
@ -337,4 +337,7 @@ ASALocalRun/
|
|||||||
.localhistory/
|
.localhistory/
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
# BeatPulse healthcheck temp database
|
||||||
healthchecksdb
|
healthchecksdb
|
||||||
|
/WebInterface/.config/dotnet-tools.json
|
||||||
|
/output
|
||||||
|
/publish
|
||||||
|
@ -4,4 +4,8 @@
|
|||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Model\Model.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Model;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@ -7,5 +8,13 @@ namespace Common
|
|||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public const string NBloodExecutable = "nblood_server";
|
public const string NBloodExecutable = "nblood_server";
|
||||||
|
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
|
||||||
|
{
|
||||||
|
{ "BLOOD", new Mod("BLOOD", "Blood", "") },
|
||||||
|
{ "CRYPTIC", new Mod("CRYPTIC", "Cryptic Passage", "-ini CRYPTIC.INI") },
|
||||||
|
{ "DW", new Mod("DW", "Death Wish", "-ini dw.ini") },
|
||||||
|
{ "FO", new Mod("FO", "Fleshed Out", "-ini fo.ini") },
|
||||||
|
{ "TWOIRA", new Mod("TWOIRA", "The Way of Ira", "-ini TWOIRA/twoira.ini -j=TWOIRA") },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Model;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -12,9 +13,9 @@ namespace Common
|
|||||||
{
|
{
|
||||||
private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
|
private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
|
||||||
|
|
||||||
public static ProcessStartInfo Get(int maxPlayers, int port)
|
public static ProcessStartInfo Get(int maxPlayers, int port, Mod mod)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo(GetExecutable(), $"-server {maxPlayers} -port {port} -pname Server")
|
var psi = new ProcessStartInfo(GetExecutable(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}")
|
||||||
{
|
{
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
WorkingDirectory = workingDir
|
WorkingDirectory = workingDir
|
||||||
|
21
Model/Mod.cs
Normal file
21
Model/Mod.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Model
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class Mod
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string FriendlyName { get; }
|
||||||
|
public string CommandLine { get; }
|
||||||
|
|
||||||
|
public Mod(string name, string friendlyName, string cmdLine)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
FriendlyName = friendlyName;
|
||||||
|
CommandLine = cmdLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ namespace Model
|
|||||||
public int CurrentPlayers { get; set; }
|
public int CurrentPlayers { get; set; }
|
||||||
public int MaximumPlayers { get; set; }
|
public int MaximumPlayers { get; set; }
|
||||||
public string GameType { get; set; }
|
public string GameType { get; set; }
|
||||||
|
public Mod Mod { get; set; }
|
||||||
public IList<Player> Players { get; set; } = new List<Player>();
|
public IList<Player> Players { get; set; } = new List<Player>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
49
README.md
49
README.md
@ -25,21 +25,52 @@ After you start the container, the following will happen:
|
|||||||
6. Start `WebInterface.exe`
|
6. Start `WebInterface.exe`
|
||||||
7. Attach the debugger to `WebInterface.exe` and/or `Supervisor.exe`
|
7. Attach the debugger to `WebInterface.exe` and/or `Supervisor.exe`
|
||||||
8. You can call the following URLs with your web browser or Postman:
|
8. You can call the following URLs with your web browser or Postman:
|
||||||
|
- http://localhost:5000/nblood/home
|
||||||
- http://localhost:5000/nblood/api/listservers
|
- http://localhost:5000/nblood/api/listservers
|
||||||
- http://localhost:5000/nblood/api/startserver?players=3&ApiKey=CHANGEME
|
- http://localhost:5000/nblood/api/startserver?players=3&modName=cryptic&apiKey=CHANGEME
|
||||||
|
|
||||||
## Deploy the server onto GNU/Linux
|
## Deploy the server onto GNU/Linux
|
||||||
1. Install Docker and wget (if you don't have already), for example like this: `sudo snap install docker && sudo apt install wget -y`
|
1. Install Docker and wget (if you don't have already), for example like this: `sudo snap install docker && sudo apt install wget -y`
|
||||||
2. Download the Dockerfile: `wget https://raw.githubusercontent.com/CommonLoon102/NBloodServerSupervisor/master/Dockerfile --directory-prefix=supervisor`
|
2. Download the Dockerfile: `wget https://raw.githubusercontent.com/CommonLoon102/NBloodServerSupervisor/master/Dockerfile --directory-prefix=supervisor`
|
||||||
3. Build the Docker image: `sudo docker build -t nblood-supervisor:latest supervisor`
|
3. Build the Docker image: `sudo docker build -t nblood-supervisor:latest supervisor`
|
||||||
4. Navigate to your Blood 1.21 directory where you have these files:
|
4. Navigate to your Blood 1.21 directory where you have the below files.
|
||||||
|
The files are from stock Blood 1.21, Cryptic Passage, Death Wish 1.6.10, The Way of Ira 1.0.1, Fleshed Out 1.3
|
||||||
- BLOOD.INI
|
- BLOOD.INI
|
||||||
- BLOOD.RFF
|
- BLOOD.RFF
|
||||||
|
- CP01.MAP-CP09.MAP
|
||||||
|
- CPART07.AR_ (Fresh Supply owners need to copy tiles007.ART from `\addons\Cryptic Passage` and rename it)
|
||||||
|
- CPART15.AR_ (Fresh Supply owners need to copy tiles015.ART from `\addons\Cryptic Passage` and rename it)
|
||||||
|
- CPBB01.MAP-CPBB04.MAP
|
||||||
|
- CPSL.MAP
|
||||||
|
- CRYPTIC.INI
|
||||||
|
- dw.ini
|
||||||
|
- DWBB1.MAP-DWBB3.MAP
|
||||||
|
- DWE1M1.MAP-DWE1M12.MAP
|
||||||
|
- DWE2M1.MAP-DWE2M12.MAP
|
||||||
|
- DWE3M1.MAP-DWE3M12.MAP
|
||||||
|
- fo.INI
|
||||||
|
- fo1m1.MAP-fo1m8.MAP
|
||||||
- GUI.RFF
|
- GUI.RFF
|
||||||
- SOUNDS.RFF
|
- SOUNDS.RFF
|
||||||
- SURFACE.DAT
|
- SURFACE.DAT
|
||||||
- TILES000.ART-TILES017.ART
|
- TILES000.ART-TILES017.ART
|
||||||
|
- TWOIRA (folder, see below)
|
||||||
- VOXEL.DAT
|
- VOXEL.DAT
|
||||||
|
|
||||||
|
You need a folder in your Blood folder, named `TWOIRA`, and inside that, these files:
|
||||||
|
- IRA01.MAP
|
||||||
|
- IRA02_A.MAP
|
||||||
|
- IRA02_B.MAP
|
||||||
|
- IRA03.MAP
|
||||||
|
- IRA04.MAP
|
||||||
|
- IRA05.MAP
|
||||||
|
- IRA06.MAP
|
||||||
|
- IRA07.MAP
|
||||||
|
- IRA08.MAP
|
||||||
|
- SURFACE.DAT
|
||||||
|
- TILES18.ART
|
||||||
|
- twoira.ini
|
||||||
|
|
||||||
5. Run a Docker container from there: `sudo docker run --volume "$PWD":/supervisor/publish/blood --network=host --detach nblood-supervisor`
|
5. Run a Docker container from there: `sudo docker run --volume "$PWD":/supervisor/publish/blood --network=host --detach nblood-supervisor`
|
||||||
6. Optional: You can see the ApiKey here:
|
6. Optional: You can see the ApiKey here:
|
||||||
- `sudo docker run -it nblood-supervisor /bin/bash`
|
- `sudo docker run -it nblood-supervisor /bin/bash`
|
||||||
@ -47,12 +78,14 @@ After you start the container, the following will happen:
|
|||||||
- `exit`
|
- `exit`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
You can list the currently running public servers via this URL:
|
User friendly homepage:
|
||||||
|
http://your.ip.goes.here:23580/nblood/home
|
||||||
|
|
||||||
|
You can list the currently running public servers via this API:
|
||||||
http://your.ip.goes.here:23580/nblood/api/listservers
|
http://your.ip.goes.here:23580/nblood/api/listservers
|
||||||
|
|
||||||
You can start new private servers via this URL:
|
You can start new private servers via this API:
|
||||||
|
http://your.ip.goes.here:23580/nblood/api/startserver?players=3&modName=cryptic&apiKey=the_actual_apikey_here
|
||||||
http://your.ip.goes.here:23580/nblood/api/startserver?players=3&ApiKey=the_actual_apikey_here
|
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 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. 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.
|
||||||
|
@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Supervisor
|
namespace Supervisor
|
||||||
{
|
{
|
||||||
class PublicServerManager
|
static class PublicServerManager
|
||||||
{
|
{
|
||||||
public static void Start()
|
public static void Start()
|
||||||
{
|
{
|
||||||
@ -39,36 +39,43 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void LaunchNewServersWhenNeeded()
|
private static void LaunchNewServersWhenNeeded()
|
||||||
{
|
{
|
||||||
const int maxPlayers = 8;
|
foreach (Mod mod in Constants.SupportedMods.Values)
|
||||||
|
|
||||||
for (int i = 3; i <= maxPlayers; i++)
|
|
||||||
{
|
{
|
||||||
if (IsNewServerNeeded(i))
|
const int maxPlayers = 8;
|
||||||
|
for (int i = 3; i <= maxPlayers; i++)
|
||||||
{
|
{
|
||||||
int port = PortUtils.GetPort();
|
if (IsNewServerNeeded(i, mod))
|
||||||
var process = Process.Start(NBloodServerStartInfo.Get(i, port));
|
|
||||||
Program.State.Servers.AddOrUpdate(port, new Server()
|
|
||||||
{
|
{
|
||||||
Port = port,
|
int port = PortUtils.GetPort();
|
||||||
ProcessId = process.Id,
|
var process = Process.Start(NBloodServerStartInfo.Get(i, port, mod));
|
||||||
MaximumPlayers = i,
|
Program.State.Servers.AddOrUpdate(port, new Server()
|
||||||
CurrentPlayers = 1,
|
{
|
||||||
},
|
Port = port,
|
||||||
(prt, server) =>
|
ProcessId = process.Id,
|
||||||
{
|
MaximumPlayers = i,
|
||||||
server.ProcessId = process.Id;
|
CurrentPlayers = 1,
|
||||||
return server;
|
Mod = mod,
|
||||||
});
|
},
|
||||||
|
(prt, server) =>
|
||||||
|
{
|
||||||
|
server.ProcessId = process.Id;
|
||||||
|
server.MaximumPlayers = i;
|
||||||
|
server.CurrentPlayers = 1;
|
||||||
|
server.Mod = mod;
|
||||||
|
return server;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsNewServerNeeded(int i)
|
private static bool IsNewServerNeeded(int i, Mod mod)
|
||||||
{
|
{
|
||||||
return !Program.State.Servers.Values.Any(s =>
|
return !Program.State.Servers.Values.Any(s =>
|
||||||
!s.IsPrivate && s.MaximumPlayers == i && s.CurrentPlayers < s.MaximumPlayers);
|
!s.IsPrivate
|
||||||
|
&& s.Mod.Name == mod.Name
|
||||||
|
&& s.MaximumPlayers == i
|
||||||
|
&& s.CurrentPlayers < s.MaximumPlayers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
WebInterface/CommandLineUtils.cs
Normal file
17
WebInterface/CommandLineUtils.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
WebInterface/Controllers/HomeController.cs
Normal file
26
WebInterface/Controllers/HomeController.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using WebInterface.Services;
|
||||||
|
|
||||||
|
namespace WebInterface.Controllers
|
||||||
|
{
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
IListServersService _serversList;
|
||||||
|
|
||||||
|
public HomeController(IListServersService serversList)
|
||||||
|
{
|
||||||
|
_serversList = serversList;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("nblood/home")]
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
var viewModel = _serversList.ListServers(HttpContext.Request.Host.Host).Servers;
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,17 @@
|
|||||||
using System;
|
using Common;
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Model;
|
using Model;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using WebInterface.Services;
|
||||||
|
|
||||||
namespace WebInterface.Controllers
|
namespace WebInterface.Controllers
|
||||||
{
|
{
|
||||||
@ -24,22 +21,19 @@ namespace WebInterface.Controllers
|
|||||||
private static bool _isBusy = false;
|
private static bool _isBusy = false;
|
||||||
private static DateTime _lastRefresh = DateTime.MinValue;
|
private static DateTime _lastRefresh = DateTime.MinValue;
|
||||||
private static ListServersResponse _lastServerList = null;
|
private static ListServersResponse _lastServerList = null;
|
||||||
private static readonly object _locker = new object();
|
|
||||||
|
|
||||||
private readonly ILogger<NBloodController> _logger;
|
private readonly ILogger<NBloodController> _logger;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IListServersService _listServersService;
|
||||||
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 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||||
private static readonly IPEndPoint webApiListenerEndPoint = new IPEndPoint(IPAddress.Loopback, 11028);
|
private static readonly IPEndPoint webApiListenerEndPoint = new IPEndPoint(IPAddress.Loopback, 11028);
|
||||||
|
|
||||||
public NBloodController(ILogger<NBloodController> logger, IConfiguration config)
|
public NBloodController(ILogger<NBloodController> logger, IConfiguration config, IListServersService listServersService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_listServersService = listServersService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -69,9 +63,10 @@ namespace WebInterface.Controllers
|
|||||||
if (serversRunning >= _config.GetValue<int>("MaximumServers"))
|
if (serversRunning >= _config.GetValue<int>("MaximumServers"))
|
||||||
return new StartServerResponse("The maximum number of servers are already running.");
|
return new StartServerResponse("The maximum number of servers are already running.");
|
||||||
|
|
||||||
|
Mod mod = GetMod(parameters.ModName);
|
||||||
int port = PortUtils.GetPort();
|
int port = PortUtils.GetPort();
|
||||||
|
|
||||||
var process = Process.Start(NBloodServerStartInfo.Get(parameters.Players, port));
|
var process = Process.Start(NBloodServerStartInfo.Get(parameters.Players, port, mod));
|
||||||
byte[] payload = Encoding.ASCII.GetBytes($"B{port}\t{process.Id}\0");
|
byte[] payload = Encoding.ASCII.GetBytes($"B{port}\t{process.Id}\0");
|
||||||
socket.SendTo(payload, webApiListenerEndPoint);
|
socket.SendTo(payload, webApiListenerEndPoint);
|
||||||
|
|
||||||
@ -79,7 +74,7 @@ namespace WebInterface.Controllers
|
|||||||
parameters.Players, port);
|
parameters.Players, port);
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||||
return new StartServerResponse(port) { CommandLine = GetCommandLine(port) };
|
return new StartServerResponse(port) { CommandLine = CommandLineUtils.GetLaunchCommand(HttpContext.Request.Host.Host, port, mod) };
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -98,32 +93,10 @@ namespace WebInterface.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (DateTime.UtcNow - _lastRefresh > TimeSpan.FromSeconds(5)
|
if (DateTime.UtcNow - _lastRefresh > TimeSpan.FromSeconds(1)
|
||||||
|| _lastServerList == null)
|
|| _lastServerList == null)
|
||||||
{
|
{
|
||||||
byte[] payload = Encoding.ASCII.GetBytes($"A");
|
_lastServerList = _listServersService.ListServers(HttpContext.Request.Host.Host);
|
||||||
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;
|
_lastRefresh = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,21 +109,15 @@ namespace WebInterface.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object ByteArrayToObject(byte[] arrBytes)
|
private Mod GetMod(string modName)
|
||||||
{
|
{
|
||||||
using (var memStream = new MemoryStream())
|
if (string.IsNullOrWhiteSpace(modName))
|
||||||
{
|
return Constants.SupportedMods["BLOOD"];
|
||||||
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)
|
if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
|
||||||
{
|
throw new Exception("This mod is not supported: " + modName);
|
||||||
return $"nblood -client {HttpContext.Request.Host.Host} -port {port}";
|
|
||||||
|
return Constants.SupportedMods[modName.ToUpper()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ namespace WebInterface
|
|||||||
public int CurrentPlayers { get; set; }
|
public int CurrentPlayers { get; set; }
|
||||||
public int MaximumPlayers { get; set; }
|
public int MaximumPlayers { get; set; }
|
||||||
public string GameType { get; set; }
|
public string GameType { get; set; }
|
||||||
|
public string Mod { get; set; }
|
||||||
public IList<Player> Players { get; set; } = new List<Player>();
|
public IList<Player> Players { get; set; } = new List<Player>();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,5 +9,6 @@ namespace WebInterface
|
|||||||
{
|
{
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
public int Players { get; set; }
|
public int Players { get; set; }
|
||||||
|
public string ModName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
12
WebInterface/Services/IListServersService.cs
Normal file
12
WebInterface/Services/IListServersService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WebInterface.Services
|
||||||
|
{
|
||||||
|
public interface IListServersService
|
||||||
|
{
|
||||||
|
ListServersResponse ListServers(string host);
|
||||||
|
}
|
||||||
|
}
|
64
WebInterface/Services/ListServersService.cs
Normal file
64
WebInterface/Services/ListServersService.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Model;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.Serialization.Formatters.Binary;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace WebInterface.Services
|
||||||
|
{
|
||||||
|
public class ListServersService : IListServersService
|
||||||
|
{
|
||||||
|
private const int listenPort = 11029;
|
||||||
|
|
||||||
|
private static readonly object _locker = new object();
|
||||||
|
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 ListServersResponse ListServers(string host)
|
||||||
|
{
|
||||||
|
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 serversResponse = new ListServersResponse
|
||||||
|
{
|
||||||
|
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." : CommandLineUtils.GetLaunchCommand(host, s.Port, s.Mod),
|
||||||
|
GameType = s.GameType,
|
||||||
|
Mod = s.Mod.FriendlyName,
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
return serversResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using WebInterface.Services;
|
||||||
|
|
||||||
namespace WebInterface
|
namespace WebInterface
|
||||||
{
|
{
|
||||||
@ -37,6 +38,8 @@ namespace WebInterface
|
|||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
|
services.AddControllersWithViews();
|
||||||
|
services.Add(new ServiceDescriptor(typeof(IListServersService), typeof(ListServersService), ServiceLifetime.Singleton));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
153
WebInterface/Views/Home/Index.cshtml
Normal file
153
WebInterface/Views/Home/Index.cshtml
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
@model IEnumerable<WebInterface.Server>
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
string b = "Blood";
|
||||||
|
string cp = "Cryptic Passage";
|
||||||
|
string dw = "Death Wish";
|
||||||
|
string fo = "Fleshed Out";
|
||||||
|
string twoira = "The Way of Ira";
|
||||||
|
|
||||||
|
Func<string, Microsoft.AspNetCore.Html.IHtmlContent>
|
||||||
|
ListServers = @<div>
|
||||||
|
@foreach (var server in Model.Where(s => s.Mod == item).OrderBy(s => s.MaximumPlayers))
|
||||||
|
{
|
||||||
|
<div style="border:2px solid cornflowerblue;">
|
||||||
|
<p style="font-weight:bold">Players: @(server.CurrentPlayers - 1)/@(server.MaximumPlayers - 1)</p>
|
||||||
|
<div>
|
||||||
|
@if (server.IsStarted)
|
||||||
|
{
|
||||||
|
<p>Game Type: @server.GameType</p>
|
||||||
|
<div>
|
||||||
|
@foreach (var player in server.Players.Skip(1))
|
||||||
|
{
|
||||||
|
<p>@player.Name: @player.Score</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (server.CurrentPlayers < server.MaximumPlayers)
|
||||||
|
{
|
||||||
|
<p>Command to join: <span class="code">@server.CommandLine</span></p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>Blood servers</title>
|
||||||
|
<style>
|
||||||
|
.code {
|
||||||
|
font-family: Consolas;
|
||||||
|
background-color: black;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
border: dashed 4px red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.version-text {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: cornsilk">
|
||||||
|
<h1>The client EXE</h1>
|
||||||
|
<p>Use this exe to connect: <a href="https://lerppu.net/wannabethesis/nblood/20200113-2073/">https://lerppu.net/wannabethesis/nblood/20200113-2073/</a></p>
|
||||||
|
|
||||||
|
<h2>@b 1.21</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Show @b <span class="version-text">version 1.21</span> servers</summary>
|
||||||
|
<p>The below files must be in your Blood directory.</p>
|
||||||
|
<ul>
|
||||||
|
<li>BLOOD.INI</li>
|
||||||
|
<li>BLOOD.RFF</li>
|
||||||
|
<li>GUI.RFF</li>
|
||||||
|
<li>SOUNDS.RFF</li>
|
||||||
|
<li>SURFACE.DAT</li>
|
||||||
|
<li>TILES000.ART-TILES017.ART</li>
|
||||||
|
<li>VOXEL.DAT</li>
|
||||||
|
</ul>
|
||||||
|
@ListServers(@b)
|
||||||
|
</details>
|
||||||
|
<br />
|
||||||
|
<h2>@cp</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Show @cp servers</summary>
|
||||||
|
<p>The below files must be in your Blood directory.</p>
|
||||||
|
<ul>
|
||||||
|
<li>CP01-CP09.MAP</li>
|
||||||
|
<li>CPART07.AR_ (Fresh Supply owners need to copy tiles007.ART from <span class="code">\addons\Cryptic Passage</span> and rename it</li>
|
||||||
|
<li>CPART15.AR_ (Fresh Supply owners need to copy tiles015.ART from <span class="code">\addons\Cryptic Passage</span> and rename it</li>
|
||||||
|
<li>CPBB01.MAP-CPBB04.MAP</li>
|
||||||
|
<li>CPSL.MAP</li>
|
||||||
|
<li>CRYPTIC.INI</li>
|
||||||
|
</ul>
|
||||||
|
<p class="warning">Don't forget to send back the ferry every time at the end of the last map, otherwise you cannot go back if the boss kills you!</p>
|
||||||
|
@ListServers(@cp)
|
||||||
|
</details>
|
||||||
|
<br />
|
||||||
|
<h2>@dw 1.6.10</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Show @dw <span class="version-text">version 1.6.10</span> servers</summary>
|
||||||
|
<p>The below files must be in your Blood directory.</p>
|
||||||
|
<ul>
|
||||||
|
<li>dw.ini</li>
|
||||||
|
<li>DWBB1.MAP-DWBB3.MAP</li>
|
||||||
|
<li>DWE1M1.MAP-DWE1M12.MAP</li>
|
||||||
|
<li>DWE2M1.MAP-DWE2M12.MAP</li>
|
||||||
|
<li>DWE3M1.MAP-DWE3M12.MAP</li>
|
||||||
|
</ul>
|
||||||
|
@ListServers(@dw)
|
||||||
|
</details>
|
||||||
|
<br />
|
||||||
|
<h2>@twoira 1.0.1</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Show @twoira <span class="version-text">version 1.0.1</span> servers</summary>
|
||||||
|
<p>@twoira </p>
|
||||||
|
<p>The below folder (TWOIRA) must be in your Blood directory, and inside that the other additional files.</p>
|
||||||
|
<ul>
|
||||||
|
<li>TWOIRA</li>
|
||||||
|
<li>
|
||||||
|
<ul>
|
||||||
|
<li>IRA01.MAP</li>
|
||||||
|
<li>IRA02_A.MAP</li>
|
||||||
|
<li>IRA02_B.MAP</li>
|
||||||
|
<li>IRA03.MAP</li>
|
||||||
|
<li>IRA04.MAP</li>
|
||||||
|
<li>IRA05.MAP</li>
|
||||||
|
<li>IRA06.MAP</li>
|
||||||
|
<li>IRA07.MAP</li>
|
||||||
|
<li>IRA08.MAP</li>
|
||||||
|
<li>SURFACE.DAT</li>
|
||||||
|
<li>TILES18.ART</li>
|
||||||
|
<li>twoira.ini</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
@ListServers(twoira)
|
||||||
|
</details>
|
||||||
|
<h2>@fo 1.3</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Show @fo <span class="version-text">version 1.3</span> servers</summary>
|
||||||
|
<p>The below files must be in your Blood directory.</p>
|
||||||
|
<ul>
|
||||||
|
<li>fo.INI</li>
|
||||||
|
<li>fo1m1.MAP-fo1m8.MAP</li>
|
||||||
|
</ul>
|
||||||
|
<p class="warning">Not tested in co-op! But I hope it works! :)</p>
|
||||||
|
@ListServers(@fo)
|
||||||
|
</details>
|
||||||
|
<br />
|
||||||
|
<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>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -7,6 +7,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MaximumServers": 20,
|
"MaximumServers": 80,
|
||||||
"ApiKey": "CHANGEME"
|
"ApiKey": "CHANGEME"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user