diff --git a/Model/Server.cs b/Model/Server.cs index 244fa09..37e29fe 100644 --- a/Model/Server.cs +++ b/Model/Server.cs @@ -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; } diff --git a/Model/State.cs b/Model/State.cs index 13c09ce..c03b6e5 100644 --- a/Model/State.cs +++ b/Model/State.cs @@ -7,6 +7,15 @@ namespace Model { public class State { + private TimeSpan playtime = new TimeSpan(0); + public ConcurrentDictionary Servers { get; } = new ConcurrentDictionary(); + public DateTime CreatedAtUtc { get; } = DateTime.UtcNow; + public TimeSpan Playtime => playtime; + + public void IncreasePlaytime(TimeSpan timeToAdd) + { + playtime += timeToAdd; + } } } diff --git a/Model/StateResponse.cs b/Model/StateResponse.cs index ae514b3..446a2ce 100644 --- a/Model/StateResponse.cs +++ b/Model/StateResponse.cs @@ -8,5 +8,7 @@ namespace Model public class StateResponse { public IList Servers { get; set; } + public int ManMinutesPlayed { get; set; } + public DateTime RunningSinceUtc { get; set; } } } diff --git a/Supervisor/Program.cs b/Supervisor/Program.cs index 1107bd2..f074955 100644 --- a/Supervisor/Program.cs +++ b/Supervisor/Program.cs @@ -15,7 +15,8 @@ namespace Supervisor WebApiListener.StartListening(); PublicServerManager.Start(); PrivateServerManager.Start(); - + StatisticsManager.Start(); + while (true) { RemoveCrashedServers(); diff --git a/Supervisor/StatisticsManager.cs b/Supervisor/StatisticsManager.cs new file mode 100644 index 0000000..3c67473 --- /dev/null +++ b/Supervisor/StatisticsManager.cs @@ -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); + } + } +} diff --git a/Supervisor/WebApiListener.cs b/Supervisor/WebApiListener.cs index 60cd587..377760c 100644 --- a/Supervisor/WebApiListener.cs +++ b/Supervisor/WebApiListener.cs @@ -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); diff --git a/WebInterface/Controllers/HomeController.cs b/WebInterface/Controllers/HomeController.cs index 5c749c7..4b20572 100644 --- a/WebInterface/Controllers/HomeController.cs +++ b/WebInterface/Controllers/HomeController.cs @@ -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); } } diff --git a/WebInterface/Controllers/NBloodController.cs b/WebInterface/Controllers/NBloodController.cs index 1ea2607..3f292a5 100644 --- a/WebInterface/Controllers/NBloodController.cs +++ b/WebInterface/Controllers/NBloodController.cs @@ -24,12 +24,12 @@ namespace WebInterface.Controllers private readonly ILogger _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 logger, IConfiguration config, IListServersService listServersService) + public NBloodController(ILogger logger, IConfiguration config, IStateService listServersService) { _logger = logger; _config = config; diff --git a/WebInterface/Models/GetStatisticsResponse.cs b/WebInterface/Models/GetStatisticsResponse.cs new file mode 100644 index 0000000..9c8df69 --- /dev/null +++ b/WebInterface/Models/GetStatisticsResponse.cs @@ -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; } + } +} diff --git a/WebInterface/Models/HomeViewModel.cs b/WebInterface/Models/HomeViewModel.cs new file mode 100644 index 0000000..a7b5e44 --- /dev/null +++ b/WebInterface/Models/HomeViewModel.cs @@ -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 Servers { get; } + public string RunningSinceUtc { get; } + public string ManHoursPlayed { get; } + + public HomeViewModel(IEnumerable servers, DateTime runningSinceUtc, int manHoursPlayed) + { + Servers = servers; + RunningSinceUtc = runningSinceUtc.ToString("r"); + ManHoursPlayed = (manHoursPlayed / 60f).ToString("n2", CultureInfo.InvariantCulture); + } + } +} diff --git a/WebInterface/Services/IListServersService.cs b/WebInterface/Services/IStateService.cs similarity index 71% rename from WebInterface/Services/IListServersService.cs rename to WebInterface/Services/IStateService.cs index 881873a..957524d 100644 --- a/WebInterface/Services/IListServersService.cs +++ b/WebInterface/Services/IStateService.cs @@ -5,8 +5,9 @@ using System.Threading.Tasks; namespace WebInterface.Services { - public interface IListServersService + public interface IStateService { ListServersResponse ListServers(string host); + GetStatisticsResponse GetStatistics(); } } diff --git a/WebInterface/Services/ListServersService.cs b/WebInterface/Services/ListServersService.cs deleted file mode 100644 index 4c95c95..0000000 --- a/WebInterface/Services/ListServersService.cs +++ /dev/null @@ -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; - } - } - } -} diff --git a/WebInterface/Services/StateService.cs b/WebInterface/Services/StateService.cs new file mode 100644 index 0000000..f7221e5 --- /dev/null +++ b/WebInterface/Services/StateService.cs @@ -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; + } + } + } +} diff --git a/WebInterface/Startup.cs b/WebInterface/Startup.cs index 30a51a6..e7199c3 100644 --- a/WebInterface/Startup.cs +++ b/WebInterface/Startup.cs @@ -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. diff --git a/WebInterface/Views/Home/Index.cshtml b/WebInterface/Views/Home/Index.cshtml index 0975292..6350de1 100644 --- a/WebInterface/Views/Home/Index.cshtml +++ b/WebInterface/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model WebInterface.HomeViewModel @{ Layout = null; @@ -10,7 +10,7 @@ Func ListServers = @
- @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)) {

Players: @(server.CurrentPlayers - 1)/@(server.MaximumPlayers - 1)

@@ -54,7 +54,7 @@ border: dashed 4px red; font-weight: bold; } - .version-text { + .bolder { font-weight: bolder; } @@ -63,9 +63,11 @@

The client EXE

Use this exe to connect: https://lerppu.net/wannabethesis/nblood/20200113-2073/

+

@Model.ManHoursPlayed man-hours played since @Model.RunningSinceUtc

+

@b 1.21

- Show @b version 1.21 servers + Show @b version 1.21 servers

The below files must be in your Blood directory.

  • BLOOD.INI
  • @@ -97,7 +99,7 @@

    @dw 1.6.10

    - Show @dw version 1.6.10 servers + Show @dw version 1.6.10 servers

    The below files must be in your Blood directory.

    • dw.ini
    • @@ -111,7 +113,7 @@

      @twoira 1.0.1

      - Show @twoira version 1.0.1 servers + Show @twoira version 1.0.1 servers

      @twoira

      The below folder (TWOIRA) must be in your Blood directory, and inside that the other additional files.

        @@ -137,7 +139,7 @@

      @fo 1.3

      - Show @fo version 1.3 servers + Show @fo version 1.3 servers

      The below files must be in your Blood directory.

      • fo.INI