persist client count history data across reboots and allow for configurable timespan

This commit is contained in:
RaidMax 2021-08-29 13:10:10 -05:00
parent 02e5e78f67
commit deff4f2947
22 changed files with 213 additions and 59 deletions

View File

@ -1002,10 +1002,13 @@ namespace IW4MAdmin
LastMessage = DateTime.Now - start; LastMessage = DateTime.Now - start;
lastCount = DateTime.Now; lastCount = DateTime.Now;
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
// update the player history // update the player history
if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval) if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval)
{ {
while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
appConfig.ServerDataCollectionInterval.TotalMinutes);
while ( ClientHistory.Count > maxItems)
{ {
ClientHistory.Dequeue(); ClientHistory.Dequeue();
} }

View File

@ -4,8 +4,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using SharedLibraryCore.Database;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Migration namespace IW4MAdmin.Application.Migration
{ {

View File

@ -10,6 +10,7 @@ using Data.Models.Server;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -21,14 +22,16 @@ namespace IW4MAdmin.Application.Misc
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IManager _manager; private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
private bool _inProgress; private bool _inProgress;
private TimeSpan _period; private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, IManager manager, public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IDatabaseContextFactory contextFactory) IManager manager, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_appConfig = appConfig;
_manager = manager; _manager = manager;
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
@ -42,7 +45,9 @@ namespace IW4MAdmin.Application.Misc
_logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector)); _logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector));
_inProgress = true; _inProgress = true;
_period = period ?? TimeSpan.FromMinutes(Utilities.IsDevelopment ? 1 : 5); _period = period ?? (Utilities.IsDevelopment
? TimeSpan.FromMinutes(1)
: _appConfig.ServerDataCollectionInterval);
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,6 +9,8 @@ using Data.Models.Server;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -19,16 +22,19 @@ namespace IW4MAdmin.Application.Misc
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDataValueCache<EFServerSnapshot, int> _snapshotCache; private readonly IDataValueCache<EFServerSnapshot, int> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache; private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly TimeSpan? _cacheTimeSpan = private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) TimeSpan.FromMinutes(1); Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) TimeSpan.FromMinutes(1);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, int> snapshotCache, public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, int> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache) IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
{ {
_logger = logger; _logger = logger;
_snapshotCache = snapshotCache; _snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache; _serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
} }
public async Task<int> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, public async Task<int> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
@ -45,14 +51,14 @@ namespace IW4MAdmin.Application.Misc
{ {
maxClients = await snapshots.Where(snapshot => snapshot.ServerId == serverId) maxClients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry) .Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.MaxAsync(snapshot => (int?)snapshot.ClientCount, cancellationToken) ?? 0; .MaxAsync(snapshot => (int?) snapshot.ClientCount, cancellationToken) ?? 0;
} }
else else
{ {
maxClients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry) maxClients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.GroupBy(snapshot => snapshot.PeriodBlock) .GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => grp.Sum(snapshot => (int?)snapshot.ClientCount)) .Select(grp => grp.Sum(snapshot => (int?) snapshot.ClientCount))
.MaxAsync(cancellationToken) ?? 0; .MaxAsync(cancellationToken) ?? 0;
} }
@ -95,5 +101,43 @@ namespace IW4MAdmin.Application.Misc
return (0, 0); return (0, 0);
} }
} }
public async Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_clientHistoryCache.SetCacheItem(async (set, cancellationToken) =>
{
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddHours(-12);
var history = await set.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.Select(snapshot =>
new
{
snapshot.ServerId,
snapshot.CapturedAt,
snapshot.ClientCount
})
.OrderBy(snapshot => snapshot.CapturedAt)
.ToListAsync(cancellationToken);
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
{
ServerId = byServer.Key,
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot()
{Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList()
}).ToList();
}, nameof(_clientHistoryCache), TimeSpan.MaxValue);
try
{
return await _clientHistoryCache.GetCacheItem(nameof(_clientHistoryCache), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientHistoryAsync));
return Enumerable.Empty<ClientHistoryInfo>();
}
}
} }
} }

View File

@ -8,7 +8,7 @@
<PackageId>RaidMax.IW4MAdmin.Data</PackageId> <PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title> <Title>RaidMax.IW4MAdmin.Data</Title>
<Authors /> <Authors />
<PackageVersion>1.0.5</PackageVersion> <PackageVersion>1.0.6</PackageVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -22,7 +22,9 @@ namespace Data.Helpers
public TimeSpan ExpirationTime { get; set; } public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<T>, CancellationToken, Task<V>> Getter { get; set; } public Func<DbSet<T>, CancellationToken, Task<V>> Getter { get; set; }
public V Value { get; set; } public V Value { get; set; }
public bool IsExpired => (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
public bool IsExpired => ExpirationTime != TimeSpan.MaxValue &&
(DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
} }
public DataValueCache(ILogger<DataValueCache<T, V>> logger, IDatabaseContextFactory contextFactory) public DataValueCache(ILogger<DataValueCache<T, V>> logger, IDatabaseContextFactory contextFactory)
@ -31,7 +33,8 @@ namespace Data.Helpers
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
public void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> getter, string key, TimeSpan? expirationTime = null) public void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> getter, string key,
TimeSpan? expirationTime = null)
{ {
if (_cacheStates.ContainsKey(key)) if (_cacheStates.ContainsKey(key))
{ {
@ -58,7 +61,7 @@ namespace Data.Helpers
var state = _cacheStates[keyName]; var state = _cacheStates[keyName];
if (state.IsExpired) if (state.IsExpired || state.Value == null)
{ {
await RunCacheUpdate(state, cancellationToken); await RunCacheUpdate(state, cancellationToken);
} }

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.27.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.29.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -141,7 +141,15 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName(("WEBFRONT_CONFIGURATION_ENABLE_PRIVILEGED_USER_PRIVACY"))] [LocalizedDisplayName(("WEBFRONT_CONFIGURATION_ENABLE_PRIVILEGED_USER_PRIVACY"))]
public bool EnablePrivilegedUserPrivacy { get; set; } public bool EnablePrivilegedUserPrivacy { get; set; }
[ConfigurationIgnore]
public bool EnableImplicitAccountLinking { get; set; } = false; public bool EnableImplicitAccountLinking { get; set; } = false;
[ConfigurationIgnore]
public TimeSpan MaxClientHistoryTime { get; set; } = TimeSpan.FromHours(12);
[ConfigurationIgnore]
public TimeSpan ServerDataCollectionInterval { get; set; } = TimeSpan.FromMinutes(5);
public Dictionary<Permission, string> OverridePermissionLevelNames { get; set; } = Enum public Dictionary<Permission, string> OverridePermissionLevelNames { get; set; } = Enum
.GetValues(typeof(Permission)) .GetValues(typeof(Permission))
.Cast<Permission>() .Cast<Permission>()

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace SharedLibraryCore.Dtos
{
public class ClientHistoryInfo
{
public long ServerId { get; set; }
public List<ClientCountSnapshot> ClientCounts { get; set; }
}
public class ClientCountSnapshot
{
public DateTime Time { get; set; }
public string TimeString => Time.ToString("yyyy-MM-ddTHH:mm:ssZ");
public int ClientCount { get; set; }
}
}

View File

@ -17,6 +17,7 @@ namespace SharedLibraryCore.Dtos
public List<ChatInfo> ChatHistory { get; set; } public List<ChatInfo> ChatHistory { get; set; }
public List<PlayerInfo> Players { get; set; } public List<PlayerInfo> Players { get; set; }
public Helpers.PlayerHistory[] PlayerHistory { get; set; } public Helpers.PlayerHistory[] PlayerHistory { get; set; }
public List<ClientCountSnapshot> ClientCountHistory { get; set; }
public long ID { get; set; } public long ID { get; set; }
public bool Online { get; set; } public bool Online { get; set; }
public string ConnectProtocolUrl { get; set; } public string ConnectProtocolUrl { get; set; }

View File

@ -1,4 +1,5 @@
using System; using System;
using SharedLibraryCore.Dtos;
namespace SharedLibraryCore.Helpers namespace SharedLibraryCore.Helpers
{ {
@ -31,5 +32,14 @@ namespace SharedLibraryCore.Helpers
/// Used by CanvasJS as a point on the y axis /// Used by CanvasJS as a point on the y axis
/// </summary> /// </summary>
public int y { get; } public int y { get; }
public ClientCountSnapshot ToClientCountSnapshot()
{
return new ClientCountSnapshot
{
ClientCount = y,
Time = When
};
}
} }
} }

View File

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
{ {
@ -22,8 +25,16 @@ namespace SharedLibraryCore.Interfaces
/// Gets the total number of clients connected and total clients connected in the given time frame /// Gets the total number of clients connected and total clients connected in the given time frame
/// </summary> /// </summary>
/// <param name="overPeriod">how far in the past to search</param> /// <param name="overPeriod">how far in the past to search</param>
/// <param name="token"></param> /// <param name="token">CancellationToken</param>
/// <returns></returns> /// <returns></returns>
Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default); Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default);
/// <summary>
/// Retrieves the client count and history over the given period
/// </summary>
/// <param name="overPeriod">how far in the past to search</param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default);
} }
} }

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2021.8.27.1</Version> <Version>2021.8.29.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2021.8.27.1</PackageVersion> <PackageVersion>2021.8.29.1</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -44,7 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.5" /> <PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.6" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" /> <PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,44 +1,93 @@
using Microsoft.AspNetCore.Mvc; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Microsoft.AspNetCore.Hosting.Server;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
namespace WebfrontCore.ViewComponents namespace WebfrontCore.ViewComponents
{ {
public class ServerListViewComponent : ViewComponent public class ServerListViewComponent : ViewComponent
{ {
private readonly IServerDataViewer _serverDataViewer;
private readonly ApplicationConfiguration _appConfig;
public ServerListViewComponent(IServerDataViewer serverDataViewer,
ApplicationConfiguration applicationConfiguration)
{
_serverDataViewer = serverDataViewer;
_appConfig = applicationConfiguration;
}
public IViewComponentResult Invoke(Game? game) public IViewComponentResult Invoke(Game? game)
{ {
var servers = Program.Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game); var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game);
var serverInfo = servers.Select(s => new ServerInfo() var serverInfo = new List<ServerInfo>();
foreach (var server in servers)
{ {
Name = s.Hostname, var serverId = server.GetIdForServer().Result;
ID = s.EndPoint, var clientHistory = _serverDataViewer.ClientHistoryAsync(_appConfig.MaxClientHistoryTime,
Port = s.Port, CancellationToken.None).Result
Map = s.CurrentMap.Alias, .FirstOrDefault(history => history.ServerId == serverId) ??
ClientCount = s.ClientNum, new ClientHistoryInfo
MaxClients = s.MaxClients, {
GameType = s.Gametype, ServerId = serverId
PlayerHistory = s.ClientHistory.ToArray(), };
Players = s.GetClientsAsList()
var counts = clientHistory.ClientCounts?.AsEnumerable() ?? Enumerable.Empty<ClientCountSnapshot>();
if (server.ClientHistory.Count > 0)
{
counts = counts.Union(server.ClientHistory
.Select(history => history.ToClientCountSnapshot()).Where(history =>
history.Time > clientHistory.ClientCounts.Last().Time));
}
serverInfo.Add(new ServerInfo()
{
Name = server.Hostname,
ID = server.EndPoint,
Port = server.Port,
Map = server.CurrentMap.Alias,
ClientCount = server.ClientNum,
MaxClients = server.MaxClients,
GameType = server.Gametype,
PlayerHistory = server.ClientHistory.ToArray(),
Players = server.GetClientsAsList()
.Select(p => new PlayerInfo() .Select(p => new PlayerInfo()
{ {
Name = p.Name, Name = p.Name,
ClientId = p.ClientId, ClientId = p.ClientId,
Level = p.Level.ToLocalizedLevelName(), Level = p.Level.ToLocalizedLevelName(),
LevelInt = (int)p.Level, LevelInt = (int) p.Level,
Tag = p.Tag, Tag = p.Tag,
ZScore = p.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers.StatManager.CLIENT_STATS_KEY)?.ZScore ZScore = p.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers
.StatManager
.CLIENT_STATS_KEY)?.ZScore
}).ToList(), }).ToList(),
ChatHistory = s.ChatHistory.ToList(), ChatHistory = server.ChatHistory.ToList(),
Online = !s.Throttled, ClientCountHistory =
IPAddress = $"{(s.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : s.IP)}:{s.Port}", counts.Where(history => history.Time >= DateTime.UtcNow - _appConfig.MaxClientHistoryTime)
ConnectProtocolUrl = s.EventParser.URLProtocolFormat.FormatExt(s.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : s.IP, s.Port) .ToList(),
}).ToList(); Online = !server.Throttled,
IPAddress =
$"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}",
ConnectProtocolUrl = server.EventParser.URLProtocolFormat.FormatExt(
server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP,
server.Port)
});
}
return View("_List", serverInfo); return View("_List", serverInfo);
} }
} }

View File

@ -50,5 +50,8 @@
</div> </div>
<div class="row server-history mb-4"> <div class="row server-history mb-4">
<div class="server-history-row" id="server_history_@Model.ID" data-serverid="@Model.ID" data-clienthistory='@Html.Raw(Json.Serialize(Model.PlayerHistory))' data-online="@Model.Online"></div> <div class="server-history-row" id="server_history_@Model.ID" data-serverid="@Model.ID"
data-clienthistory='@Html.Raw(Json.Serialize(Model.PlayerHistory))'
data-clienthistory-ex='@Html.Raw(Json.Serialize(Model.ClientCountHistory))'
data-online="@Model.Online"></div>
</div> </div>

View File

@ -2,7 +2,8 @@
/////////////////////////////////////// ///////////////////////////////////////
// thanks to canvasjs :( // thanks to canvasjs :(
playerHistory.forEach(function (item, i) { playerHistory.forEach(function (item, i) {
playerHistory[i].x = new Date(playerHistory[i].x); playerHistory[i].x = new Date(playerHistory[i].timeString);
playerHistory[i].y = playerHistory[i].clientCount;
}); });
return new CanvasJS.Chart(`server_history_${i}`, { return new CanvasJS.Chart(`server_history_${i}`, {
@ -84,7 +85,7 @@ $(document).ready(function () {
}); });
$('.server-history-row').each(function (index, element) { $('.server-history-row').each(function (index, element) {
let clientHistory = $(this).data('clienthistory'); let clientHistory = $(this).data('clienthistory-ex');
let serverId = $(this).data('serverid'); let serverId = $(this).data('serverid');
let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text()); let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text());
let primaryColor = $('title').css('background-color'); let primaryColor = $('title').css('background-color');