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.Configuration;
using SharedLibraryCore.Interfaces;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands;
@ -33,7 +32,7 @@ class MostKillsCommand : Command
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);
if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{

View File

@ -15,10 +15,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
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)
{
var serverId = StatManager.GetIdForServer(s);
var serverId = gameServer.LegacyDatabaseId;
var mostPlayed = new List<string>
{

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands
{
@ -35,7 +34,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
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();
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)
{
var serverId = StatManager.GetIdForServer(server);
var topStatsText = new List<string>()
var serverId = server.LegacyDatabaseId;
var topStatsText = new List<string>
{
$"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
};
@ -29,7 +29,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// no one qualified
if (topStatsText.Count == 1)
{
topStatsText = new List<string>()
topStatsText = new List<string>
{
translationLookup["PLUGINS_STATS_TEXT_NOQUALIFY"]
};

View File

@ -38,37 +38,36 @@ namespace IW4MAdmin.Plugins.Stats.Commands
_statManager = statManager;
}
public override async Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent gameEvent)
{
string statLine;
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);
// 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
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
// 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
@ -76,7 +75,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
await using var context = _contextFactory.CreateContext(false);
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
@ -89,15 +88,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// getting self stats
else
{
var performanceRanking = await _statManager.GetClientOverallRanking(E.Origin.ClientId, serverId);
var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId);
var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
// 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
@ -105,7 +104,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
await using var context = _contextFactory.CreateContext(false);
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
@ -115,21 +114,21 @@ namespace IW4MAdmin.Plugins.Stats.Commands
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;
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
E.Owner.Broadcast(statLine);
var name = gameEvent.Target == null ? gameEvent.Origin.Name : gameEvent.Target.Name;
gameEvent.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
gameEvent.Owner.Broadcast(statLine);
}
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 IDatabaseContextFactory _contextFactory;
private readonly StatsConfiguration _config;
private static List<EFServer> serverModels;
public static string CLIENT_STATS_KEY = "ClientStats";
public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
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 ILookupCache<EFServer> _serverCache;
public StatManager(ILogger<StatManager> logger, IDatabaseContextFactory contextFactory,
StatsConfiguration statsConfig,
IServerDistributionCalculator serverDistributionCalculator)
IServerDistributionCalculator serverDistributionCalculator, ILookupCache<EFServer> serverCache)
{
_servers = new ConcurrentDictionary<long, ServerStats>();
_log = logger;
_contextFactory = contextFactory;
_config = statsConfig;
_serverDistributionCalculator = serverDistributionCalculator;
_serverCache = serverCache;
}
~StatManager()
@ -62,12 +63,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
_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)
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
@ -364,72 +359,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
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
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"
});
@ -459,7 +394,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
try
{
await _addPlayerWaiter.WaitAsync();
long serverId = GetIdForServer(pl.CurrentServer);
var serverId = (pl.CurrentServer as IGameServer).LegacyDatabaseId;
if (!_servers.ContainsKey(serverId))
{
@ -593,7 +528,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return;
}
var serverId = GetIdForServer(client.CurrentServer);
var serverId = (client.CurrentServer as IGameServer).LegacyDatabaseId;
var serverStats = _servers[serverId].ServerStatistics;
// get individual client's stats
@ -954,7 +889,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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 victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
@ -1582,8 +1517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task Sync(IGameServer gameServer, CancellationToken token)
{
var serverId = GetIdForServer(gameServer);
var serverId = gameServer.LegacyDatabaseId;
var waiter = _servers[serverId].OnSaving;
try
{
@ -1622,25 +1556,5 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
_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;
_statManager = statManager;
IGameServerEventSubscriptions.MonitoringStarted +=
async (monitorEvent, token) => await _statManager.EnsureServerAdded(monitorEvent.Server, token);
IGameServerEventSubscriptions.MonitoringStopped +=
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
IManagementEventSubscriptions.ClientStateInitialized += async (clientEvent, token) =>
@ -97,6 +95,13 @@ public class Plugin : IPluginV2
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)
{
await calculator.CalculateForEvent(clientEvent);
@ -108,7 +113,7 @@ public class Plugin : IPluginV2
messageEvent.Client.ClientId > 1)
{
await _statManager.AddMessageAsync(messageEvent.Client.ClientId,
StatManager.GetIdForServer(messageEvent.Server), true, messageEvent.Message, token);
messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token);
}
};
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
@ -191,7 +196,7 @@ public class Plugin : IPluginV2
await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime,
antiCheatDamageEvent.Origin,
antiCheatDamageEvent.Target,
StatManager.GetIdForServer(antiCheatDamageEvent.Server), antiCheatDamageEvent.Server.Map.Name,
antiCheatDamageEvent.Server.LegacyDatabaseId, antiCheatDamageEvent.Server.Map.Name,
killInfo[7], killInfo[8],
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]);
@ -205,13 +210,14 @@ public class Plugin : IPluginV2
if (shouldPersist)
{
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)
{
_statManager.SetTeamBased(StatManager.GetIdForServer(gameEvent.Server), gameEvent.Server.Gametype != "dm");
_statManager.SetTeamBased(gameEvent.Server.LegacyDatabaseId, gameEvent.Server.Gametype != "dm");
_statManager.ResetKillstreaks(gameEvent.Server);
await _statManager.Sync(gameEvent.Server, token);
@ -510,10 +516,10 @@ public class Plugin : IPluginV2
_databaseContextFactory));
}
async Task<string> MostKills(Server gameServer)
async Task<string> MostKills(IGameServer gameServer)
{
return string.Join(Environment.NewLine,
await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), _statsConfig,
await Commands.MostKillsCommand.GetMostKills(gameServer.LegacyDatabaseId, _statsConfig,
_databaseContextFactory, _translationLookup));
}

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">