1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-08 22:28:15 -05:00
IW4M-Admin/Plugins/ZombieStats/Events/ZombieEventProcessor.cs
2024-07-02 16:27:28 -05:00

451 lines
18 KiB
C#

using System.Globalization;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Zombie;
using IW4MAdmin.Plugins.ZombieStats.States;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Events.Game.GameScript;
using SharedLibraryCore.Events.Game.GameScript.Zombie;
namespace IW4MAdmin.Plugins.ZombieStats.Events;
public class ZombieEventProcessor(ILogger<ZombieEventProcessor> logger, ZombieClientStateManager stateManager)
{
private const int RoundsConsidered = 200;
public void ProcessEvent(GameEventV2 gameEvent)
{
if (gameEvent.Origin is not null)
{
gameEvent.Origin.GameName = (Reference.Game)gameEvent.Owner.GameName;
}
if (gameEvent.Target is not null)
{
gameEvent.Target.GameName = (Reference.Game)gameEvent.Owner.GameName;
}
switch (gameEvent.GetType().Name)
{
case nameof(PlayerKilledGameEvent):
OnPlayerKilled((PlayerKilledGameEvent)gameEvent);
break;
case nameof(PlayerDamageGameEvent):
OnPlayerDamaged((PlayerDamageGameEvent)gameEvent);
break;
case nameof(ZombieKilledGameEvent):
OnZombieKilled((ZombieKilledGameEvent)gameEvent);
break;
case nameof(ZombieDamageGameEvent):
OnZombieDamaged((ZombieDamageGameEvent)gameEvent);
break;
case nameof(PlayerConsumedPerkGameEvent):
OnPlayerConsumedPerk((PlayerConsumedPerkGameEvent)gameEvent);
break;
case nameof(PlayerGrabbedPowerupGameEvent):
OnPlayerGrabbedPowerup((PlayerGrabbedPowerupGameEvent)gameEvent);
break;
case nameof(PlayerRevivedGameEvent):
OnPlayerRevived((PlayerRevivedGameEvent)gameEvent);
break;
case nameof(PlayerDownedGameEvent):
OnPlayerDowned((PlayerDownedGameEvent)gameEvent);
break;
case nameof(PlayerRoundDataGameEvent):
OnPlayerRoundDataReceived((PlayerRoundDataGameEvent)gameEvent);
break;
case nameof(RoundEndEvent):
OnRoundCompleted((RoundEndEvent)gameEvent);
break;
case nameof(PlayerStatUpdatedGameEvent):
OnPlayerStatUpdated((PlayerStatUpdatedGameEvent)gameEvent);
break;
}
}
public Func<EFClient, EFClientStatistics, double> SkillCalculation()
{
return (client, clientStats) =>
{
var state = stateManager.GetStateForClient(client);
if (state is null)
{
return clientStats.Skill;
}
if (!state.PersistentLifetimeAggregateStats.TryGetValue(client.NetworkId, out var aggregateStats))
{
return clientStats.Skill;
}
var currentClientRound = state.RoundStates[client.NetworkId];
var normalizedValues = new List<double>();
foreach (var key in ZombieAggregateClientStat.RecordsKeys)
{
var clientValue =
Convert.ToDouble(aggregateStats.GetType().GetProperty(key)!.GetValue(aggregateStats)?.ToString(),
CultureInfo.InvariantCulture);
var maxRecord = stateManager.GetClientNumericalRecord(key) ??
stateManager.CreateClientNumericalRecord(
client, currentClientRound.PersistentClientRound, key, clientValue);
var maxValue = Convert.ToDouble(maxRecord.Value, CultureInfo.InvariantCulture);
if (clientValue > maxValue)
{
maxRecord.Value = clientValue.ToString(CultureInfo.InvariantCulture);
maxRecord.Client = client;
maxRecord.Round = currentClientRound.PersistentClientRound;
stateManager.TrackUpdatedState(maxRecord);
}
if (!ZombieAggregateClientStat.SkillKeys.Contains(key))
{
continue;
}
var normalizedValue = clientValue / maxValue;
if (double.IsNaN(normalizedValue))
{
normalizedValue = 1;
}
normalizedValues.Add(normalizedValue);
}
var avg = normalizedValues.Any() ? normalizedValues.Average() : 0.0;
avg *= 1000.0;
var roundWeightFactor = Math.Max(1, aggregateStats.TotalRoundsPlayed) <= RoundsConsidered
? 1.0 / Math.Max(1, aggregateStats.TotalRoundsPlayed)
: 2.0 / (RoundsConsidered + 1);
var average = CalculateAverage(clientStats.Skill, avg, roundWeightFactor);
if (double.IsInfinity(average))
{
average = 0;
}
return average;
};
}
private void OnPlayerKilled(PlayerKilledGameEvent gameEvent)
{
RunCalculation(gameEvent.Victim, curr =>
{
curr.PersistentClientRound.Deaths++;
curr.PersistentClientRound.DamageReceived += gameEvent.Damage;
curr.DiedAt = DateTimeOffset.UtcNow;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Died, curr.PersistentClientRound.Client,
numericalValue: gameEvent.Damage);
});
}
private void OnPlayerDamaged(PlayerDamageGameEvent gameEvent)
{
RunCalculation(gameEvent.Victim, curr =>
{
curr.PersistentClientRound.DamageReceived += gameEvent.Damage;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.DamageTaken, curr.PersistentClientRound.Client,
numericalValue: gameEvent.Damage);
});
}
private void OnZombieKilled(ZombieKilledGameEvent gameEvent)
{
RunCalculation(gameEvent.Attacker, curr =>
{
curr.PersistentClientRound.Kills++;
curr.PersistentClientRound.DamageDealt += gameEvent.Damage;
if (gameEvent.HitLocation.StartsWith("head") || gameEvent.MeansOfDeath == "MOD_HEADSHOT")
{
curr.PersistentClientRound.Headshots++;
curr.PersistentClientRound.HeadshotKills++;
}
if (gameEvent.MeansOfDeath == "MOD_MELEE")
{
curr.PersistentClientRound.Melees++;
}
curr.Hits++;
});
}
private void OnZombieDamaged(ZombieDamageGameEvent gameEvent)
{
RunCalculation(gameEvent.Attacker, curr =>
{
curr.PersistentClientRound.DamageDealt += gameEvent.Damage;
if (gameEvent.HitLocation == "head" || gameEvent.MeansOfDeath == "MOD_HEADSHOT")
{
curr.PersistentClientRound.Headshots++;
}
if (gameEvent.MeansOfDeath == "MOD_MELEE")
{
curr.PersistentClientRound.Melees++;
}
curr.Hits++;
});
}
private void OnPlayerConsumedPerk(PlayerConsumedPerkGameEvent gameEvent)
{
RunCalculation(gameEvent.Client, curr =>
{
curr.PersistentClientRound.PerksConsumed++;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.PerkConsumed, curr.PersistentClientRound.Client,
textualValue: gameEvent.PerkName);
});
}
private void OnPlayerGrabbedPowerup(PlayerGrabbedPowerupGameEvent gameEvent)
{
RunCalculation(gameEvent.Client, curr =>
{
curr.PersistentClientRound.PowerupsGrabbed++;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.PowerupGrabbed, curr.PersistentClientRound.Client,
textualValue: gameEvent.PowerupName);
});
}
private void OnPlayerRevived(PlayerRevivedGameEvent gameEvent)
{
RunCalculation(gameEvent.Reviver, curr =>
{
curr.PersistentClientRound.Revives++;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Revived, curr.PersistentClientRound.Client);
//_stateManager.TrackEventForLog(gameEvent, EventLogType.WasRevived, gameEvent.Revived,
// gameEvent.Reviver);
});
}
private void OnPlayerDowned(PlayerDownedGameEvent gameEvent)
{
RunCalculation(gameEvent.Client, curr =>
{
curr.PersistentClientRound.Downs++;
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Downed, curr.PersistentClientRound.Client);
});
}
private void OnRoundCompleted(RoundEndEvent roundEndEvent)
{
stateManager.StartNextRound(roundEndEvent.RoundNumber, roundEndEvent.Owner);
}
private void OnPlayerStatUpdated(PlayerStatUpdatedGameEvent gameEvent)
{
var tagValue = stateManager.GetStatTagValueForClient(gameEvent.Client, gameEvent.StatTag);
if (tagValue is null)
{
logger.LogWarning("[ZM] Cannot update stat value {Key} for {Client} because no entry exists",
gameEvent.StatTag, gameEvent.Client.ToString());
return;
}
tagValue.StatValue ??= 0;
switch (gameEvent.UpdateType)
{
case PlayerStatUpdatedGameEvent.StatUpdateType.Absolute:
tagValue.StatValue = gameEvent.StatValue;
break;
case PlayerStatUpdatedGameEvent.StatUpdateType.Increment:
tagValue.StatValue += gameEvent.StatValue;
break;
case PlayerStatUpdatedGameEvent.StatUpdateType.Decrement:
tagValue.StatValue -= gameEvent.StatValue;
break;
}
if (tagValue.ZombieClientStatTagValueId != 0)
{
stateManager.TrackUpdatedState(tagValue);
}
}
private void OnPlayerRoundDataReceived(PlayerRoundDataGameEvent gameEvent)
{
RunAggregateCalculation(gameEvent.Client, (matchState, roundState, matchStat, lifetimeStat, lifetimeServerStat) =>
{
var currentScore = gameEvent.CurrentScore;
var isForfeit = gameEvent is { CurrentScore: 0, TotalScore: 0 };
// in T4 on the last round the current score is set to total score, so we need to
// undo that
if (gameEvent.IsGameOver && !isForfeit)
{
var previousCumulativePoints = (int)matchStat.PointsEarned;
var lastRoundPoints = gameEvent.TotalScore - previousCumulativePoints;
currentScore = lastRoundPoints + (previousCumulativePoints - (int)matchStat.PointsSpent);
}
roundState.PersistentClientRound.Points = currentScore;
roundState.PersistentClientRound.EndTime = DateTimeOffset.UtcNow;
roundState.PersistentClientRound.Duration =
roundState.PersistentClientRound.EndTime - roundState.PersistentClientRound.StartTime;
roundState.PersistentClientRound.TimeAlive =
roundState.PersistentClientRound.EndTime.Value - roundState.PersistentClientRound.StartTime;
if (roundState.DiedAt is not null)
{
// subtract the time since they died from the total round time
roundState.PersistentClientRound.TimeAlive =
roundState.PersistentClientRound.TimeAlive.Value.Subtract(
roundState.PersistentClientRound.EndTime.Value - roundState.DiedAt.Value);
}
var earnedPoints = isForfeit ? 0 : gameEvent.TotalScore - matchStat.PointsEarned;
var spentPoints = isForfeit ? 0 : Math.Abs(currentScore - earnedPoints - (matchStat.PointsEarned - matchStat.PointsSpent));
roundState.PersistentClientRound.PointsSpent = spentPoints;
roundState.PersistentClientRound.PointsEarned = earnedPoints;
// add to the match aggregates
foreach (var set in new ZombieClientStat[] { matchStat, lifetimeStat, lifetimeServerStat })
{
set.Kills += roundState.PersistentClientRound.Kills;
set.Deaths += roundState.PersistentClientRound.Deaths;
set.DamageDealt += roundState.PersistentClientRound.DamageDealt;
set.DamageReceived += roundState.PersistentClientRound.DamageReceived;
set.Headshots += roundState.PersistentClientRound.Headshots;
set.HeadshotKills += roundState.PersistentClientRound.HeadshotKills;
set.Melees += roundState.PersistentClientRound.Melees;
set.Downs += roundState.PersistentClientRound.Downs;
set.Revives += roundState.PersistentClientRound.Revives;
set.PointsEarned += roundState.PersistentClientRound.PointsEarned;
set.PointsSpent += roundState.PersistentClientRound.PointsSpent;
set.PerksConsumed += roundState.PersistentClientRound.PerksConsumed;
set.PowerupsGrabbed += roundState.PersistentClientRound.PowerupsGrabbed;
}
#region maximums and averages
if (gameEvent.IsGameOver)
{
// make it easier to track how many players made it to the end
matchState.PersistentMatch.ClientsCompleted++;
lifetimeStat.TotalMatchesCompleted++;
lifetimeServerStat.TotalMatchesCompleted++;
}
CalculateAveragesAndTotals(matchState, lifetimeStat, roundState, matchStat);
CalculateAveragesAndTotals(matchState, lifetimeServerStat, roundState, matchStat);
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.RoundCompleted, matchStat.Client,
numericalValue: gameEvent.CurrentRound);
#endregion
});
}
private static void CalculateAveragesAndTotals(MatchState matchState,
ZombieAggregateClientStat lifetimeStat, RoundState roundState, ZombieMatchClientStat matchStat)
{
var currentRoundNumber = roundState.PersistentClientRound.RoundNumber;
// don't credit if played less than 50%
var shouldCountHighestRound = matchState.RoundNumber > lifetimeStat.HighestRound &&
matchStat.JoinedRound is not null &&
currentRoundNumber - matchStat.JoinedRound.Value >=
currentRoundNumber / 2.0;
if (shouldCountHighestRound)
{
lifetimeStat.HighestRound = matchState.RoundNumber;
}
lifetimeStat.TotalRoundsPlayed++;
var roundWeightFactor = lifetimeStat.TotalRoundsPlayed <= RoundsConsidered
? 1.0 / lifetimeStat.TotalRoundsPlayed
: 2.0 / (RoundsConsidered + 1);
var roundKpd = roundState.PersistentClientRound.Kills / Math.Max(1,
roundState.PersistentClientRound.Deaths + roundState.PersistentClientRound.Downs);
lifetimeStat.AverageKillsPerDown =
CalculateAverage(lifetimeStat.AverageKillsPerDown, roundKpd, roundWeightFactor);
lifetimeStat.AverageDowns =
CalculateAverage(lifetimeStat.AverageDowns, matchStat.Downs, roundWeightFactor);
lifetimeStat.AverageRevives =
CalculateAverage(lifetimeStat.AverageRevives, matchStat.Revives, roundWeightFactor);
lifetimeStat.AverageRoundReached = CalculateAverage(lifetimeStat.AverageRoundReached,
matchState.RoundNumber, roundWeightFactor);
lifetimeStat.AverageMelees =
CalculateAverage(lifetimeStat.AverageMelees, matchStat.Melees, roundWeightFactor);
var hsp = roundState.PersistentClientRound.Headshots / (double)Math.Max(1, roundState.Hits);
lifetimeStat.HeadshotPercentage = CalculateAverage(lifetimeStat.HeadshotPercentage, hsp, roundWeightFactor);
lifetimeStat.AlivePercentage = CalculateAverage(lifetimeStat.AlivePercentage,
roundState.PersistentClientRound.TimeAlive!.Value.TotalMilliseconds /
roundState.PersistentClientRound.Duration!.Value.TotalMilliseconds, roundWeightFactor);
lifetimeStat.AveragePoints = CalculateAverage(lifetimeStat.AveragePoints,
roundState.PersistentClientRound.PointsEarned, roundWeightFactor);
}
private void RunCalculation(EFClient client, Action<RoundState> calculation)
{
if (stateManager.GetStateForClient(client) is not { } match)
{
logger.LogWarning("[ZM] No active zombie match for {Client}", client.ToString());
return;
}
if (!match.RoundStates.TryGetValue(client.NetworkId, out var roundState))
{
logger.LogWarning("[ZM] No active zombie round for {Client}", client.ToString());
return;
}
calculation(roundState);
stateManager.TrackUpdatedState(roundState.PersistentClientRound);
}
private void RunAggregateCalculation(EFClient client,
Action<MatchState, RoundState, ZombieMatchClientStat, ZombieAggregateClientStat, ZombieAggregateClientStat> action)
{
if (stateManager.GetStateForClient(client) is not { } match)
{
logger.LogWarning("[ZM] No active zombie match for {Client}", client.ToString());
return;
}
var currentRoundState = match.RoundStates[client.NetworkId];
var matchStat = match.PersistentMatchAggregateStats[client.NetworkId];
var lifetimeAggregateStat = match.PersistentLifetimeAggregateStats[client.NetworkId];
var lifetimeServerAggregateState = match.PersistentLifetimeServerAggregateStats[client.NetworkId];
action(match, currentRoundState, matchStat, lifetimeAggregateStat, lifetimeServerAggregateState);
stateManager.TrackUpdatedState(currentRoundState.PersistentClientRound);
stateManager.TrackUpdatedState(matchStat);
stateManager.TrackUpdatedState(lifetimeAggregateStat);
stateManager.TrackUpdatedState(lifetimeServerAggregateState);
}
private static double CalculateAverage(double previousAverage, double currentValue, double factor) =>
Math.Round(currentValue * factor + previousAverage * (1 - factor), 2);
}