Mods and home page (#3)

* add some mods

* refactor

* user friendly home page added

* refactor

* update readme

* update readme
This commit is contained in:
CommonLoon102 2020-01-27 12:23:00 +00:00 committed by GitHub
parent 74b9946969
commit a6dea8efbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 416 additions and 93 deletions

5
.gitignore vendored
View File

@ -337,4 +337,7 @@ ASALocalRun/
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
healthchecksdb
/WebInterface/.config/dotnet-tools.json
/output
/publish

View File

@ -4,4 +4,8 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using System;
using Model;
using System;
using System.Collections.Generic;
using System.Text;
@ -7,5 +8,13 @@ namespace Common
public class Constants
{
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") },
};
}
}

View File

@ -1,4 +1,5 @@
using System;
using Model;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -12,9 +13,9 @@ namespace Common
{
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,
WorkingDirectory = workingDir

21
Model/Mod.cs Normal file
View 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;
}
}
}

View File

@ -16,6 +16,7 @@ namespace Model
public int CurrentPlayers { get; set; }
public int MaximumPlayers { get; set; }
public string GameType { get; set; }
public Mod Mod { get; set; }
public IList<Player> Players { get; set; } = new List<Player>();
}
}

View File

@ -25,21 +25,52 @@ After you start the container, the following will happen:
6. Start `WebInterface.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:
- http://localhost:5000/nblood/home
- 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
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`
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.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
- SOUNDS.RFF
- SURFACE.DAT
- TILES000.ART-TILES017.ART
- TWOIRA (folder, see below)
- 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`
6. Optional: You can see the ApiKey here:
- `sudo docker run -it nblood-supervisor /bin/bash`
@ -47,12 +78,14 @@ After you start the container, the following will happen:
- `exit`
## 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
You can start new private servers via this URL:
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. You can see the port and the command line command to join in the response.
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
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.
You can see the port and the command line command to join in the response.

View File

@ -10,7 +10,7 @@ using System.Threading.Tasks;
namespace Supervisor
{
class PublicServerManager
static class PublicServerManager
{
public static void Start()
{
@ -39,36 +39,43 @@ namespace Supervisor
private static void LaunchNewServersWhenNeeded()
{
const int maxPlayers = 8;
for (int i = 3; i <= maxPlayers; i++)
foreach (Mod mod in Constants.SupportedMods.Values)
{
if (IsNewServerNeeded(i))
const int maxPlayers = 8;
for (int i = 3; i <= maxPlayers; i++)
{
int port = PortUtils.GetPort();
var process = Process.Start(NBloodServerStartInfo.Get(i, port));
Program.State.Servers.AddOrUpdate(port, new Server()
if (IsNewServerNeeded(i, mod))
{
Port = port,
ProcessId = process.Id,
MaximumPlayers = i,
CurrentPlayers = 1,
},
(prt, server) =>
{
server.ProcessId = process.Id;
return server;
});
int port = PortUtils.GetPort();
var process = Process.Start(NBloodServerStartInfo.Get(i, port, mod));
Program.State.Servers.AddOrUpdate(port, new Server()
{
Port = port,
ProcessId = process.Id,
MaximumPlayers = i,
CurrentPlayers = 1,
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 =>
!s.IsPrivate && s.MaximumPlayers == i && s.CurrentPlayers < s.MaximumPlayers);
!s.IsPrivate
&& s.Mod.Name == mod.Name
&& s.MaximumPlayers == i
&& s.CurrentPlayers < s.MaximumPlayers);
}
}
}

View 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}";
}
}
}

View 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);
}
}
}

View File

@ -1,20 +1,17 @@
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 Common;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
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
{
@ -24,22 +21,19 @@ namespace WebInterface.Controllers
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 readonly IListServersService _listServersService;
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)
public NBloodController(ILogger<NBloodController> logger, IConfiguration config, IListServersService listServersService)
{
_logger = logger;
_config = config;
_listServersService = listServersService;
}
[HttpGet]
@ -69,9 +63,10 @@ namespace WebInterface.Controllers
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));
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);
@ -79,7 +74,7 @@ namespace WebInterface.Controllers
parameters.Players, port);
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)
{
@ -98,32 +93,10 @@ namespace WebInterface.Controllers
{
try
{
if (DateTime.UtcNow - _lastRefresh > TimeSpan.FromSeconds(5)
if (DateTime.UtcNow - _lastRefresh > TimeSpan.FromSeconds(1)
|| _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;
_lastServerList = _listServersService.ListServers(HttpContext.Request.Host.Host);
_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())
{
var binForm = new BinaryFormatter();
memStream.Write(arrBytes, 0, arrBytes.Length);
memStream.Seek(0, SeekOrigin.Begin);
var obj = binForm.Deserialize(memStream);
return obj;
}
}
if (string.IsNullOrWhiteSpace(modName))
return Constants.SupportedMods["BLOOD"];
private string GetCommandLine(int port)
{
return $"nblood -client {HttpContext.Request.Host.Host} -port {port}";
if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
throw new Exception("This mod is not supported: " + modName);
return Constants.SupportedMods[modName.ToUpper()];
}
}
}

View File

@ -14,6 +14,7 @@ namespace WebInterface
public int CurrentPlayers { get; set; }
public int MaximumPlayers { get; set; }
public string GameType { get; set; }
public string Mod { get; set; }
public IList<Player> Players { get; set; } = new List<Player>();
}
}

View File

@ -9,5 +9,6 @@ namespace WebInterface
{
public string ApiKey { get; set; }
public int Players { get; set; }
public string ModName { get; set; }
}
}

View 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);
}
}

View 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;
}
}
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WebInterface.Services;
namespace WebInterface
{
@ -37,6 +38,8 @@ namespace WebInterface
public void ConfigureServices(IServiceCollection services)
{
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.

View 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>

View File

@ -7,6 +7,6 @@
}
},
"AllowedHosts": "*",
"MaximumServers": 20,
"MaximumServers": 80,
"ApiKey": "CHANGEME"
}