mirror of
https://github.com/CommonLoon102/NBloodServerSupervisor.git
synced 2025-01-03 08:32:45 +01:00
add man-hours played statistic (#4)
This commit is contained in:
parent
a6dea8efbc
commit
96ae1a29b2
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Model
|
namespace Model
|
||||||
{
|
{
|
||||||
@ -8,6 +7,7 @@ namespace Model
|
|||||||
public class Server
|
public class Server
|
||||||
{
|
{
|
||||||
public DateTime SpawnedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime SpawnedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? LastCollectionUtc { get; set; }
|
||||||
public DateTime LastHeartBeatUtc { get; set; }
|
public DateTime LastHeartBeatUtc { get; set; }
|
||||||
public int ProcessId { get; set; }
|
public int ProcessId { get; set; }
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
@ -7,6 +7,15 @@ namespace Model
|
|||||||
{
|
{
|
||||||
public class State
|
public class State
|
||||||
{
|
{
|
||||||
|
private TimeSpan playtime = new TimeSpan(0);
|
||||||
|
|
||||||
public ConcurrentDictionary<int, Server> Servers { get; } = new ConcurrentDictionary<int, Server>();
|
public ConcurrentDictionary<int, Server> Servers { get; } = new ConcurrentDictionary<int, Server>();
|
||||||
|
public DateTime CreatedAtUtc { get; } = DateTime.UtcNow;
|
||||||
|
public TimeSpan Playtime => playtime;
|
||||||
|
|
||||||
|
public void IncreasePlaytime(TimeSpan timeToAdd)
|
||||||
|
{
|
||||||
|
playtime += timeToAdd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,7 @@ namespace Model
|
|||||||
public class StateResponse
|
public class StateResponse
|
||||||
{
|
{
|
||||||
public IList<Server> Servers { get; set; }
|
public IList<Server> Servers { get; set; }
|
||||||
|
public int ManMinutesPlayed { get; set; }
|
||||||
|
public DateTime RunningSinceUtc { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@ namespace Supervisor
|
|||||||
WebApiListener.StartListening();
|
WebApiListener.StartListening();
|
||||||
PublicServerManager.Start();
|
PublicServerManager.Start();
|
||||||
PrivateServerManager.Start();
|
PrivateServerManager.Start();
|
||||||
|
StatisticsManager.Start();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
RemoveCrashedServers();
|
RemoveCrashedServers();
|
||||||
|
42
Supervisor/StatisticsManager.cs
Normal file
42
Supervisor/StatisticsManager.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Supervisor
|
||||||
|
{
|
||||||
|
class StatisticsManager
|
||||||
|
{
|
||||||
|
public static void Start()
|
||||||
|
{
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||||
|
UpdatePlaytime();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdatePlaytime()
|
||||||
|
{
|
||||||
|
TimeSpan timeToAdd = new TimeSpan(0);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var server in Program.State.Servers.Values.Where(s => s.IsStarted))
|
||||||
|
{
|
||||||
|
if (server.LastCollectionUtc.HasValue)
|
||||||
|
{
|
||||||
|
var elapsedTime = now - server.LastCollectionUtc.Value;
|
||||||
|
timeToAdd += elapsedTime * (server.CurrentPlayers - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.LastCollectionUtc = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
Program.State.IncreasePlaytime(timeToAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -47,8 +47,12 @@ namespace Supervisor
|
|||||||
|
|
||||||
private static void ProcessGetCurrentStateRequest()
|
private static void ProcessGetCurrentStateRequest()
|
||||||
{
|
{
|
||||||
var response = new StateResponse();
|
StateResponse response = new StateResponse
|
||||||
response.Servers = Program.State.Servers.Values.ToList();
|
{
|
||||||
|
Servers = Program.State.Servers.Values.ToList(),
|
||||||
|
ManMinutesPlayed = (int)Math.Floor(Program.State.Playtime.TotalMinutes),
|
||||||
|
RunningSinceUtc = Program.State.CreatedAtUtc,
|
||||||
|
};
|
||||||
|
|
||||||
byte[] serializedResponse = ObjectToByteArray(response);
|
byte[] serializedResponse = ObjectToByteArray(response);
|
||||||
socket.SendTo(serializedResponse, webApiEndPoint);
|
socket.SendTo(serializedResponse, webApiEndPoint);
|
||||||
|
@ -9,17 +9,20 @@ namespace WebInterface.Controllers
|
|||||||
{
|
{
|
||||||
public class HomeController : Controller
|
public class HomeController : Controller
|
||||||
{
|
{
|
||||||
IListServersService _serversList;
|
IStateService _stateService;
|
||||||
|
|
||||||
public HomeController(IListServersService serversList)
|
public HomeController(IStateService stateService)
|
||||||
{
|
{
|
||||||
_serversList = serversList;
|
_stateService = stateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("nblood/home")]
|
[Route("nblood/home")]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
var viewModel = _serversList.ListServers(HttpContext.Request.Host.Host).Servers;
|
var servers = _stateService.ListServers(HttpContext.Request.Host.Host).Servers;
|
||||||
|
var stats = _stateService.GetStatistics();
|
||||||
|
|
||||||
|
var viewModel = new HomeViewModel(servers, stats.RunningSinceUtc, stats.ManMinutesPlayed);
|
||||||
return View(viewModel);
|
return View(viewModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,12 @@ namespace WebInterface.Controllers
|
|||||||
|
|
||||||
private readonly ILogger<NBloodController> _logger;
|
private readonly ILogger<NBloodController> _logger;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly IListServersService _listServersService;
|
private readonly IStateService _listServersService;
|
||||||
|
|
||||||
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, IListServersService listServersService)
|
public NBloodController(ILogger<NBloodController> logger, IConfiguration config, IStateService listServersService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
13
WebInterface/Models/GetStatisticsResponse.cs
Normal file
13
WebInterface/Models/GetStatisticsResponse.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WebInterface
|
||||||
|
{
|
||||||
|
public class GetStatisticsResponse
|
||||||
|
{
|
||||||
|
public DateTime RunningSinceUtc { get; set; }
|
||||||
|
public int ManMinutesPlayed { get; set; }
|
||||||
|
}
|
||||||
|
}
|
22
WebInterface/Models/HomeViewModel.cs
Normal file
22
WebInterface/Models/HomeViewModel.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WebInterface
|
||||||
|
{
|
||||||
|
public class HomeViewModel
|
||||||
|
{
|
||||||
|
public IEnumerable<Server> Servers { get; }
|
||||||
|
public string RunningSinceUtc { get; }
|
||||||
|
public string ManHoursPlayed { get; }
|
||||||
|
|
||||||
|
public HomeViewModel(IEnumerable<Server> servers, DateTime runningSinceUtc, int manHoursPlayed)
|
||||||
|
{
|
||||||
|
Servers = servers;
|
||||||
|
RunningSinceUtc = runningSinceUtc.ToString("r");
|
||||||
|
ManHoursPlayed = (manHoursPlayed / 60f).ToString("n2", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,9 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace WebInterface.Services
|
namespace WebInterface.Services
|
||||||
{
|
{
|
||||||
public interface IListServersService
|
public interface IStateService
|
||||||
{
|
{
|
||||||
ListServersResponse ListServers(string host);
|
ListServersResponse ListServers(string host);
|
||||||
|
GetStatisticsResponse GetStatistics();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,64 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
82
WebInterface/Services/StateService.cs
Normal file
82
WebInterface/Services/StateService.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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 StateService : IStateService
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
StateResponse stateResponse = RequestState();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetStatisticsResponse GetStatistics()
|
||||||
|
{
|
||||||
|
StateResponse stateResponse = RequestState();
|
||||||
|
var statisticsResponse = new GetStatisticsResponse()
|
||||||
|
{
|
||||||
|
ManMinutesPlayed = stateResponse.ManMinutesPlayed,
|
||||||
|
RunningSinceUtc = stateResponse.RunningSinceUtc
|
||||||
|
};
|
||||||
|
|
||||||
|
return statisticsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StateResponse RequestState()
|
||||||
|
{
|
||||||
|
byte[] payload = Encoding.ASCII.GetBytes($"A");
|
||||||
|
byte[] response;
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
socket.SendTo(payload, webApiListenerEndPoint);
|
||||||
|
response = udpClient.ReceiveAsync().Result.Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
StateResponse stateResponse = (StateResponse)ByteArrayToObject(response);
|
||||||
|
return stateResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,7 @@ namespace WebInterface
|
|||||||
{
|
{
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
services.AddControllersWithViews();
|
services.AddControllersWithViews();
|
||||||
services.Add(new ServiceDescriptor(typeof(IListServersService), typeof(ListServersService), ServiceLifetime.Singleton));
|
services.Add(new ServiceDescriptor(typeof(IStateService), typeof(StateService), 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.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@model IEnumerable<WebInterface.Server>
|
@model WebInterface.HomeViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
Func<string, Microsoft.AspNetCore.Html.IHtmlContent>
|
Func<string, Microsoft.AspNetCore.Html.IHtmlContent>
|
||||||
ListServers = @<div>
|
ListServers = @<div>
|
||||||
@foreach (var server in Model.Where(s => s.Mod == item).OrderBy(s => s.MaximumPlayers))
|
@foreach (var server in Model.Servers.Where(s => s.Mod == item).OrderBy(s => s.MaximumPlayers))
|
||||||
{
|
{
|
||||||
<div style="border:2px solid cornflowerblue;">
|
<div style="border:2px solid cornflowerblue;">
|
||||||
<p style="font-weight:bold">Players: @(server.CurrentPlayers - 1)/@(server.MaximumPlayers - 1)</p>
|
<p style="font-weight:bold">Players: @(server.CurrentPlayers - 1)/@(server.MaximumPlayers - 1)</p>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
border: dashed 4px red;
|
border: dashed 4px red;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.version-text {
|
.bolder {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -63,9 +63,11 @@
|
|||||||
<h1>The client EXE</h1>
|
<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>
|
<p>Use this exe to connect: <a href="https://lerppu.net/wannabethesis/nblood/20200113-2073/">https://lerppu.net/wannabethesis/nblood/20200113-2073/</a></p>
|
||||||
|
|
||||||
|
<p><span class="bolder" style="font-size:larger">@Model.ManHoursPlayed</span> man-hours played since @Model.RunningSinceUtc</p>
|
||||||
|
|
||||||
<h2>@b 1.21</h2>
|
<h2>@b 1.21</h2>
|
||||||
<details>
|
<details>
|
||||||
<summary>Show @b <span class="version-text">version 1.21</span> servers</summary>
|
<summary>Show @b <span class="bolder">version 1.21</span> servers</summary>
|
||||||
<p>The below files must be in your Blood directory.</p>
|
<p>The below files must be in your Blood directory.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>BLOOD.INI</li>
|
<li>BLOOD.INI</li>
|
||||||
@ -97,7 +99,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<h2>@dw 1.6.10</h2>
|
<h2>@dw 1.6.10</h2>
|
||||||
<details>
|
<details>
|
||||||
<summary>Show @dw <span class="version-text">version 1.6.10</span> servers</summary>
|
<summary>Show @dw <span class="bolder">version 1.6.10</span> servers</summary>
|
||||||
<p>The below files must be in your Blood directory.</p>
|
<p>The below files must be in your Blood directory.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>dw.ini</li>
|
<li>dw.ini</li>
|
||||||
@ -111,7 +113,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<h2>@twoira 1.0.1</h2>
|
<h2>@twoira 1.0.1</h2>
|
||||||
<details>
|
<details>
|
||||||
<summary>Show @twoira <span class="version-text">version 1.0.1</span> servers</summary>
|
<summary>Show @twoira <span class="bolder">version 1.0.1</span> servers</summary>
|
||||||
<p>@twoira </p>
|
<p>@twoira </p>
|
||||||
<p>The below folder (TWOIRA) must be in your Blood directory, and inside that the other additional files.</p>
|
<p>The below folder (TWOIRA) must be in your Blood directory, and inside that the other additional files.</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -137,7 +139,7 @@
|
|||||||
</details>
|
</details>
|
||||||
<h2>@fo 1.3</h2>
|
<h2>@fo 1.3</h2>
|
||||||
<details>
|
<details>
|
||||||
<summary>Show @fo <span class="version-text">version 1.3</span> servers</summary>
|
<summary>Show @fo <span class="bolder">version 1.3</span> servers</summary>
|
||||||
<p>The below files must be in your Blood directory.</p>
|
<p>The below files must be in your Blood directory.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>fo.INI</li>
|
<li>fo.INI</li>
|
||||||
|
Loading…
Reference in New Issue
Block a user