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.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; }
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ namespace Supervisor
|
||||
WebApiListener.StartListening();
|
||||
PublicServerManager.Start();
|
||||
PrivateServerManager.Start();
|
||||
StatisticsManager.Start();
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
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()
|
||||
{
|
||||
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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
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
|
||||
{
|
||||
public interface IListServersService
|
||||
public interface IStateService
|
||||
{
|
||||
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.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.
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user