update stats plugin for server caching and better DI usage

This commit is contained in:
RaidMax 2023-04-05 10:15:10 -05:00
parent f41ce39180
commit d9d5a56ab0
8 changed files with 56 additions and 139 deletions

View File

@ -9,7 +9,6 @@ using Data.Models.Client.Stats;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config; using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands; namespace IW4MAdmin.Plugins.Stats.Commands;
@ -33,7 +32,7 @@ class MostKillsCommand : Command
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
var mostKills = await GetMostKills(StatManager.GetIdForServer(gameEvent.Owner), _statsConfig, var mostKills = await GetMostKills((gameEvent.Owner as IGameServer).LegacyDatabaseId, _statsConfig,
_contextFactory, _translationLookup); _contextFactory, _translationLookup);
if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {

View File

@ -15,10 +15,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
class MostPlayedCommand : Command class MostPlayedCommand : Command
{ {
public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup, public static async Task<List<string>> GetMostPlayed(IGameServer gameServer, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) IDatabaseContextFactory contextFactory)
{ {
var serverId = StatManager.GetIdForServer(s); var serverId = gameServer.LegacyDatabaseId;
var mostPlayed = new List<string> var mostPlayed = new List<string>
{ {

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands namespace IW4MAdmin.Plugins.Stats.Commands
{ {
@ -35,7 +34,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
if (gameEvent.Origin.ClientNumber >= 0) if (gameEvent.Origin.ClientNumber >= 0)
{ {
var serverId = Helpers.StatManager.GetIdForServer(gameEvent.Owner); var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
await using var context = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var clientStats = await context.Set<EFClientStatistics>() var clientStats = await context.Set<EFClientStatistics>()

View File

@ -13,8 +13,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
public static async Task<List<string>> GetTopStats(IGameServer server, ITranslationLookup translationLookup, StatManager statManager) public static async Task<List<string>> GetTopStats(IGameServer server, ITranslationLookup translationLookup, StatManager statManager)
{ {
var serverId = StatManager.GetIdForServer(server); var serverId = server.LegacyDatabaseId;
var topStatsText = new List<string>() var topStatsText = new List<string>
{ {
$"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--" $"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
}; };
@ -29,7 +29,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// no one qualified // no one qualified
if (topStatsText.Count == 1) if (topStatsText.Count == 1)
{ {
topStatsText = new List<string>() topStatsText = new List<string>
{ {
translationLookup["PLUGINS_STATS_TEXT_NOQUALIFY"] translationLookup["PLUGINS_STATS_TEXT_NOQUALIFY"]
}; };

View File

@ -38,37 +38,36 @@ namespace IW4MAdmin.Plugins.Stats.Commands
_statManager = statManager; _statManager = statManager;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
string statLine; string statLine;
EFClientStatistics pStats = null; EFClientStatistics pStats = null;
if (E.Data.Length > 0 && E.Target == null) if (gameEvent.Data.Length > 0 && gameEvent.Target == null)
{ {
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault(); gameEvent.Target = gameEvent.Owner.GetClientByName(gameEvent.Data).FirstOrDefault();
if (E.Target == null) if (gameEvent.Target == null)
{ {
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]); gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
} }
} }
var serverId = StatManager.GetIdForServer(E.Owner); var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId); var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId);
// getting stats for a particular client // getting stats for a particular client
if (E.Target != null) if (gameEvent.Target != null)
{ {
var performanceRanking = await _statManager.GetClientOverallRanking(E.Target.ClientId, serverId); var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Target.ClientId, serverId);
var performanceRankingString = performanceRanking == 0 var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
// target is currently connected so we want their cached stats if they exist // target is currently connected so we want their cached stats if they exist
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Target))) if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target)))
{ {
pStats = E.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY); pStats = gameEvent.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
} }
// target is not connected so we want to look up via database // target is not connected so we want to look up via database
@ -76,7 +75,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
pStats = await context.Set<EFClientStatistics>() pStats = await context.Set<EFClientStatistics>()
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId); .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Target.ClientId);
} }
// if it's still null then they've not gotten a kill or death yet // if it's still null then they've not gotten a kill or death yet
@ -89,15 +88,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// getting self stats // getting self stats
else else
{ {
var performanceRanking = await _statManager.GetClientOverallRanking(E.Origin.ClientId, serverId); var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId);
var performanceRankingString = performanceRanking == 0 var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
// check if current client is connected to the server // check if current client is connected to the server
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin))) if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin)))
{ {
pStats = E.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY); pStats = gameEvent.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
} }
// happens if the user has not gotten a kill/death since connecting // happens if the user has not gotten a kill/death since connecting
@ -105,7 +104,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
pStats = (await context.Set<EFClientStatistics>() pStats = (await context.Set<EFClientStatistics>()
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)); .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Origin.ClientId));
} }
// if it's still null then they've not gotten a kill or death yet // if it's still null then they've not gotten a kill or death yet
@ -115,21 +114,21 @@ namespace IW4MAdmin.Plugins.Stats.Commands
pStats.KDR, pStats.Performance, performanceRankingString); pStats.KDR, pStats.Performance, performanceRankingString);
} }
if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
var name = E.Target == null ? E.Origin.Name : E.Target.Name; var name = gameEvent.Target == null ? gameEvent.Origin.Name : gameEvent.Target.Name;
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name)); gameEvent.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
E.Owner.Broadcast(statLine); gameEvent.Owner.Broadcast(statLine);
} }
else else
{ {
if (E.Target != null) if (gameEvent.Target != null)
{ {
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(gameEvent.Target.Name));
} }
E.Origin.Tell(statLine); gameEvent.Origin.Tell(statLine);
} }
} }
} }

View File

@ -39,22 +39,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private readonly ILogger _log; private readonly ILogger _log;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly StatsConfiguration _config; private readonly StatsConfiguration _config;
private static List<EFServer> serverModels;
public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_STATS_KEY = "ClientStats";
public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
public static string ESTIMATED_SCORE = "EstimatedScore"; public static string ESTIMATED_SCORE = "EstimatedScore";
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _addPlayerWaiter = new(1, 1);
private readonly IServerDistributionCalculator _serverDistributionCalculator; private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly ILookupCache<EFServer> _serverCache;
public StatManager(ILogger<StatManager> logger, IDatabaseContextFactory contextFactory, public StatManager(ILogger<StatManager> logger, IDatabaseContextFactory contextFactory,
StatsConfiguration statsConfig, StatsConfiguration statsConfig,
IServerDistributionCalculator serverDistributionCalculator) IServerDistributionCalculator serverDistributionCalculator, ILookupCache<EFServer> serverCache)
{ {
_servers = new ConcurrentDictionary<long, ServerStats>(); _servers = new ConcurrentDictionary<long, ServerStats>();
_log = logger; _log = logger;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_config = statsConfig; _config = statsConfig;
_serverDistributionCalculator = serverDistributionCalculator; _serverDistributionCalculator = serverDistributionCalculator;
_serverCache = serverCache;
} }
~StatManager() ~StatManager()
@ -62,12 +63,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
_addPlayerWaiter.Dispose(); _addPlayerWaiter.Dispose();
} }
private void SetupServerIds()
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
serverModels = ctx.Set<EFServer>().ToList();
}
public Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null) public Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null)
{ {
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
@ -364,72 +359,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
try try
{ {
if (serverModels == null)
{
SetupServerIds();
}
var serverId = GetIdForServer(gameServer as Server);
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var serverSet = ctx.Set<EFServer>();
// get the server from the database if it exists, otherwise create and insert a new one
var cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.ServerId == serverId, token);
// the server might be using legacy server id
if (cachedServerModel == null)
{
cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.EndPoint == gameServer.Id, token);
if (cachedServerModel != null)
{
// this provides a way to identify legacy server entries
cachedServerModel.EndPoint = gameServer.Id;
ctx.Update(cachedServerModel);
ctx.SaveChanges();
}
}
// server has never been added before
if (cachedServerModel == null)
{
cachedServerModel = new EFServer
{
Port = gameServer.ListenPort,
EndPoint = gameServer.Id,
ServerId = serverId,
GameName = gameServer.GameCode,
HostName = gameServer.ListenAddress
};
cachedServerModel = serverSet.Add(cachedServerModel).Entity;
// this doesn't need to be async as it's during initialization
await ctx.SaveChangesAsync(token);
}
// we want to set the gamename up if it's never been set, or it changed
else if (!cachedServerModel.GameName.HasValue || cachedServerModel.GameName.Value != gameServer.GameCode)
{
cachedServerModel.GameName = gameServer.GameCode;
ctx.Entry(cachedServerModel).Property(property => property.GameName).IsModified = true;
await ctx.SaveChangesAsync(token);
}
if (cachedServerModel.HostName == null || cachedServerModel.HostName != gameServer.ServerName)
{
cachedServerModel.HostName = gameServer.ServerName;
ctx.Entry(cachedServerModel).Property(property => property.HostName).IsModified = true;
await ctx.SaveChangesAsync(token);
}
ctx.Entry(cachedServerModel).Property(property => property.IsPasswordProtected).IsModified = true;
cachedServerModel.IsPasswordProtected = !string.IsNullOrEmpty(gameServer.GamePassword);
await ctx.SaveChangesAsync(token);
// check to see if the stats have ever been initialized // check to see if the stats have ever been initialized
var serverStats = InitializeServerStats(cachedServerModel.ServerId); var cachedServer =
await _serverCache.FirstAsync(cachedServer => cachedServer.EndPoint == gameServer.Id);
var serverStats = InitializeServerStats(gameServer.LegacyDatabaseId);
_servers.TryAdd(serverId, new ServerStats(cachedServerModel, serverStats, gameServer as Server) _servers.TryAdd(cachedServer.ServerId, new ServerStats(cachedServer, serverStats, gameServer as Server)
{ {
IsTeamBased = gameServer.Gametype != "dm" IsTeamBased = gameServer.Gametype != "dm"
}); });
@ -459,7 +394,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
try try
{ {
await _addPlayerWaiter.WaitAsync(); await _addPlayerWaiter.WaitAsync();
long serverId = GetIdForServer(pl.CurrentServer); var serverId = (pl.CurrentServer as IGameServer).LegacyDatabaseId;
if (!_servers.ContainsKey(serverId)) if (!_servers.ContainsKey(serverId))
{ {
@ -593,7 +528,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return; return;
} }
var serverId = GetIdForServer(client.CurrentServer); var serverId = (client.CurrentServer as IGameServer).LegacyDatabaseId;
var serverStats = _servers[serverId].ServerStatistics; var serverStats = _servers[serverId].ServerStatistics;
// get individual client's stats // get individual client's stats
@ -954,7 +889,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task AddStandardKill(EFClient attacker, EFClient victim) public async Task AddStandardKill(EFClient attacker, EFClient victim)
{ {
var serverId = GetIdForServer(attacker.CurrentServer); var serverId = (attacker.CurrentServer as IGameServer).LegacyDatabaseId;
var attackerStats = attacker.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var attackerStats = attacker.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
var victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
@ -1582,8 +1517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task Sync(IGameServer gameServer, CancellationToken token) public async Task Sync(IGameServer gameServer, CancellationToken token)
{ {
var serverId = GetIdForServer(gameServer); var serverId = gameServer.LegacyDatabaseId;
var waiter = _servers[serverId].OnSaving; var waiter = _servers[serverId].OnSaving;
try try
{ {
@ -1622,25 +1556,5 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
_servers[serverId].IsTeamBased = isTeamBased; _servers[serverId].IsTeamBased = isTeamBased;
} }
public static long GetIdForServer(IGameServer gameServer)
{
if (gameServer.Id == "66.150.121.184:28965")
{
return 886229536;
}
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(gameServer.ListenAddress, gameServer.ListenPort);
id = id < 0 ? Math.Abs(id) : id;
#pragma warning disable CS0618
var serverId = serverModels.FirstOrDefault(cachedServer => cachedServer.ServerId == gameServer.LegacyEndpoint ||
#pragma warning restore CS0618
cachedServer.EndPoint == gameServer.ToString() ||
cachedServer.ServerId == id)?.ServerId;
return serverId ?? id;
}
} }
} }

View File

@ -71,8 +71,6 @@ public class Plugin : IPluginV2
_statsConfig = statsConfig; _statsConfig = statsConfig;
_statManager = statManager; _statManager = statManager;
IGameServerEventSubscriptions.MonitoringStarted +=
async (monitorEvent, token) => await _statManager.EnsureServerAdded(monitorEvent.Server, token);
IGameServerEventSubscriptions.MonitoringStopped += IGameServerEventSubscriptions.MonitoringStopped +=
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token); async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
IManagementEventSubscriptions.ClientStateInitialized += async (clientEvent, token) => IManagementEventSubscriptions.ClientStateInitialized += async (clientEvent, token) =>
@ -97,6 +95,13 @@ public class Plugin : IPluginV2
return; return;
} }
if (clientEvent.Client.ClientId == 0)
{
_logger.LogWarning("No client id for {Client}, so we are not doing any stat calculation",
clientEvent.Client.ToString());
return;
}
foreach (var calculator in _statCalculators) foreach (var calculator in _statCalculators)
{ {
await calculator.CalculateForEvent(clientEvent); await calculator.CalculateForEvent(clientEvent);
@ -108,7 +113,7 @@ public class Plugin : IPluginV2
messageEvent.Client.ClientId > 1) messageEvent.Client.ClientId > 1)
{ {
await _statManager.AddMessageAsync(messageEvent.Client.ClientId, await _statManager.AddMessageAsync(messageEvent.Client.ClientId,
StatManager.GetIdForServer(messageEvent.Server), true, messageEvent.Message, token); messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token);
} }
}; };
IGameEventSubscriptions.MatchEnded += OnMatchEvent; IGameEventSubscriptions.MatchEnded += OnMatchEvent;
@ -191,7 +196,7 @@ public class Plugin : IPluginV2
await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime, await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime,
antiCheatDamageEvent.Origin, antiCheatDamageEvent.Origin,
antiCheatDamageEvent.Target, antiCheatDamageEvent.Target,
StatManager.GetIdForServer(antiCheatDamageEvent.Server), antiCheatDamageEvent.Server.Map.Name, antiCheatDamageEvent.Server.LegacyDatabaseId, antiCheatDamageEvent.Server.Map.Name,
killInfo[7], killInfo[8], killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11],
killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
@ -205,13 +210,14 @@ public class Plugin : IPluginV2
if (shouldPersist) if (shouldPersist)
{ {
await _statManager.AddMessageAsync(commandEvent.Client.ClientId, await _statManager.AddMessageAsync(commandEvent.Client.ClientId,
StatManager.GetIdForServer(commandEvent.Client.CurrentServer), false, commandEvent.CommandText, token); (commandEvent.Client.CurrentServer as IGameServer).LegacyDatabaseId, false,
commandEvent.CommandText, token);
} }
} }
private async Task OnMatchEvent(GameEventV2 gameEvent, CancellationToken token) private async Task OnMatchEvent(GameEventV2 gameEvent, CancellationToken token)
{ {
_statManager.SetTeamBased(StatManager.GetIdForServer(gameEvent.Server), gameEvent.Server.Gametype != "dm"); _statManager.SetTeamBased(gameEvent.Server.LegacyDatabaseId, gameEvent.Server.Gametype != "dm");
_statManager.ResetKillstreaks(gameEvent.Server); _statManager.ResetKillstreaks(gameEvent.Server);
await _statManager.Sync(gameEvent.Server, token); await _statManager.Sync(gameEvent.Server, token);
@ -510,10 +516,10 @@ public class Plugin : IPluginV2
_databaseContextFactory)); _databaseContextFactory));
} }
async Task<string> MostKills(Server gameServer) async Task<string> MostKills(IGameServer gameServer)
{ {
return string.Join(Environment.NewLine, return string.Join(Environment.NewLine,
await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), _statsConfig, await Commands.MostKillsCommand.GetMostKills(gameServer.LegacyDatabaseId, _statsConfig,
_databaseContextFactory, _translationLookup)); _databaseContextFactory, _translationLookup));
} }

View File

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