mirror of
				https://github.com/CommonLoon102/NBloodServerSupervisor.git
				synced 2025-11-03 23:37:25 +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/
 | 
			
		||||
 | 
			
		||||
# BeatPulse healthcheck temp database
 | 
			
		||||
healthchecksdb
 | 
			
		||||
healthchecksdb
 | 
			
		||||
/WebInterface/.config/dotnet-tools.json
 | 
			
		||||
/output
 | 
			
		||||
/publish
 | 
			
		||||
 | 
			
		||||
@ -4,4 +4,8 @@
 | 
			
		||||
    <TargetFramework>netstandard2.0</TargetFramework>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <ProjectReference Include="..\Model\Model.csproj" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 | 
			
		||||
@ -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") },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										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 MaximumPlayers { get; set; }
 | 
			
		||||
        public string GameType { get; set; }
 | 
			
		||||
        public Mod Mod { get; set; }
 | 
			
		||||
        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`
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 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()];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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>();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -9,5 +9,6 @@ namespace WebInterface
 | 
			
		||||
    {
 | 
			
		||||
        public string ApiKey { 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.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.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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": "*",
 | 
			
		||||
    "MaximumServers": 20,
 | 
			
		||||
    "MaximumServers": 80,
 | 
			
		||||
    "ApiKey": "CHANGEME"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user