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.Collections.Generic;
using System.Text;
namespace Model
{
@ -8,6 +7,7 @@ namespace Model
public class Server
{
public DateTime SpawnedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? LastCollectionUtc { get; set; }
public DateTime LastHeartBeatUtc { get; set; }
public int ProcessId { get; set; }
public int Port { get; set; }

View File

@ -7,6 +7,15 @@ namespace Model
{
public class State
{
private TimeSpan playtime = new TimeSpan(0);
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 IList<Server> Servers { get; set; }
public int ManMinutesPlayed { get; set; }
public DateTime RunningSinceUtc { get; set; }
}
}

View File

@ -15,6 +15,7 @@ namespace Supervisor
WebApiListener.StartListening();
PublicServerManager.Start();
PrivateServerManager.Start();
StatisticsManager.Start();
while (true)
{

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()
{
var response = new StateResponse();
response.Servers = Program.State.Servers.Values.ToList();
StateResponse response = new StateResponse
{
Servers = Program.State.Servers.Values.ToList(),
ManMinutesPlayed = (int)Math.Floor(Program.State.Playtime.TotalMinutes),
RunningSinceUtc = Program.State.CreatedAtUtc,
};
byte[] serializedResponse = ObjectToByteArray(response);
socket.SendTo(serializedResponse, webApiEndPoint);

View File

@ -9,17 +9,20 @@ namespace WebInterface.Controllers
{
public class HomeController : Controller
{
IListServersService _serversList;
IStateService _stateService;
public HomeController(IListServersService serversList)
public HomeController(IStateService stateService)
{
_serversList = serversList;
_stateService = stateService;
}
[Route("nblood/home")]
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);
}
}

View File

@ -24,12 +24,12 @@ namespace WebInterface.Controllers
private readonly ILogger<NBloodController> _logger;
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 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;
_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
{
public interface IListServersService
public interface IStateService
{
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.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.

View File

@ -1,4 +1,4 @@
@model IEnumerable<WebInterface.Server>
@model WebInterface.HomeViewModel
@{
Layout = null;
@ -10,7 +10,7 @@
Func<string, Microsoft.AspNetCore.Html.IHtmlContent>
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;">
<p style="font-weight:bold">Players: @(server.CurrentPlayers - 1)/@(server.MaximumPlayers - 1)</p>
@ -54,7 +54,7 @@
border: dashed 4px red;
font-weight: bold;
}
.version-text {
.bolder {
font-weight: bolder;
}
</style>
@ -63,9 +63,11 @@
<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><span class="bolder" style="font-size:larger">@Model.ManHoursPlayed</span> man-hours played since @Model.RunningSinceUtc</p>
<h2>@b 1.21</h2>
<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>
<ul>
<li>BLOOD.INI</li>
@ -97,7 +99,7 @@
<br />
<h2>@dw 1.6.10</h2>
<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>
<ul>
<li>dw.ini</li>
@ -111,7 +113,7 @@
<br />
<h2>@twoira 1.0.1</h2>
<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>The below folder (TWOIRA) must be in your Blood directory, and inside that the other additional files.</p>
<ul>
@ -137,7 +139,7 @@
</details>
<h2>@fo 1.3</h2>
<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>
<ul>
<li>fo.INI</li>