update top level client count stats to support filtering per game

This commit is contained in:
RaidMax 2023-04-19 19:55:33 -05:00
parent c53e0de7d0
commit 92992dfb13
8 changed files with 126 additions and 87 deletions

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Data.Models.Server; using Data.Models.Server;
@ -40,21 +41,31 @@ namespace IW4MAdmin.Application.Misc
} }
public async Task<(int?, DateTime?)> public async Task<(int?, DateTime?)>
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
CancellationToken token = default) CancellationToken token = default)
{ {
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) => _snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) =>
{ {
Reference.Game? game = null;
long? id = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
id = (long?)ids.Last();
}
var oldestEntry = overPeriod.HasValue var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value ? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1); : DateTime.UtcNow.AddDays(-1);
int? maxClients; int? maxClients;
DateTime? maxClientsTime; DateTime? maxClientsTime;
if (serverId != null) if (id != null)
{ {
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId) var clients = await snapshots.Where(snapshot => snapshot.ServerId == id)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry) .Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.OrderByDescending(snapshot => snapshot.ClientCount) .OrderByDescending(snapshot => snapshot.ClientCount)
.Select(snapshot => new .Select(snapshot => new
@ -71,15 +82,16 @@ namespace IW4MAdmin.Application.Misc
else else
{ {
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry) var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
.GroupBy(snapshot => snapshot.PeriodBlock) .GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => new .Select(grp => new
{ {
ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount), ClientCount = grp.Sum(snapshot => (int?)snapshot.ClientCount),
Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt) Time = grp.Max(snapshot => (DateTime?)snapshot.CapturedAt)
}) })
.OrderByDescending(snapshot => snapshot.ClientCount) .OrderByDescending(snapshot => snapshot.ClientCount)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
maxClients = clients?.ClientCount; maxClients = clients?.ClientCount;
maxClientsTime = clients?.Time; maxClientsTime = clients?.Time;
} }
@ -87,11 +99,12 @@ namespace IW4MAdmin.Application.Misc
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients); _logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return (maxClients, maxClientsTime); return (maxClients, maxClientsTime);
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true); }, nameof(MaxConcurrentClientsAsync), new object[] { gameCode, serverId }, _cacheTimeSpan, true);
try try
{ {
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token); return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync),
new object[] { gameCode, serverId }, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -100,22 +113,30 @@ namespace IW4MAdmin.Application.Misc
} }
} }
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default) public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default)
{ {
_serverStatsCache.SetCacheItem(async (set, cancellationToken) => _serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) =>
{ {
var count = await set.CountAsync(cancellationToken); Reference.Game? game = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
}
var count = await set.CountAsync(item => game == null || item.GameName == game,
cancellationToken);
var startOfPeriod = var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24); DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod, var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod,
cancellationToken); cancellationToken);
return (count, recentCount); return (count, recentCount);
}, nameof(_serverStatsCache), _cacheTimeSpan, true); }, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true);
try try
{ {
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token); return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -166,21 +187,28 @@ namespace IW4MAdmin.Application.Misc
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default) public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{ {
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) => _rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
{ {
long? id = null;
if (ids.Any())
{
id = (long?)ids.First();
}
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set return set
.Where(rating => rating.Newest) .Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId) .Where(rating => rating.ServerId == id)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo) .Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned) .Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null) .Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken); .CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan); }, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
try try
{ {
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token); return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -11,10 +11,11 @@ namespace Data.Abstractions
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName, void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false); TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName, void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false); IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default); Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null, CancellationToken token = default);
} }
} }

View File

@ -18,7 +18,7 @@ namespace Data.Helpers
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new(); private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private readonly object _defaultKey = new(); private readonly string _defaultKey = null;
private bool _autoRefresh; private bool _autoRefresh;
private const int DefaultExpireMinutes = 15; private const int DefaultExpireMinutes = 15;
@ -29,7 +29,7 @@ namespace Data.Helpers
public string Key { get; set; } public string Key { get; set; }
public DateTime LastRetrieval { get; set; } public DateTime LastRetrieval { get; set; }
public TimeSpan ExpirationTime { get; set; } public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<TEntityType>, CancellationToken, Task<TCacheType>> Getter { get; set; } public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
public TCacheType Value { get; set; } public TCacheType Value { get; set; }
public bool IsSet { get; set; } public bool IsSet { get; set; }
@ -53,60 +53,58 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key, public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false) TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
SetCacheItem(getter, key, null, expirationTime, autoRefresh); SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh);
} }
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key, public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false) IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
ids ??= new[] { _defaultKey }; ids ??= new[] { _defaultKey };
if (!_cacheStates.ContainsKey(key)) if (!_cacheStates.ContainsKey(key))
{ {
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>()); _cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
} }
foreach (var id in ids) var cacheInstance = _cacheStates[key];
var id = GenerateKeyFromIds(ids);
lock (_cacheStates)
{ {
var cacheInstance = _cacheStates[key]; if (cacheInstance.ContainsKey(id))
lock (_cacheStates)
{
if (cacheInstance.ContainsKey(id))
{
continue;
}
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
lock (_cacheStates)
{
cacheInstance.Add(id, state);
}
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{ {
return; return;
} }
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
} }
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
lock (_cacheStates)
{
cacheInstance.Add(id, state);
}
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None);
_timer.Start();
} }
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) => public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
GetCacheItem(keyName, null, cancellationToken); GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null, public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!_cacheStates.ContainsKey(keyName)) if (!_cacheStates.ContainsKey(keyName))
@ -120,27 +118,27 @@ namespace Data.Helpers
lock (_cacheStates) lock (_cacheStates)
{ {
state = id is null ? cacheInstance.Values.First() : _cacheStates[keyName][id]; state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)];
} }
// when auto refresh is off we want to check the expiration and value // when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically // when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet) if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
{ {
await RunCacheUpdate(state, cancellationToken); await RunCacheUpdate(state, ids, cancellationToken);
} }
return state.Value; return state.Value;
} }
private async Task RunCacheUpdate(CacheState<TReturnType> state, CancellationToken token) private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> ids, CancellationToken token)
{ {
try try
{ {
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state); _logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
var set = context.Set<TEntityType>(); var set = context.Set<TEntityType>();
var value = await state.Getter(set, token); var value = await state.Getter(set, ids, token);
state.Value = value; state.Value = value;
state.IsSet = true; state.IsSet = true;
state.LastRetrieval = DateTime.Now; state.LastRetrieval = DateTime.Now;
@ -150,5 +148,8 @@ namespace Data.Helpers
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key); _logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
} }
} }
private static string GenerateKeyFromIds(IEnumerable<object> ids) =>
string.Join("_", ids.Select(id => id?.ToString() ?? "null"));
} }
} }

View File

@ -1,5 +1,5 @@
using System; using System;
using static SharedLibraryCore.Server; using Data.Models;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
{ {
@ -15,11 +15,11 @@ namespace SharedLibraryCore.Dtos
/// <summary> /// <summary>
/// specifies the game name filter /// specifies the game name filter
/// </summary> /// </summary>
public Game? Game { get; set; } public Reference.Game? Game { get; set; }
/// <summary> /// <summary>
/// collection of unique game names being monitored /// collection of unique game names being monitored
/// </summary> /// </summary>
public Game[] ActiveServerGames { get; set; } public Reference.Game[] ActiveServerGames { get; set; }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
@ -15,19 +16,21 @@ namespace SharedLibraryCore.Interfaces
/// Retrieves the max concurrent clients over a give time period for all servers or given server id /// Retrieves the max concurrent clients over a give time period for all servers or given server id
/// </summary> /// </summary>
/// <param name="serverId">ServerId to query on</param> /// <param name="serverId">ServerId to query on</param>
/// <param name="gameCode"><see cref="Reference.Game"/></param>
/// <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">CancellationToken</param> /// <param name="token">CancellationToken</param>
/// <returns></returns> /// <returns></returns>
Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
CancellationToken token = default); CancellationToken token = default);
/// <summary> /// <summary>
/// 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="gameCode"><see cref="Reference.Game"/></param>
/// <param name="token">CancellationToken</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, Reference.Game? gameCode = null, CancellationToken token = default);
/// <summary> /// <summary>
/// Retrieves the client count and history over the given period /// Retrieves the client count and history over the given period

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -21,13 +22,13 @@ public class Info : BaseController
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> Get(int period = 24, CancellationToken token = default) public async Task<IActionResult> Get(int period = 24, Reference.Game? game = null, CancellationToken token = default)
{ {
// todo: this is hardcoded currently because the cache doesn't take into consideration the duration, so // todo: this is hardcoded currently because the cache doesn't take into consideration the duration, so
// we could impact the webfront usage too // we could impact the webfront usage too
var duration = TimeSpan.FromHours(24); var duration = TimeSpan.FromHours(24);
var (totalClients, totalRecentClients) = var (totalClients, totalRecentClients) =
await _serverDataViewer.ClientCountsAsync(duration, token); await _serverDataViewer.ClientCountsAsync(duration, game, token);
var (maxConcurrent, maxConcurrentTime) = await _serverDataViewer.MaxConcurrentClientsAsync(overPeriod: duration, token: token); var (maxConcurrent, maxConcurrentTime) = await _serverDataViewer.MaxConcurrentClientsAsync(overPeriod: duration, token: token);
var response = new InfoResponse var response = new InfoResponse
{ {

View File

@ -7,8 +7,8 @@ using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
@ -27,26 +27,31 @@ namespace WebfrontCore.Controllers
_serverDataViewer = serverDataViewer; _serverDataViewer = serverDataViewer;
} }
public async Task<IActionResult> Index(Game? game = null, CancellationToken cancellationToken = default) public async Task<IActionResult> Index(Reference.Game? game = null,
CancellationToken cancellationToken = default)
{ {
ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_HOME"]; ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_HOME"];
ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"]; ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"];
ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"]; ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"];
var servers = Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game); var servers = Manager.GetServers().Where(server => game is null || server.GameName == (Server.Game?)game)
var (clientCount, time) = await _serverDataViewer.MaxConcurrentClientsAsync(token: cancellationToken); .ToList();
var (count, recentCount) = await _serverDataViewer.ClientCountsAsync(token: cancellationToken); var (clientCount, time) =
await _serverDataViewer.MaxConcurrentClientsAsync(gameCode: game, token: cancellationToken);
var (count, recentCount) =
await _serverDataViewer.ClientCountsAsync(gameCode: game, token: cancellationToken);
var model = new IW4MAdminInfo() var model = new IW4MAdminInfo
{ {
TotalAvailableClientSlots = servers.Sum(_server => _server.MaxClients), TotalAvailableClientSlots = servers.Sum(server => server.MaxClients),
TotalOccupiedClientSlots = servers.SelectMany(_server => _server.GetClientsAsList()).Count(), TotalOccupiedClientSlots = servers.SelectMany(server => server.GetClientsAsList()).Count(),
TotalClientCount = count, TotalClientCount = count,
RecentClientCount = recentCount, RecentClientCount = recentCount,
MaxConcurrentClients = clientCount ?? 0, MaxConcurrentClients = clientCount ?? 0,
MaxConcurrentClientsTime = time ?? DateTime.UtcNow, MaxConcurrentClientsTime = time ?? DateTime.UtcNow,
Game = game, Game = game,
ActiveServerGames = Manager.GetServers().Select(_server => _server.GameName).Distinct().ToArray() ActiveServerGames = Manager.GetServers().Select(server => (Reference.Game)server.GameName).Distinct()
.ToArray()
}; };
return View(model); return View(model);

View File

@ -10,7 +10,6 @@ using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server;
namespace WebfrontCore.ViewComponents namespace WebfrontCore.ViewComponents
{ {
@ -28,19 +27,20 @@ namespace WebfrontCore.ViewComponents
_defaultSettings = defaultSettings; _defaultSettings = defaultSettings;
} }
public IViewComponentResult Invoke(Game? game) public IViewComponentResult Invoke(Reference.Game? game)
{ {
if (game.HasValue) if (game.HasValue)
{ {
ViewBag.Maps = _defaultSettings.Maps.FirstOrDefault(map => map.Game == game)?.Maps.ToList() ?? ViewBag.Maps = _defaultSettings.Maps?.FirstOrDefault(map => map.Game == (Server.Game)game)?.Maps
new List<Map>(); ?.ToList() ?? new List<Map>();
} }
else else
{ {
ViewBag.Maps = _defaultSettings.Maps.SelectMany(maps => maps.Maps).ToList(); ViewBag.Maps = _defaultSettings.Maps?.SelectMany(maps => maps.Maps).ToList();
} }
var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game); var servers = Program.Manager.GetServers()
.Where(server => game is null || server.GameName == (Server.Game)game);
var serverInfo = new List<ServerInfo>(); var serverInfo = new List<ServerInfo>();