add man-hours played statistic (#4)

This commit is contained in:
CommonLoon102 2020-01-27 13:48:53 +00:00 committed by GitHub
parent a6dea8efbc
commit 96ae1a29b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 200 additions and 83 deletions

View File

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

View File

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

View File

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

View File

@ -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();

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

View File

@ -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);

View File

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

View File

@ -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;

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

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

View File

@ -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();
} }
} }

View File

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

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

View File

@ -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.

View File

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