From 79bd6ca8e19b5b021aa1744f4cdf6ccd96078cf8 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 2 Jul 2024 16:09:30 -0500 Subject: [PATCH] zombie stats code --- Application/Application.csproj | 2 +- Application/Extensions/StartupExtensions.cs | 6 +- Application/IW4MServer.cs | 2 +- Application/Main.cs | 2 +- Data/Context/DatabaseContext.cs | 50 +- ...0905194120_IntitialZombieStats.Designer.cs | 1959 ---------------- ...6230124_AddZombieStatsEventLog.Designer.cs | 2010 ----------------- .../20230906230124_AddZombieStatsEventLog.cs | 99 - ...708_AddHeadshotKillsToZombieClientState.cs | 29 - ...0219203501_InitialZombieStats.Designer.cs} | 86 +- ...s => 20240219203501_InitialZombieStats.cs} | 232 +- .../SqliteDatabaseContextModelSnapshot.cs | 82 +- .../Client/Stats/EFClientHitStatistic.cs | 5 + .../Client/Stats/EFClientRankingHistory.cs | 5 +- Data/Models/Client/Stats/EFClientStatTag.cs | 12 + .../Client/Stats/EFClientStatTagValue.cs | 23 + .../Client/Stats/EFPerformanceBucket.cs | 15 + Data/Models/Server/EFServer.cs | 5 +- Plugins/Stats/Client/HitCalculator.cs | 16 +- .../Client/ServerDistributionCalculator.cs | 21 +- .../Config/PerformanceBucketConfiguration.cs | 3 +- .../AdvancedClientStatsResourceQueryHelper.cs | 18 +- Plugins/Stats/Helpers/StatManager.cs | 4 +- Plugins/Stats/Plugin.cs | 16 +- Plugins/Stats/Stats.csproj | 5 +- Plugins/Stats/TalkerPoC/CodResponse.cs | 73 + .../ZombieStats/Events/ZombieEventParser.cs | 251 ++ .../Events/ZombieEventProcessor.cs | 450 ++++ Plugins/ZombieStats/States/MatchState.cs | 15 + Plugins/ZombieStats/States/RoundState.cs | 10 + .../States/ZombieClientStateManager.cs | 824 +++++++ Plugins/ZombieStats/ZombieStats.cs | 196 ++ SharedLibraryCore/Dtos/ServerInfo.cs | 1 + .../GameScript/{Zombie => }/RoundEndEvent.cs | 2 +- .../Zombie/PlayerStatUpdatedGameEvent.cs | 15 + .../Events/Server/ServerStatusReceiveEvent.cs | 1 - .../Events/IGameEventSubscriptions.cs | 1 + SharedLibraryCore/SharedLibraryCore.csproj | 2 +- .../Client/ClientStatisticsController.cs | 7 +- .../Client/Legacy/StatsController.cs | 12 +- .../ViewComponents/TopPlayersViewComponent.cs | 5 +- .../Views/Client/Statistics/Advanced.cshtml | 14 +- .../Views/Client/Statistics/Index.cshtml | 30 +- WebfrontCore/WebfrontCore.csproj | 2 +- 44 files changed, 2354 insertions(+), 4264 deletions(-) delete mode 100644 Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.Designer.cs delete mode 100644 Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.Designer.cs delete mode 100644 Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.cs delete mode 100644 Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.cs rename Data/Migrations/Sqlite/{20240212024708_AddHeadshotKillsToZombieClientState.Designer.cs => 20240219203501_InitialZombieStats.Designer.cs} (95%) rename Data/Migrations/Sqlite/{20230905194120_IntitialZombieStats.cs => 20240219203501_InitialZombieStats.cs} (55%) create mode 100644 Data/Models/Client/Stats/EFClientStatTag.cs create mode 100644 Data/Models/Client/Stats/EFClientStatTagValue.cs create mode 100644 Data/Models/Client/Stats/EFPerformanceBucket.cs create mode 100644 Plugins/Stats/TalkerPoC/CodResponse.cs create mode 100644 Plugins/ZombieStats/Events/ZombieEventParser.cs create mode 100644 Plugins/ZombieStats/Events/ZombieEventProcessor.cs create mode 100644 Plugins/ZombieStats/States/MatchState.cs create mode 100644 Plugins/ZombieStats/States/RoundState.cs create mode 100644 Plugins/ZombieStats/States/ZombieClientStateManager.cs create mode 100644 Plugins/ZombieStats/ZombieStats.cs rename SharedLibraryCore/Events/Game/GameScript/{Zombie => }/RoundEndEvent.cs (58%) create mode 100644 SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index cfade8ce..91495317 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -5,7 +5,7 @@ net8.0 false RaidMax.IW4MAdmin.Application - 2020.0.0.0 + 2024.0.0.0 RaidMax Forever None IW4MAdmin diff --git a/Application/Extensions/StartupExtensions.cs b/Application/Extensions/StartupExtensions.cs index be6acabe..d7b59a07 100644 --- a/Application/Extensions/StartupExtensions.cs +++ b/Application/Extensions/StartupExtensions.cs @@ -47,9 +47,9 @@ namespace IW4MAdmin.Application.Extensions { loggerConfig = loggerConfig.WriteTo.Console( outputTemplate: - "[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}") - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Debug(); + "{Message:lj}{NewLine}{Exception}") + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Warning(); } _defaultLogger = loggerConfig.CreateLogger(); diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 810b5579..35db2f72 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -1458,7 +1458,7 @@ namespace IW4MAdmin MaxClients = maxplayers; FSGame = game.Value; Gametype = gametype; - IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress; + IP = ServerConfig.IPAddress; GamePassword = gamePassword.Value; PrivateClientSlots = privateClients.Value; diff --git a/Application/Main.cs b/Application/Main.cs index 0362688b..12193b40 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -59,7 +59,7 @@ namespace IW4MAdmin.Application /// entrypoint of the application /// /// - public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25) + public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 40, int? requestQueueLimit = 40) { AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory); AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => diff --git a/Data/Context/DatabaseContext.cs b/Data/Context/DatabaseContext.cs index 81f18afd..ae60d6e1 100644 --- a/Data/Context/DatabaseContext.cs +++ b/Data/Context/DatabaseContext.cs @@ -38,6 +38,9 @@ namespace Data.Context public DbSet HitStatistics { get; set; } public DbSet Weapons { get; set; } public DbSet WeaponAttachments { get; set; } + + public DbSet ClientStatTags { get; set; } + public DbSet ClientStatTagValues { get; set; } public DbSet Maps { get; set; } #endregion @@ -58,7 +61,7 @@ namespace Data.Context public DbSet ZombieClientStatAggregates { get; set; } public DbSet ZombieClientStatRecords { get; set; } public DbSet ZombieEvents { get; set; } - + #endregion private void SetAuditColumns() @@ -103,19 +106,6 @@ namespace Data.Context client.NetworkId, client.GameName }); - - - /* entity.HasMany(prop => prop.ZombieMatchClientStats) - .WithOne(prop => prop.Client) - .HasForeignKey(prop => prop.ClientId); - - entity.HasMany(prop => prop.ZombieRoundClientStats) - .WithOne(prop => prop.Client) - .HasForeignKey(prop => prop.ClientId); - - entity.HasMany(prop => prop.ZombieAggregateClientStats) - .WithOne(prop => prop.Client) - .HasForeignKey(prop => prop.ClientId);*/ }); modelBuilder.Entity(entity => @@ -185,11 +175,11 @@ namespace Data.Context modelBuilder.Entity().ToTable(nameof(EFServerSnapshot)); modelBuilder.Entity().ToTable(nameof(EFClientConnectionHistory)); - modelBuilder.Entity().ToTable($"EF{nameof(ZombieMatch)}"); + modelBuilder.Entity().ToTable($"EF{nameof(ZombieMatches)}"); modelBuilder.Entity(ent => { - ent.ToTable($"EF{nameof(ZombieClientStat)}"); + ent.ToTable($"EF{nameof(ZombieClientStat)}s"); ent.HasOne(prop => prop.Client) .WithMany(prop => prop.ZombieClientStats) .HasForeignKey(prop => prop.ClientId); @@ -197,23 +187,39 @@ namespace Data.Context modelBuilder.Entity(ent => { - ent.ToTable($"EF{nameof(ZombieMatchClientStat)}"); + ent.ToTable($"EF{nameof(ZombieMatchClientStats)}"); }); modelBuilder.Entity(ent => { - ent.ToTable($"EF{nameof(ZombieRoundClientStat)}"); + ent.ToTable($"EF{nameof(ZombieRoundClientStats)}"); }); modelBuilder.Entity(ent => { - ent.ToTable($"EF{nameof(ZombieAggregateClientStat)}"); + ent.ToTable($"EF{nameof(ZombieClientStatAggregates)}"); }); - modelBuilder.Entity().ToTable($"EF{nameof(ZombieEvents)}"); - - modelBuilder.Entity().ToTable($"EF{nameof(ZombieClientStatRecord)}"); + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieEvents)}"); + }); + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieClientStatRecords)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ClientStatTags)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ClientStatTagValues)}"); + }); + Models.Configuration.StatsModelConfiguration.Configure(modelBuilder); base.OnModelCreating(modelBuilder); diff --git a/Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.Designer.cs b/Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.Designer.cs deleted file mode 100644 index 8bb4b764..00000000 --- a/Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.Designer.cs +++ /dev/null @@ -1,1959 +0,0 @@ -// -using System; -using Data.MigrationContext; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Data.Migrations.Sqlite -{ - [DbContext(typeof(SqliteDatabaseContext))] - [Migration("20230905194120_IntitialZombieStats")] - partial class IntitialZombieStats - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); - - modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => - { - b.Property("ACSnapshotVector3Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("SnapshotId") - .HasColumnType("INTEGER"); - - b.Property("Vector3Id") - .HasColumnType("INTEGER"); - - b.HasKey("ACSnapshotVector3Id"); - - b.HasIndex("SnapshotId"); - - b.HasIndex("Vector3Id"); - - b.ToTable("EFACSnapshotVector3", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.Property("ClientId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AliasLinkId") - .HasColumnType("INTEGER"); - - b.Property("Connections") - .HasColumnType("INTEGER"); - - b.Property("CurrentAliasId") - .HasColumnType("INTEGER"); - - b.Property("FirstConnection") - .HasColumnType("TEXT"); - - b.Property("GameName") - .HasColumnType("INTEGER"); - - b.Property("LastConnection") - .HasColumnType("TEXT"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Masked") - .HasColumnType("INTEGER"); - - b.Property("NetworkId") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasColumnType("TEXT"); - - b.Property("PasswordSalt") - .HasColumnType("TEXT"); - - b.Property("TotalConnectionTime") - .HasColumnType("INTEGER"); - - b.HasKey("ClientId"); - - b.HasAlternateKey("NetworkId", "GameName"); - - b.HasIndex("AliasLinkId"); - - b.HasIndex("CurrentAliasId"); - - b.HasIndex("LastConnection"); - - b.HasIndex("NetworkId"); - - b.ToTable("EFClients", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => - { - b.Property("ClientConnectionId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("ConnectionType") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ClientConnectionId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CreatedDateTime"); - - b.HasIndex("ServerId"); - - b.ToTable("EFClientConnectionHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientKill", b => - { - b.Property("KillId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AttackerId") - .HasColumnType("INTEGER"); - - b.Property("Damage") - .HasColumnType("INTEGER"); - - b.Property("DeathOriginVector3Id") - .HasColumnType("INTEGER"); - - b.Property("DeathType") - .HasColumnType("INTEGER"); - - b.Property("Fraction") - .HasColumnType("REAL"); - - b.Property("HitLoc") - .HasColumnType("INTEGER"); - - b.Property("IsKill") - .HasColumnType("INTEGER"); - - b.Property("KillOriginVector3Id") - .HasColumnType("INTEGER"); - - b.Property("Map") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("VictimId") - .HasColumnType("INTEGER"); - - b.Property("ViewAnglesVector3Id") - .HasColumnType("INTEGER"); - - b.Property("VisibilityPercentage") - .HasColumnType("REAL"); - - b.Property("Weapon") - .HasColumnType("INTEGER"); - - b.Property("WeaponReference") - .HasColumnType("TEXT"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("KillId"); - - b.HasIndex("AttackerId"); - - b.HasIndex("DeathOriginVector3Id"); - - b.HasIndex("KillOriginVector3Id"); - - b.HasIndex("ServerId"); - - b.HasIndex("VictimId"); - - b.HasIndex("ViewAnglesVector3Id"); - - b.ToTable("EFClientKills", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => - { - b.Property("MessageId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("SentIngame") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TimeSent") - .HasColumnType("TEXT"); - - b.HasKey("MessageId"); - - b.HasIndex("ClientId"); - - b.HasIndex("ServerId"); - - b.HasIndex("TimeSent"); - - b.ToTable("EFClientMessages", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.Property("SnapshotId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CurrentSessionLength") - .HasColumnType("INTEGER"); - - b.Property("CurrentStrain") - .HasColumnType("REAL"); - - b.Property("CurrentViewAngleId") - .HasColumnType("INTEGER"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("Distance") - .HasColumnType("REAL"); - - b.Property("EloRating") - .HasColumnType("REAL"); - - b.Property("HitDestinationId") - .HasColumnType("INTEGER"); - - b.Property("HitLocation") - .HasColumnType("INTEGER"); - - b.Property("HitLocationReference") - .HasColumnType("TEXT"); - - b.Property("HitOriginId") - .HasColumnType("INTEGER"); - - b.Property("HitType") - .HasColumnType("INTEGER"); - - b.Property("Hits") - .HasColumnType("INTEGER"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("LastStrainAngleId") - .HasColumnType("INTEGER"); - - b.Property("RecoilOffset") - .HasColumnType("REAL"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SessionAngleOffset") - .HasColumnType("REAL"); - - b.Property("SessionAverageSnapValue") - .HasColumnType("REAL"); - - b.Property("SessionSPM") - .HasColumnType("REAL"); - - b.Property("SessionScore") - .HasColumnType("INTEGER"); - - b.Property("SessionSnapHits") - .HasColumnType("INTEGER"); - - b.Property("StrainAngleBetween") - .HasColumnType("REAL"); - - b.Property("TimeSinceLastEvent") - .HasColumnType("INTEGER"); - - b.Property("WeaponId") - .HasColumnType("INTEGER"); - - b.Property("WeaponReference") - .HasColumnType("TEXT"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("SnapshotId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CurrentViewAngleId"); - - b.HasIndex("HitDestinationId"); - - b.HasIndex("HitOriginId"); - - b.HasIndex("LastStrainAngleId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFACSnapshot", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => - { - b.Property("ClientHitStatisticId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DamageInflicted") - .HasColumnType("INTEGER"); - - b.Property("DamageReceived") - .HasColumnType("INTEGER"); - - b.Property("DeathCount") - .HasColumnType("INTEGER"); - - b.Property("HitCount") - .HasColumnType("INTEGER"); - - b.Property("HitLocationId") - .HasColumnType("INTEGER"); - - b.Property("KillCount") - .HasColumnType("INTEGER"); - - b.Property("MeansOfDeathId") - .HasColumnType("INTEGER"); - - b.Property("ReceivedHitCount") - .HasColumnType("INTEGER"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SuicideCount") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("UsageSeconds") - .HasColumnType("INTEGER"); - - b.Property("WeaponAttachmentComboId") - .HasColumnType("INTEGER"); - - b.Property("WeaponId") - .HasColumnType("INTEGER"); - - b.HasKey("ClientHitStatisticId"); - - b.HasIndex("ClientId"); - - b.HasIndex("HitLocationId"); - - b.HasIndex("MeansOfDeathId"); - - b.HasIndex("ServerId"); - - b.HasIndex("WeaponAttachmentComboId"); - - b.HasIndex("WeaponId"); - - b.ToTable("EFClientHitStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => - { - b.Property("ClientRankingHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Newest") - .HasColumnType("INTEGER"); - - b.Property("PerformanceBucket") - .HasColumnType("TEXT"); - - b.Property("PerformanceMetric") - .HasColumnType("REAL"); - - b.Property("Ranking") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("ZScore") - .HasColumnType("REAL"); - - b.HasKey("ClientRankingHistoryId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CreatedDateTime"); - - b.HasIndex("Ranking"); - - b.HasIndex("ServerId"); - - b.HasIndex("UpdatedDateTime"); - - b.HasIndex("ZScore"); - - b.ToTable("EFClientRankingHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.Property("RatingHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.HasKey("RatingHistoryId"); - - b.HasIndex("ClientId"); - - b.ToTable("EFClientRatingHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AverageSnapValue") - .HasColumnType("REAL"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("EloRating") - .HasColumnType("REAL"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("MaxStrain") - .HasColumnType("REAL"); - - b.Property("RollingWeightedKDR") - .HasColumnType("REAL"); - - b.Property("SPM") - .HasColumnType("REAL"); - - b.Property("Skill") - .HasColumnType("REAL"); - - b.Property("SnapHitCount") - .HasColumnType("INTEGER"); - - b.Property("TimePlayed") - .HasColumnType("INTEGER"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("ZScore") - .HasColumnType("REAL"); - - b.HasKey("ClientId", "ServerId"); - - b.HasIndex("ServerId"); - - b.HasIndex("ZScore"); - - b.HasIndex("ClientId", "TimePlayed", "ZScore"); - - b.ToTable("EFClientStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => - { - b.Property("HitLocationCountId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("EFClientStatisticsClientId") - .HasColumnType("INTEGER") - .HasColumnName("EFClientStatisticsClientId"); - - b.Property("EFClientStatisticsServerId") - .HasColumnType("INTEGER") - .HasColumnName("EFClientStatisticsServerId"); - - b.Property("HitCount") - .HasColumnType("INTEGER"); - - b.Property("HitOffsetAverage") - .HasColumnType("REAL"); - - b.Property("Location") - .HasColumnType("INTEGER"); - - b.Property("MaxAngleDistance") - .HasColumnType("REAL"); - - b.HasKey("HitLocationCountId"); - - b.HasIndex("EFClientStatisticsServerId"); - - b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); - - b.ToTable("EFHitLocationCounts", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => - { - b.Property("RatingId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ActivityAmount") - .HasColumnType("INTEGER"); - - b.Property("Newest") - .HasColumnType("INTEGER"); - - b.Property("Performance") - .HasColumnType("REAL"); - - b.Property("Ranking") - .HasColumnType("INTEGER"); - - b.Property("RatingHistoryId") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("RatingId"); - - b.HasIndex("RatingHistoryId"); - - b.HasIndex("ServerId"); - - b.HasIndex("Performance", "Ranking", "When"); - - b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); - - b.ToTable("EFRating", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => - { - b.Property("HitLocationId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("HitLocationId"); - - b.HasIndex("Name"); - - b.ToTable("EFHitLocations", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => - { - b.Property("MapId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("MapId"); - - b.ToTable("EFMaps", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => - { - b.Property("MeansOfDeathId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("MeansOfDeathId"); - - b.ToTable("EFMeansOfDeath", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => - { - b.Property("WeaponId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponId"); - - b.HasIndex("Name"); - - b.ToTable("EFWeapons", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => - { - b.Property("WeaponAttachmentId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponAttachmentId"); - - b.ToTable("EFWeaponAttachments", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => - { - b.Property("WeaponAttachmentComboId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Attachment1Id") - .HasColumnType("INTEGER"); - - b.Property("Attachment2Id") - .HasColumnType("INTEGER"); - - b.Property("Attachment3Id") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponAttachmentComboId"); - - b.HasIndex("Attachment1Id"); - - b.HasIndex("Attachment2Id"); - - b.HasIndex("Attachment3Id"); - - b.ToTable("EFWeaponAttachmentCombos", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFAlias", b => - { - b.Property("AliasId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("DateAdded") - .HasColumnType("TEXT"); - - b.Property("IPAddress") - .HasColumnType("INTEGER"); - - b.Property("LinkId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(24) - .HasColumnType("TEXT"); - - b.Property("SearchableIPAddress") - .ValueGeneratedOnAddOrUpdate() - .HasMaxLength(255) - .HasColumnType("TEXT") - .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); - - b.Property("SearchableName") - .HasMaxLength(24) - .HasColumnType("TEXT"); - - b.HasKey("AliasId"); - - b.HasIndex("IPAddress"); - - b.HasIndex("LinkId"); - - b.HasIndex("Name"); - - b.HasIndex("SearchableIPAddress"); - - b.HasIndex("SearchableName"); - - b.HasIndex("Name", "IPAddress"); - - b.ToTable("EFAlias", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFAliasLink", b => - { - b.Property("AliasLinkId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.HasKey("AliasLinkId"); - - b.ToTable("EFAliasLinks", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFChangeHistory", b => - { - b.Property("ChangeHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("CurrentValue") - .HasColumnType("TEXT"); - - b.Property("ImpersonationEntityId") - .HasColumnType("INTEGER"); - - b.Property("OriginEntityId") - .HasColumnType("INTEGER"); - - b.Property("PreviousValue") - .HasColumnType("TEXT"); - - b.Property("TargetEntityId") - .HasColumnType("INTEGER"); - - b.Property("TimeChanged") - .HasColumnType("TEXT"); - - b.Property("TypeOfChange") - .HasColumnType("INTEGER"); - - b.HasKey("ChangeHistoryId"); - - b.ToTable("EFChangeHistory"); - }); - - modelBuilder.Entity("Data.Models.EFMeta", b => - { - b.Property("MetaId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("Extra") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("LinkedMetaId") - .HasColumnType("INTEGER"); - - b.Property("Updated") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("MetaId"); - - b.HasIndex("ClientId"); - - b.HasIndex("Key"); - - b.HasIndex("LinkedMetaId"); - - b.ToTable("EFMeta"); - }); - - modelBuilder.Entity("Data.Models.EFPenalty", b => - { - b.Property("PenaltyId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AutomatedOffense") - .HasColumnType("TEXT"); - - b.Property("Expires") - .HasColumnType("TEXT"); - - b.Property("IsEvadedOffense") - .HasColumnType("INTEGER"); - - b.Property("LinkId") - .HasColumnType("INTEGER"); - - b.Property("OffenderId") - .HasColumnType("INTEGER"); - - b.Property("Offense") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PunisherId") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("PenaltyId"); - - b.HasIndex("LinkId"); - - b.HasIndex("OffenderId"); - - b.HasIndex("PunisherId"); - - b.ToTable("EFPenalties", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => - { - b.Property("PenaltyIdentifierId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("IPv4Address") - .HasColumnType("INTEGER"); - - b.Property("NetworkId") - .HasColumnType("INTEGER"); - - b.Property("PenaltyId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("PenaltyIdentifierId"); - - b.HasIndex("IPv4Address"); - - b.HasIndex("NetworkId"); - - b.HasIndex("PenaltyId"); - - b.ToTable("EFPenaltyIdentifiers", (string)null); - }); - - modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => - { - b.Property("InboxMessageId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DestinationClientId") - .HasColumnType("INTEGER"); - - b.Property("IsDelivered") - .HasColumnType("INTEGER"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SourceClientId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("InboxMessageId"); - - b.HasIndex("DestinationClientId"); - - b.HasIndex("ServerId"); - - b.HasIndex("SourceClientId"); - - b.ToTable("InboxMessages"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServer", b => - { - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("EndPoint") - .HasColumnType("TEXT"); - - b.Property("GameName") - .HasColumnType("INTEGER"); - - b.Property("HostName") - .HasColumnType("TEXT"); - - b.Property("IsPasswordProtected") - .HasColumnType("INTEGER"); - - b.Property("PerformanceBucket") - .HasColumnType("TEXT"); - - b.Property("Port") - .HasColumnType("INTEGER"); - - b.HasKey("ServerId"); - - b.ToTable("EFServers", (string)null); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => - { - b.Property("ServerSnapshotId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("CapturedAt") - .HasColumnType("TEXT"); - - b.Property("ClientCount") - .HasColumnType("INTEGER"); - - b.Property("ConnectionInterrupted") - .HasColumnType("INTEGER"); - - b.Property("MapId") - .HasColumnType("INTEGER"); - - b.Property("PeriodBlock") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.HasKey("ServerSnapshotId"); - - b.HasIndex("CapturedAt"); - - b.HasIndex("MapId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFServerSnapshot", (string)null); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => - { - b.Property("StatisticId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TotalKills") - .HasColumnType("INTEGER"); - - b.Property("TotalPlayTime") - .HasColumnType("INTEGER"); - - b.HasKey("StatisticId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFServerStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Vector3", b => - { - b.Property("Vector3Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("X") - .HasColumnType("REAL"); - - b.Property("Y") - .HasColumnType("REAL"); - - b.Property("Z") - .HasColumnType("REAL"); - - b.HasKey("Vector3Id"); - - b.ToTable("Vector3", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => - { - b.Property("ZombieClientStatId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DamageDealt") - .HasColumnType("INTEGER"); - - b.Property("DamageReceived") - .HasColumnType("INTEGER"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("Downs") - .HasColumnType("INTEGER"); - - b.Property("Headshots") - .HasColumnType("INTEGER"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("MatchId") - .HasColumnType("INTEGER"); - - b.Property("Melees") - .HasColumnType("INTEGER"); - - b.Property("PerksConsumed") - .HasColumnType("INTEGER"); - - b.Property("PointsEarned") - .HasColumnType("INTEGER"); - - b.Property("PointsSpent") - .HasColumnType("INTEGER"); - - b.Property("PowerupsGrabbed") - .HasColumnType("INTEGER"); - - b.Property("Revives") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ZombieClientStatId"); - - b.HasIndex("ClientId"); - - b.HasIndex("MatchId"); - - b.ToTable("EFZombieClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => - { - b.Property("ZombieClientStatRecordId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RoundId") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ZombieClientStatRecordId"); - - b.HasIndex("ClientId"); - - b.HasIndex("RoundId"); - - b.ToTable("EFZombieClientStatRecord", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => - { - b.Property("ZombieMatchId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientsCompleted") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("EFClientClientId") - .HasColumnType("INTEGER"); - - b.Property("MapId") - .HasColumnType("INTEGER"); - - b.Property("MatchEndDate") - .HasColumnType("TEXT"); - - b.Property("MatchStartDate") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ZombieMatchId"); - - b.HasIndex("EFClientClientId"); - - b.HasIndex("MapId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFZombieMatch", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.Property("AlivePercentage") - .HasColumnType("REAL"); - - b.Property("AverageDowns") - .HasColumnType("REAL"); - - b.Property("AverageKillsPerDown") - .HasColumnType("REAL"); - - b.Property("AverageMelees") - .HasColumnType("REAL"); - - b.Property("AveragePoints") - .HasColumnType("REAL"); - - b.Property("AverageRevives") - .HasColumnType("REAL"); - - b.Property("AverageRoundReached") - .HasColumnType("REAL"); - - b.Property("HeadshotPercentage") - .HasColumnType("REAL"); - - b.Property("HighestRound") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TotalMatchesCompleted") - .HasColumnType("INTEGER"); - - b.Property("TotalMatchesPlayed") - .HasColumnType("INTEGER"); - - b.Property("TotalRoundsPlayed") - .HasColumnType("INTEGER"); - - b.HasIndex("ServerId"); - - b.ToTable("EFZombieAggregateClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.ToTable("EFZombieMatchClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("Points") - .HasColumnType("INTEGER"); - - b.Property("RoundNumber") - .HasColumnType("INTEGER"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("TimeAlive") - .HasColumnType("TEXT"); - - b.ToTable("EFZombieRoundClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => - { - b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") - .WithMany("PredictedViewAngles") - .HasForeignKey("SnapshotId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "Vector") - .WithMany() - .HasForeignKey("Vector3Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Snapshot"); - - b.Navigation("Vector"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.HasOne("Data.Models.EFAliasLink", "AliasLink") - .WithMany() - .HasForeignKey("AliasLinkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.EFAlias", "CurrentAlias") - .WithMany() - .HasForeignKey("CurrentAliasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AliasLink"); - - b.Navigation("CurrentAlias"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientKill", b => - { - b.HasOne("Data.Models.Client.EFClient", "Attacker") - .WithMany() - .HasForeignKey("AttackerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "DeathOrigin") - .WithMany() - .HasForeignKey("DeathOriginVector3Id"); - - b.HasOne("Data.Models.Vector3", "KillOrigin") - .WithMany() - .HasForeignKey("KillOriginVector3Id"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.EFClient", "Victim") - .WithMany() - .HasForeignKey("VictimId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "ViewAngles") - .WithMany() - .HasForeignKey("ViewAnglesVector3Id"); - - b.Navigation("Attacker"); - - b.Navigation("DeathOrigin"); - - b.Navigation("KillOrigin"); - - b.Navigation("Server"); - - b.Navigation("Victim"); - - b.Navigation("ViewAngles"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "CurrentViewAngle") - .WithMany() - .HasForeignKey("CurrentViewAngleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "HitDestination") - .WithMany() - .HasForeignKey("HitDestinationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "HitOrigin") - .WithMany() - .HasForeignKey("HitOriginId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "LastStrainAngle") - .WithMany() - .HasForeignKey("LastStrainAngleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Client"); - - b.Navigation("CurrentViewAngle"); - - b.Navigation("HitDestination"); - - b.Navigation("HitOrigin"); - - b.Navigation("LastStrainAngle"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") - .WithMany() - .HasForeignKey("HitLocationId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") - .WithMany() - .HasForeignKey("MeansOfDeathId"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") - .WithMany() - .HasForeignKey("WeaponAttachmentComboId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") - .WithMany() - .HasForeignKey("WeaponId"); - - b.Navigation("Client"); - - b.Navigation("HitLocation"); - - b.Navigation("MeansOfDeath"); - - b.Navigation("Server"); - - b.Navigation("Weapon"); - - b.Navigation("WeaponAttachmentCombo"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("EFClientStatisticsClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("EFClientStatisticsServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) - .WithMany("HitLocations") - .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => - { - b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") - .WithMany("Ratings") - .HasForeignKey("RatingHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("RatingHistory"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => - { - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") - .WithMany() - .HasForeignKey("Attachment1Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") - .WithMany() - .HasForeignKey("Attachment2Id"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") - .WithMany() - .HasForeignKey("Attachment3Id"); - - b.Navigation("Attachment1"); - - b.Navigation("Attachment2"); - - b.Navigation("Attachment3"); - }); - - modelBuilder.Entity("Data.Models.EFAlias", b => - { - b.HasOne("Data.Models.EFAliasLink", "Link") - .WithMany("Children") - .HasForeignKey("LinkId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Link"); - }); - - modelBuilder.Entity("Data.Models.EFMeta", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany("Meta") - .HasForeignKey("ClientId"); - - b.HasOne("Data.Models.EFMeta", "LinkedMeta") - .WithMany() - .HasForeignKey("LinkedMetaId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Client"); - - b.Navigation("LinkedMeta"); - }); - - modelBuilder.Entity("Data.Models.EFPenalty", b => - { - b.HasOne("Data.Models.EFAliasLink", "Link") - .WithMany("ReceivedPenalties") - .HasForeignKey("LinkId"); - - b.HasOne("Data.Models.Client.EFClient", "Offender") - .WithMany("ReceivedPenalties") - .HasForeignKey("OffenderId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Data.Models.Client.EFClient", "Punisher") - .WithMany("AdministeredPenalties") - .HasForeignKey("PunisherId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Link"); - - b.Navigation("Offender"); - - b.Navigation("Punisher"); - }); - - modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => - { - b.HasOne("Data.Models.EFPenalty", "Penalty") - .WithMany() - .HasForeignKey("PenaltyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Penalty"); - }); - - modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => - { - b.HasOne("Data.Models.Client.EFClient", "DestinationClient") - .WithMany() - .HasForeignKey("DestinationClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Client.EFClient", "SourceClient") - .WithMany() - .HasForeignKey("SourceClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DestinationClient"); - - b.Navigation("Server"); - - b.Navigation("SourceClient"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => - { - b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") - .WithMany() - .HasForeignKey("MapId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Map"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => - { - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany("ZombieClientStats") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") - .WithMany() - .HasForeignKey("MatchId"); - - b.Navigation("Client"); - - b.Navigation("Match"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId"); - - b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") - .WithMany() - .HasForeignKey("RoundId"); - - b.Navigation("Client"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => - { - b.HasOne("Data.Models.Client.EFClient", null) - .WithMany("ZombieMatches") - .HasForeignKey("EFClientClientId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") - .WithMany() - .HasForeignKey("MapId"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Map"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => - { - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => - { - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => - { - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.Navigation("AdministeredPenalties"); - - b.Navigation("Meta"); - - b.Navigation("ReceivedPenalties"); - - b.Navigation("ZombieClientStats"); - - b.Navigation("ZombieMatches"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.Navigation("PredictedViewAngles"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.Navigation("Ratings"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.Navigation("HitLocations"); - }); - - modelBuilder.Entity("Data.Models.EFAliasLink", b => - { - b.Navigation("Children"); - - b.Navigation("ReceivedPenalties"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.Designer.cs b/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.Designer.cs deleted file mode 100644 index ff2da29b..00000000 --- a/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.Designer.cs +++ /dev/null @@ -1,2010 +0,0 @@ -// -using System; -using Data.MigrationContext; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Data.Migrations.Sqlite -{ - [DbContext(typeof(SqliteDatabaseContext))] - [Migration("20230906230124_AddZombieStatsEventLog")] - partial class AddZombieStatsEventLog - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); - - modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => - { - b.Property("ACSnapshotVector3Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("SnapshotId") - .HasColumnType("INTEGER"); - - b.Property("Vector3Id") - .HasColumnType("INTEGER"); - - b.HasKey("ACSnapshotVector3Id"); - - b.HasIndex("SnapshotId"); - - b.HasIndex("Vector3Id"); - - b.ToTable("EFACSnapshotVector3", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.Property("ClientId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AliasLinkId") - .HasColumnType("INTEGER"); - - b.Property("Connections") - .HasColumnType("INTEGER"); - - b.Property("CurrentAliasId") - .HasColumnType("INTEGER"); - - b.Property("FirstConnection") - .HasColumnType("TEXT"); - - b.Property("GameName") - .HasColumnType("INTEGER"); - - b.Property("LastConnection") - .HasColumnType("TEXT"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Masked") - .HasColumnType("INTEGER"); - - b.Property("NetworkId") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasColumnType("TEXT"); - - b.Property("PasswordSalt") - .HasColumnType("TEXT"); - - b.Property("TotalConnectionTime") - .HasColumnType("INTEGER"); - - b.HasKey("ClientId"); - - b.HasAlternateKey("NetworkId", "GameName"); - - b.HasIndex("AliasLinkId"); - - b.HasIndex("CurrentAliasId"); - - b.HasIndex("LastConnection"); - - b.HasIndex("NetworkId"); - - b.ToTable("EFClients", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => - { - b.Property("ClientConnectionId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("ConnectionType") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ClientConnectionId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CreatedDateTime"); - - b.HasIndex("ServerId"); - - b.ToTable("EFClientConnectionHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientKill", b => - { - b.Property("KillId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AttackerId") - .HasColumnType("INTEGER"); - - b.Property("Damage") - .HasColumnType("INTEGER"); - - b.Property("DeathOriginVector3Id") - .HasColumnType("INTEGER"); - - b.Property("DeathType") - .HasColumnType("INTEGER"); - - b.Property("Fraction") - .HasColumnType("REAL"); - - b.Property("HitLoc") - .HasColumnType("INTEGER"); - - b.Property("IsKill") - .HasColumnType("INTEGER"); - - b.Property("KillOriginVector3Id") - .HasColumnType("INTEGER"); - - b.Property("Map") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("VictimId") - .HasColumnType("INTEGER"); - - b.Property("ViewAnglesVector3Id") - .HasColumnType("INTEGER"); - - b.Property("VisibilityPercentage") - .HasColumnType("REAL"); - - b.Property("Weapon") - .HasColumnType("INTEGER"); - - b.Property("WeaponReference") - .HasColumnType("TEXT"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("KillId"); - - b.HasIndex("AttackerId"); - - b.HasIndex("DeathOriginVector3Id"); - - b.HasIndex("KillOriginVector3Id"); - - b.HasIndex("ServerId"); - - b.HasIndex("VictimId"); - - b.HasIndex("ViewAnglesVector3Id"); - - b.ToTable("EFClientKills", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => - { - b.Property("MessageId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("SentIngame") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TimeSent") - .HasColumnType("TEXT"); - - b.HasKey("MessageId"); - - b.HasIndex("ClientId"); - - b.HasIndex("ServerId"); - - b.HasIndex("TimeSent"); - - b.ToTable("EFClientMessages", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.Property("SnapshotId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CurrentSessionLength") - .HasColumnType("INTEGER"); - - b.Property("CurrentStrain") - .HasColumnType("REAL"); - - b.Property("CurrentViewAngleId") - .HasColumnType("INTEGER"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("Distance") - .HasColumnType("REAL"); - - b.Property("EloRating") - .HasColumnType("REAL"); - - b.Property("HitDestinationId") - .HasColumnType("INTEGER"); - - b.Property("HitLocation") - .HasColumnType("INTEGER"); - - b.Property("HitLocationReference") - .HasColumnType("TEXT"); - - b.Property("HitOriginId") - .HasColumnType("INTEGER"); - - b.Property("HitType") - .HasColumnType("INTEGER"); - - b.Property("Hits") - .HasColumnType("INTEGER"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("LastStrainAngleId") - .HasColumnType("INTEGER"); - - b.Property("RecoilOffset") - .HasColumnType("REAL"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SessionAngleOffset") - .HasColumnType("REAL"); - - b.Property("SessionAverageSnapValue") - .HasColumnType("REAL"); - - b.Property("SessionSPM") - .HasColumnType("REAL"); - - b.Property("SessionScore") - .HasColumnType("INTEGER"); - - b.Property("SessionSnapHits") - .HasColumnType("INTEGER"); - - b.Property("StrainAngleBetween") - .HasColumnType("REAL"); - - b.Property("TimeSinceLastEvent") - .HasColumnType("INTEGER"); - - b.Property("WeaponId") - .HasColumnType("INTEGER"); - - b.Property("WeaponReference") - .HasColumnType("TEXT"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("SnapshotId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CurrentViewAngleId"); - - b.HasIndex("HitDestinationId"); - - b.HasIndex("HitOriginId"); - - b.HasIndex("LastStrainAngleId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFACSnapshot", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => - { - b.Property("ClientHitStatisticId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DamageInflicted") - .HasColumnType("INTEGER"); - - b.Property("DamageReceived") - .HasColumnType("INTEGER"); - - b.Property("DeathCount") - .HasColumnType("INTEGER"); - - b.Property("HitCount") - .HasColumnType("INTEGER"); - - b.Property("HitLocationId") - .HasColumnType("INTEGER"); - - b.Property("KillCount") - .HasColumnType("INTEGER"); - - b.Property("MeansOfDeathId") - .HasColumnType("INTEGER"); - - b.Property("ReceivedHitCount") - .HasColumnType("INTEGER"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SuicideCount") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("UsageSeconds") - .HasColumnType("INTEGER"); - - b.Property("WeaponAttachmentComboId") - .HasColumnType("INTEGER"); - - b.Property("WeaponId") - .HasColumnType("INTEGER"); - - b.HasKey("ClientHitStatisticId"); - - b.HasIndex("ClientId"); - - b.HasIndex("HitLocationId"); - - b.HasIndex("MeansOfDeathId"); - - b.HasIndex("ServerId"); - - b.HasIndex("WeaponAttachmentComboId"); - - b.HasIndex("WeaponId"); - - b.ToTable("EFClientHitStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => - { - b.Property("ClientRankingHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Newest") - .HasColumnType("INTEGER"); - - b.Property("PerformanceBucket") - .HasColumnType("TEXT"); - - b.Property("PerformanceMetric") - .HasColumnType("REAL"); - - b.Property("Ranking") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("ZScore") - .HasColumnType("REAL"); - - b.HasKey("ClientRankingHistoryId"); - - b.HasIndex("ClientId"); - - b.HasIndex("CreatedDateTime"); - - b.HasIndex("Ranking"); - - b.HasIndex("ServerId"); - - b.HasIndex("UpdatedDateTime"); - - b.HasIndex("ZScore"); - - b.ToTable("EFClientRankingHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.Property("RatingHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.HasKey("RatingHistoryId"); - - b.HasIndex("ClientId"); - - b.ToTable("EFClientRatingHistory", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AverageSnapValue") - .HasColumnType("REAL"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("EloRating") - .HasColumnType("REAL"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("MaxStrain") - .HasColumnType("REAL"); - - b.Property("RollingWeightedKDR") - .HasColumnType("REAL"); - - b.Property("SPM") - .HasColumnType("REAL"); - - b.Property("Skill") - .HasColumnType("REAL"); - - b.Property("SnapHitCount") - .HasColumnType("INTEGER"); - - b.Property("TimePlayed") - .HasColumnType("INTEGER"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("ZScore") - .HasColumnType("REAL"); - - b.HasKey("ClientId", "ServerId"); - - b.HasIndex("ServerId"); - - b.HasIndex("ZScore"); - - b.HasIndex("ClientId", "TimePlayed", "ZScore"); - - b.ToTable("EFClientStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => - { - b.Property("HitLocationCountId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("EFClientStatisticsClientId") - .HasColumnType("INTEGER") - .HasColumnName("EFClientStatisticsClientId"); - - b.Property("EFClientStatisticsServerId") - .HasColumnType("INTEGER") - .HasColumnName("EFClientStatisticsServerId"); - - b.Property("HitCount") - .HasColumnType("INTEGER"); - - b.Property("HitOffsetAverage") - .HasColumnType("REAL"); - - b.Property("Location") - .HasColumnType("INTEGER"); - - b.Property("MaxAngleDistance") - .HasColumnType("REAL"); - - b.HasKey("HitLocationCountId"); - - b.HasIndex("EFClientStatisticsServerId"); - - b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); - - b.ToTable("EFHitLocationCounts", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => - { - b.Property("RatingId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ActivityAmount") - .HasColumnType("INTEGER"); - - b.Property("Newest") - .HasColumnType("INTEGER"); - - b.Property("Performance") - .HasColumnType("REAL"); - - b.Property("Ranking") - .HasColumnType("INTEGER"); - - b.Property("RatingHistoryId") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("RatingId"); - - b.HasIndex("RatingHistoryId"); - - b.HasIndex("ServerId"); - - b.HasIndex("Performance", "Ranking", "When"); - - b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); - - b.ToTable("EFRating", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => - { - b.Property("HitLocationId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("HitLocationId"); - - b.HasIndex("Name"); - - b.ToTable("EFHitLocations", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => - { - b.Property("MapId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("MapId"); - - b.ToTable("EFMaps", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => - { - b.Property("MeansOfDeathId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("MeansOfDeathId"); - - b.ToTable("EFMeansOfDeath", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => - { - b.Property("WeaponId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponId"); - - b.HasIndex("Name"); - - b.ToTable("EFWeapons", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => - { - b.Property("WeaponAttachmentId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponAttachmentId"); - - b.ToTable("EFWeaponAttachments", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => - { - b.Property("WeaponAttachmentComboId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Attachment1Id") - .HasColumnType("INTEGER"); - - b.Property("Attachment2Id") - .HasColumnType("INTEGER"); - - b.Property("Attachment3Id") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Game") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("WeaponAttachmentComboId"); - - b.HasIndex("Attachment1Id"); - - b.HasIndex("Attachment2Id"); - - b.HasIndex("Attachment3Id"); - - b.ToTable("EFWeaponAttachmentCombos", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFAlias", b => - { - b.Property("AliasId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("DateAdded") - .HasColumnType("TEXT"); - - b.Property("IPAddress") - .HasColumnType("INTEGER"); - - b.Property("LinkId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(24) - .HasColumnType("TEXT"); - - b.Property("SearchableIPAddress") - .ValueGeneratedOnAddOrUpdate() - .HasMaxLength(255) - .HasColumnType("TEXT") - .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); - - b.Property("SearchableName") - .HasMaxLength(24) - .HasColumnType("TEXT"); - - b.HasKey("AliasId"); - - b.HasIndex("IPAddress"); - - b.HasIndex("LinkId"); - - b.HasIndex("Name"); - - b.HasIndex("SearchableIPAddress"); - - b.HasIndex("SearchableName"); - - b.HasIndex("Name", "IPAddress"); - - b.ToTable("EFAlias", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFAliasLink", b => - { - b.Property("AliasLinkId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.HasKey("AliasLinkId"); - - b.ToTable("EFAliasLinks", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFChangeHistory", b => - { - b.Property("ChangeHistoryId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("Comment") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("CurrentValue") - .HasColumnType("TEXT"); - - b.Property("ImpersonationEntityId") - .HasColumnType("INTEGER"); - - b.Property("OriginEntityId") - .HasColumnType("INTEGER"); - - b.Property("PreviousValue") - .HasColumnType("TEXT"); - - b.Property("TargetEntityId") - .HasColumnType("INTEGER"); - - b.Property("TimeChanged") - .HasColumnType("TEXT"); - - b.Property("TypeOfChange") - .HasColumnType("INTEGER"); - - b.HasKey("ChangeHistoryId"); - - b.ToTable("EFChangeHistory"); - }); - - modelBuilder.Entity("Data.Models.EFMeta", b => - { - b.Property("MetaId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("Extra") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("LinkedMetaId") - .HasColumnType("INTEGER"); - - b.Property("Updated") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("MetaId"); - - b.HasIndex("ClientId"); - - b.HasIndex("Key"); - - b.HasIndex("LinkedMetaId"); - - b.ToTable("EFMeta"); - }); - - modelBuilder.Entity("Data.Models.EFPenalty", b => - { - b.Property("PenaltyId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("AutomatedOffense") - .HasColumnType("TEXT"); - - b.Property("Expires") - .HasColumnType("TEXT"); - - b.Property("IsEvadedOffense") - .HasColumnType("INTEGER"); - - b.Property("LinkId") - .HasColumnType("INTEGER"); - - b.Property("OffenderId") - .HasColumnType("INTEGER"); - - b.Property("Offense") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PunisherId") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("When") - .HasColumnType("TEXT"); - - b.HasKey("PenaltyId"); - - b.HasIndex("LinkId"); - - b.HasIndex("OffenderId"); - - b.HasIndex("PunisherId"); - - b.ToTable("EFPenalties", (string)null); - }); - - modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => - { - b.Property("PenaltyIdentifierId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("IPv4Address") - .HasColumnType("INTEGER"); - - b.Property("NetworkId") - .HasColumnType("INTEGER"); - - b.Property("PenaltyId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("PenaltyIdentifierId"); - - b.HasIndex("IPv4Address"); - - b.HasIndex("NetworkId"); - - b.HasIndex("PenaltyId"); - - b.ToTable("EFPenaltyIdentifiers", (string)null); - }); - - modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => - { - b.Property("InboxMessageId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DestinationClientId") - .HasColumnType("INTEGER"); - - b.Property("IsDelivered") - .HasColumnType("INTEGER"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("SourceClientId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("InboxMessageId"); - - b.HasIndex("DestinationClientId"); - - b.HasIndex("ServerId"); - - b.HasIndex("SourceClientId"); - - b.ToTable("InboxMessages"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServer", b => - { - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("EndPoint") - .HasColumnType("TEXT"); - - b.Property("GameName") - .HasColumnType("INTEGER"); - - b.Property("HostName") - .HasColumnType("TEXT"); - - b.Property("IsPasswordProtected") - .HasColumnType("INTEGER"); - - b.Property("PerformanceBucket") - .HasColumnType("TEXT"); - - b.Property("Port") - .HasColumnType("INTEGER"); - - b.HasKey("ServerId"); - - b.ToTable("EFServers", (string)null); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => - { - b.Property("ServerSnapshotId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("CapturedAt") - .HasColumnType("TEXT"); - - b.Property("ClientCount") - .HasColumnType("INTEGER"); - - b.Property("ConnectionInterrupted") - .HasColumnType("INTEGER"); - - b.Property("MapId") - .HasColumnType("INTEGER"); - - b.Property("PeriodBlock") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.HasKey("ServerSnapshotId"); - - b.HasIndex("CapturedAt"); - - b.HasIndex("MapId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFServerSnapshot", (string)null); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => - { - b.Property("StatisticId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Active") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TotalKills") - .HasColumnType("INTEGER"); - - b.Property("TotalPlayTime") - .HasColumnType("INTEGER"); - - b.HasKey("StatisticId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFServerStatistics", (string)null); - }); - - modelBuilder.Entity("Data.Models.Vector3", b => - { - b.Property("Vector3Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("X") - .HasColumnType("REAL"); - - b.Property("Y") - .HasColumnType("REAL"); - - b.Property("Z") - .HasColumnType("REAL"); - - b.HasKey("Vector3Id"); - - b.ToTable("Vector3", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => - { - b.Property("ZombieClientStatId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("DamageDealt") - .HasColumnType("INTEGER"); - - b.Property("DamageReceived") - .HasColumnType("INTEGER"); - - b.Property("Deaths") - .HasColumnType("INTEGER"); - - b.Property("Downs") - .HasColumnType("INTEGER"); - - b.Property("Headshots") - .HasColumnType("INTEGER"); - - b.Property("Kills") - .HasColumnType("INTEGER"); - - b.Property("MatchId") - .HasColumnType("INTEGER"); - - b.Property("Melees") - .HasColumnType("INTEGER"); - - b.Property("PerksConsumed") - .HasColumnType("INTEGER"); - - b.Property("PointsEarned") - .HasColumnType("INTEGER"); - - b.Property("PointsSpent") - .HasColumnType("INTEGER"); - - b.Property("PowerupsGrabbed") - .HasColumnType("INTEGER"); - - b.Property("Revives") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ZombieClientStatId"); - - b.HasIndex("ClientId"); - - b.HasIndex("MatchId"); - - b.ToTable("EFZombieClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => - { - b.Property("ZombieClientStatRecordId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RoundId") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ZombieClientStatRecordId"); - - b.HasIndex("ClientId"); - - b.HasIndex("RoundId"); - - b.ToTable("EFZombieClientStatRecord", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => - { - b.Property("ZombieEventLogId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AssociatedClientId") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("EventType") - .HasColumnType("INTEGER"); - - b.Property("MatchId") - .HasColumnType("INTEGER"); - - b.Property("NumericalValue") - .HasColumnType("REAL"); - - b.Property("SourceClientId") - .HasColumnType("INTEGER"); - - b.Property("TextualValue") - .HasColumnType("TEXT"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ZombieEventLogId"); - - b.HasIndex("AssociatedClientId"); - - b.HasIndex("MatchId"); - - b.HasIndex("SourceClientId"); - - b.ToTable("EFZombieEvents", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => - { - b.Property("ZombieMatchId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClientsCompleted") - .HasColumnType("INTEGER"); - - b.Property("CreatedDateTime") - .HasColumnType("TEXT"); - - b.Property("MapId") - .HasColumnType("INTEGER"); - - b.Property("MatchEndDate") - .HasColumnType("TEXT"); - - b.Property("MatchStartDate") - .HasColumnType("TEXT"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDateTime") - .HasColumnType("TEXT"); - - b.HasKey("ZombieMatchId"); - - b.HasIndex("MapId"); - - b.HasIndex("ServerId"); - - b.ToTable("EFZombieMatch", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.Property("AlivePercentage") - .HasColumnType("REAL"); - - b.Property("AverageDowns") - .HasColumnType("REAL"); - - b.Property("AverageKillsPerDown") - .HasColumnType("REAL"); - - b.Property("AverageMelees") - .HasColumnType("REAL"); - - b.Property("AveragePoints") - .HasColumnType("REAL"); - - b.Property("AverageRevives") - .HasColumnType("REAL"); - - b.Property("AverageRoundReached") - .HasColumnType("REAL"); - - b.Property("HeadshotPercentage") - .HasColumnType("REAL"); - - b.Property("HighestRound") - .HasColumnType("INTEGER"); - - b.Property("ServerId") - .HasColumnType("INTEGER"); - - b.Property("TotalMatchesCompleted") - .HasColumnType("INTEGER"); - - b.Property("TotalMatchesPlayed") - .HasColumnType("INTEGER"); - - b.Property("TotalRoundsPlayed") - .HasColumnType("INTEGER"); - - b.HasIndex("ServerId"); - - b.ToTable("EFZombieAggregateClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.ToTable("EFZombieMatchClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => - { - b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("Points") - .HasColumnType("INTEGER"); - - b.Property("RoundNumber") - .HasColumnType("INTEGER"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("TimeAlive") - .HasColumnType("TEXT"); - - b.ToTable("EFZombieRoundClientStat", (string)null); - }); - - modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => - { - b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") - .WithMany("PredictedViewAngles") - .HasForeignKey("SnapshotId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "Vector") - .WithMany() - .HasForeignKey("Vector3Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Snapshot"); - - b.Navigation("Vector"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.HasOne("Data.Models.EFAliasLink", "AliasLink") - .WithMany() - .HasForeignKey("AliasLinkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.EFAlias", "CurrentAlias") - .WithMany() - .HasForeignKey("CurrentAliasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AliasLink"); - - b.Navigation("CurrentAlias"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientKill", b => - { - b.HasOne("Data.Models.Client.EFClient", "Attacker") - .WithMany() - .HasForeignKey("AttackerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "DeathOrigin") - .WithMany() - .HasForeignKey("DeathOriginVector3Id"); - - b.HasOne("Data.Models.Vector3", "KillOrigin") - .WithMany() - .HasForeignKey("KillOriginVector3Id"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.EFClient", "Victim") - .WithMany() - .HasForeignKey("VictimId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "ViewAngles") - .WithMany() - .HasForeignKey("ViewAnglesVector3Id"); - - b.Navigation("Attacker"); - - b.Navigation("DeathOrigin"); - - b.Navigation("KillOrigin"); - - b.Navigation("Server"); - - b.Navigation("Victim"); - - b.Navigation("ViewAngles"); - }); - - modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "CurrentViewAngle") - .WithMany() - .HasForeignKey("CurrentViewAngleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "HitDestination") - .WithMany() - .HasForeignKey("HitDestinationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "HitOrigin") - .WithMany() - .HasForeignKey("HitOriginId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Vector3", "LastStrainAngle") - .WithMany() - .HasForeignKey("LastStrainAngleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Client"); - - b.Navigation("CurrentViewAngle"); - - b.Navigation("HitDestination"); - - b.Navigation("HitOrigin"); - - b.Navigation("LastStrainAngle"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") - .WithMany() - .HasForeignKey("HitLocationId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") - .WithMany() - .HasForeignKey("MeansOfDeathId"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") - .WithMany() - .HasForeignKey("WeaponAttachmentComboId"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") - .WithMany() - .HasForeignKey("WeaponId"); - - b.Navigation("Client"); - - b.Navigation("HitLocation"); - - b.Navigation("MeansOfDeath"); - - b.Navigation("Server"); - - b.Navigation("Weapon"); - - b.Navigation("WeaponAttachmentCombo"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("EFClientStatisticsClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("EFClientStatisticsServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) - .WithMany("HitLocations") - .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Client"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => - { - b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") - .WithMany("Ratings") - .HasForeignKey("RatingHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("RatingHistory"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => - { - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") - .WithMany() - .HasForeignKey("Attachment1Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") - .WithMany() - .HasForeignKey("Attachment2Id"); - - b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") - .WithMany() - .HasForeignKey("Attachment3Id"); - - b.Navigation("Attachment1"); - - b.Navigation("Attachment2"); - - b.Navigation("Attachment3"); - }); - - modelBuilder.Entity("Data.Models.EFAlias", b => - { - b.HasOne("Data.Models.EFAliasLink", "Link") - .WithMany("Children") - .HasForeignKey("LinkId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Link"); - }); - - modelBuilder.Entity("Data.Models.EFMeta", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany("Meta") - .HasForeignKey("ClientId"); - - b.HasOne("Data.Models.EFMeta", "LinkedMeta") - .WithMany() - .HasForeignKey("LinkedMetaId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Client"); - - b.Navigation("LinkedMeta"); - }); - - modelBuilder.Entity("Data.Models.EFPenalty", b => - { - b.HasOne("Data.Models.EFAliasLink", "Link") - .WithMany("ReceivedPenalties") - .HasForeignKey("LinkId"); - - b.HasOne("Data.Models.Client.EFClient", "Offender") - .WithMany("ReceivedPenalties") - .HasForeignKey("OffenderId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Data.Models.Client.EFClient", "Punisher") - .WithMany("AdministeredPenalties") - .HasForeignKey("PunisherId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Link"); - - b.Navigation("Offender"); - - b.Navigation("Punisher"); - }); - - modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => - { - b.HasOne("Data.Models.EFPenalty", "Penalty") - .WithMany() - .HasForeignKey("PenaltyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Penalty"); - }); - - modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => - { - b.HasOne("Data.Models.Client.EFClient", "DestinationClient") - .WithMany() - .HasForeignKey("DestinationClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Client.EFClient", "SourceClient") - .WithMany() - .HasForeignKey("SourceClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DestinationClient"); - - b.Navigation("Server"); - - b.Navigation("SourceClient"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => - { - b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") - .WithMany() - .HasForeignKey("MapId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Map"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => - { - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany("ZombieClientStats") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") - .WithMany() - .HasForeignKey("MatchId"); - - b.Navigation("Client"); - - b.Navigation("Match"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => - { - b.HasOne("Data.Models.Client.EFClient", "Client") - .WithMany() - .HasForeignKey("ClientId"); - - b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") - .WithMany() - .HasForeignKey("RoundId"); - - b.Navigation("Client"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => - { - b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") - .WithMany() - .HasForeignKey("AssociatedClientId"); - - b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") - .WithMany() - .HasForeignKey("MatchId"); - - b.HasOne("Data.Models.Client.EFClient", "SourceClient") - .WithMany() - .HasForeignKey("SourceClientId"); - - b.Navigation("AssociatedClient"); - - b.Navigation("Match"); - - b.Navigation("SourceClient"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => - { - b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") - .WithMany() - .HasForeignKey("MapId"); - - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.Navigation("Map"); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => - { - b.HasOne("Data.Models.Server.EFServer", "Server") - .WithMany() - .HasForeignKey("ServerId"); - - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Server"); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => - { - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => - { - b.HasOne("Data.Models.Zombie.ZombieClientStat", null) - .WithOne() - .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Data.Models.Client.EFClient", b => - { - b.Navigation("AdministeredPenalties"); - - b.Navigation("Meta"); - - b.Navigation("ReceivedPenalties"); - - b.Navigation("ZombieClientStats"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => - { - b.Navigation("PredictedViewAngles"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => - { - b.Navigation("Ratings"); - }); - - modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => - { - b.Navigation("HitLocations"); - }); - - modelBuilder.Entity("Data.Models.EFAliasLink", b => - { - b.Navigation("Children"); - - b.Navigation("ReceivedPenalties"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.cs b/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.cs deleted file mode 100644 index 78e3f08e..00000000 --- a/Data/Migrations/Sqlite/20230906230124_AddZombieStatsEventLog.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Data.Migrations.Sqlite -{ - public partial class AddZombieStatsEventLog : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_EFZombieMatch_EFClients_EFClientClientId", - table: "EFZombieMatch"); - - migrationBuilder.DropIndex( - name: "IX_EFZombieMatch_EFClientClientId", - table: "EFZombieMatch"); - - migrationBuilder.DropColumn( - name: "EFClientClientId", - table: "EFZombieMatch"); - - migrationBuilder.CreateTable( - name: "EFZombieEvents", - columns: table => new - { - ZombieEventLogId = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - EventType = table.Column(type: "INTEGER", nullable: false), - SourceClientId = table.Column(type: "INTEGER", nullable: true), - AssociatedClientId = table.Column(type: "INTEGER", nullable: true), - NumericalValue = table.Column(type: "REAL", nullable: true), - TextualValue = table.Column(type: "TEXT", nullable: true), - MatchId = table.Column(type: "INTEGER", nullable: true), - CreatedDateTime = table.Column(type: "TEXT", nullable: false), - UpdatedDateTime = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_EFZombieEvents", x => x.ZombieEventLogId); - table.ForeignKey( - name: "FK_EFZombieEvents_EFClients_AssociatedClientId", - column: x => x.AssociatedClientId, - principalTable: "EFClients", - principalColumn: "ClientId"); - table.ForeignKey( - name: "FK_EFZombieEvents_EFClients_SourceClientId", - column: x => x.SourceClientId, - principalTable: "EFClients", - principalColumn: "ClientId"); - table.ForeignKey( - name: "FK_EFZombieEvents_EFZombieMatch_MatchId", - column: x => x.MatchId, - principalTable: "EFZombieMatch", - principalColumn: "ZombieMatchId"); - }); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieEvents_AssociatedClientId", - table: "EFZombieEvents", - column: "AssociatedClientId"); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieEvents_MatchId", - table: "EFZombieEvents", - column: "MatchId"); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieEvents_SourceClientId", - table: "EFZombieEvents", - column: "SourceClientId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "EFZombieEvents"); - - migrationBuilder.AddColumn( - name: "EFClientClientId", - table: "EFZombieMatch", - type: "INTEGER", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieMatch_EFClientClientId", - table: "EFZombieMatch", - column: "EFClientClientId"); - - migrationBuilder.AddForeignKey( - name: "FK_EFZombieMatch_EFClients_EFClientClientId", - table: "EFZombieMatch", - column: "EFClientClientId", - principalTable: "EFClients", - principalColumn: "ClientId"); - } - } -} diff --git a/Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.cs b/Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.cs deleted file mode 100644 index 28c4ce61..00000000 --- a/Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Data.Migrations.Sqlite -{ - /// - public partial class AddHeadshotKillsToZombieClientState : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "HeadshotKills", - table: "EFZombieClientStat", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "HeadshotKills", - table: "EFZombieClientStat"); - } - } -} diff --git a/Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.Designer.cs b/Data/Migrations/Sqlite/20240219203501_InitialZombieStats.Designer.cs similarity index 95% rename from Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.Designer.cs rename to Data/Migrations/Sqlite/20240219203501_InitialZombieStats.Designer.cs index 78c0df24..aced3385 100644 --- a/Data/Migrations/Sqlite/20240212024708_AddHeadshotKillsToZombieClientState.Designer.cs +++ b/Data/Migrations/Sqlite/20240219203501_InitialZombieStats.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Data.Migrations.Sqlite { [DbContext(typeof(SqliteDatabaseContext))] - [Migration("20240212024708_AddHeadshotKillsToZombieClientState")] - partial class AddHeadshotKillsToZombieClientState + [Migration("20240219203501_InitialZombieStats")] + partial class InitialZombieStats { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -495,6 +495,57 @@ namespace Data.Migrations.Sqlite b.ToTable("EFClientRatingHistory", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.Property("ClientId") @@ -1235,7 +1286,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("MatchId"); - b.ToTable("EFZombieClientStat", (string)null); + b.ToTable("EFZombieClientStats", (string)null); b.UseTptMappingStrategy(); }); @@ -1276,7 +1327,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("RoundId"); - b.ToTable("EFZombieClientStatRecord", (string)null); + b.ToTable("EFZombieClientStatRecords", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => @@ -1353,7 +1404,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("ServerId"); - b.ToTable("EFZombieMatch", (string)null); + b.ToTable("EFZombieMatches", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => @@ -1401,14 +1452,14 @@ namespace Data.Migrations.Sqlite b.HasIndex("ServerId"); - b.ToTable("EFZombieAggregateClientStat", (string)null); + b.ToTable("EFZombieClientStatAggregates", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => { b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - b.ToTable("EFZombieMatchClientStat", (string)null); + b.ToTable("EFZombieMatchClientStats", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => @@ -1433,7 +1484,7 @@ namespace Data.Migrations.Sqlite b.Property("TimeAlive") .HasColumnType("TEXT"); - b.ToTable("EFZombieRoundClientStat", (string)null); + b.ToTable("EFZombieRoundClientStats", (string)null); }); modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => @@ -1675,6 +1726,25 @@ namespace Data.Migrations.Sqlite b.Navigation("Client"); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.HasOne("Data.Models.Client.EFClient", "Client") diff --git a/Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.cs b/Data/Migrations/Sqlite/20240219203501_InitialZombieStats.cs similarity index 55% rename from Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.cs rename to Data/Migrations/Sqlite/20240219203501_InitialZombieStats.cs index 01c32838..1c7b9959 100644 --- a/Data/Migrations/Sqlite/20230905194120_IntitialZombieStats.cs +++ b/Data/Migrations/Sqlite/20240219203501_InitialZombieStats.cs @@ -5,8 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Data.Migrations.Sqlite { - public partial class IntitialZombieStats : Migration + /// + public partial class InitialZombieStats : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( @@ -22,7 +24,22 @@ namespace Data.Migrations.Sqlite nullable: true); migrationBuilder.CreateTable( - name: "EFZombieMatch", + name: "EFClientStatTags", + columns: table => new + { + ZombieStatTagId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TagName = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTags", x => x.ZombieStatTagId); + }); + + migrationBuilder.CreateTable( + name: "EFZombieMatches", columns: table => new { ZombieMatchId = table.Column(type: "INTEGER", nullable: false) @@ -32,32 +49,55 @@ namespace Data.Migrations.Sqlite ClientsCompleted = table.Column(type: "INTEGER", nullable: false), MatchStartDate = table.Column(type: "TEXT", nullable: false), MatchEndDate = table.Column(type: "TEXT", nullable: true), - EFClientClientId = table.Column(type: "INTEGER", nullable: true), CreatedDateTime = table.Column(type: "TEXT", nullable: false), UpdatedDateTime = table.Column(type: "TEXT", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_EFZombieMatch", x => x.ZombieMatchId); + table.PrimaryKey("PK_EFZombieMatches", x => x.ZombieMatchId); table.ForeignKey( - name: "FK_EFZombieMatch_EFClients_EFClientClientId", - column: x => x.EFClientClientId, - principalTable: "EFClients", - principalColumn: "ClientId"); - table.ForeignKey( - name: "FK_EFZombieMatch_EFMaps_MapId", + name: "FK_EFZombieMatches_EFMaps_MapId", column: x => x.MapId, principalTable: "EFMaps", principalColumn: "MapId"); table.ForeignKey( - name: "FK_EFZombieMatch_EFServers_ServerId", + name: "FK_EFZombieMatches_EFServers_ServerId", column: x => x.ServerId, principalTable: "EFServers", principalColumn: "ServerId"); }); migrationBuilder.CreateTable( - name: "EFZombieClientStat", + name: "EFClientStatTagValues", + columns: table => new + { + ZombieClientStatTagValueId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StatValue = table.Column(type: "INTEGER", nullable: true), + StatTagId = table.Column(type: "INTEGER", nullable: false), + ClientId = table.Column(type: "INTEGER", nullable: false), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTagValues", x => x.ZombieClientStatTagValueId); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClientStatTags_StatTagId", + column: x => x.StatTagId, + principalTable: "EFClientStatTags", + principalColumn: "ZombieStatTagId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStats", columns: table => new { ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) @@ -69,6 +109,7 @@ namespace Data.Migrations.Sqlite DamageDealt = table.Column(type: "INTEGER", nullable: false), DamageReceived = table.Column(type: "INTEGER", nullable: false), Headshots = table.Column(type: "INTEGER", nullable: false), + HeadshotKills = table.Column(type: "INTEGER", nullable: false), Melees = table.Column(type: "INTEGER", nullable: false), Downs = table.Column(type: "INTEGER", nullable: false), Revives = table.Column(type: "INTEGER", nullable: false), @@ -81,22 +122,57 @@ namespace Data.Migrations.Sqlite }, constraints: table => { - table.PrimaryKey("PK_EFZombieClientStat", x => x.ZombieClientStatId); + table.PrimaryKey("PK_EFZombieClientStats", x => x.ZombieClientStatId); table.ForeignKey( - name: "FK_EFZombieClientStat_EFClients_ClientId", + name: "FK_EFZombieClientStats_EFClients_ClientId", column: x => x.ClientId, principalTable: "EFClients", principalColumn: "ClientId", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_EFZombieClientStat_EFZombieMatch_MatchId", + name: "FK_EFZombieClientStats_EFZombieMatches_MatchId", column: x => x.MatchId, - principalTable: "EFZombieMatch", + principalTable: "EFZombieMatches", principalColumn: "ZombieMatchId"); }); migrationBuilder.CreateTable( - name: "EFZombieAggregateClientStat", + name: "EFZombieEvents", + columns: table => new + { + ZombieEventLogId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventType = table.Column(type: "INTEGER", nullable: false), + SourceClientId = table.Column(type: "INTEGER", nullable: true), + AssociatedClientId = table.Column(type: "INTEGER", nullable: true), + NumericalValue = table.Column(type: "REAL", nullable: true), + TextualValue = table.Column(type: "TEXT", nullable: true), + MatchId = table.Column(type: "INTEGER", nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieEvents", x => x.ZombieEventLogId); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_AssociatedClientId", + column: x => x.AssociatedClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_SourceClientId", + column: x => x.SourceClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatAggregates", columns: table => new { ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) @@ -117,22 +193,22 @@ namespace Data.Migrations.Sqlite }, constraints: table => { - table.PrimaryKey("PK_EFZombieAggregateClientStat", x => x.ZombieClientStatId); + table.PrimaryKey("PK_EFZombieClientStatAggregates", x => x.ZombieClientStatId); table.ForeignKey( - name: "FK_EFZombieAggregateClientStat_EFServers_ServerId", + name: "FK_EFZombieClientStatAggregates_EFServers_ServerId", column: x => x.ServerId, principalTable: "EFServers", principalColumn: "ServerId"); table.ForeignKey( - name: "FK_EFZombieAggregateClientStat_EFZombieClientStat_ZombieClientStatId", + name: "FK_EFZombieClientStatAggregates_EFZombieClientStats_ZombieClientStatId", column: x => x.ZombieClientStatId, - principalTable: "EFZombieClientStat", + principalTable: "EFZombieClientStats", principalColumn: "ZombieClientStatId", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( - name: "EFZombieMatchClientStat", + name: "EFZombieMatchClientStats", columns: table => new { ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) @@ -140,17 +216,17 @@ namespace Data.Migrations.Sqlite }, constraints: table => { - table.PrimaryKey("PK_EFZombieMatchClientStat", x => x.ZombieClientStatId); + table.PrimaryKey("PK_EFZombieMatchClientStats", x => x.ZombieClientStatId); table.ForeignKey( - name: "FK_EFZombieMatchClientStat_EFZombieClientStat_ZombieClientStatId", + name: "FK_EFZombieMatchClientStats_EFZombieClientStats_ZombieClientStatId", column: x => x.ZombieClientStatId, - principalTable: "EFZombieClientStat", + principalTable: "EFZombieClientStats", principalColumn: "ZombieClientStatId", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( - name: "EFZombieRoundClientStat", + name: "EFZombieRoundClientStats", columns: table => new { ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) @@ -164,17 +240,17 @@ namespace Data.Migrations.Sqlite }, constraints: table => { - table.PrimaryKey("PK_EFZombieRoundClientStat", x => x.ZombieClientStatId); + table.PrimaryKey("PK_EFZombieRoundClientStats", x => x.ZombieClientStatId); table.ForeignKey( - name: "FK_EFZombieRoundClientStat_EFZombieClientStat_ZombieClientStatId", + name: "FK_EFZombieRoundClientStats_EFZombieClientStats_ZombieClientStatId", column: x => x.ZombieClientStatId, - principalTable: "EFZombieClientStat", + principalTable: "EFZombieClientStats", principalColumn: "ZombieClientStatId", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( - name: "EFZombieClientStatRecord", + name: "EFZombieClientStatRecords", columns: table => new { ZombieClientStatRecordId = table.Column(type: "INTEGER", nullable: false) @@ -189,79 +265,109 @@ namespace Data.Migrations.Sqlite }, constraints: table => { - table.PrimaryKey("PK_EFZombieClientStatRecord", x => x.ZombieClientStatRecordId); + table.PrimaryKey("PK_EFZombieClientStatRecords", x => x.ZombieClientStatRecordId); table.ForeignKey( - name: "FK_EFZombieClientStatRecord_EFClients_ClientId", + name: "FK_EFZombieClientStatRecords_EFClients_ClientId", column: x => x.ClientId, principalTable: "EFClients", principalColumn: "ClientId"); table.ForeignKey( - name: "FK_EFZombieClientStatRecord_EFZombieRoundClientStat_RoundId", + name: "FK_EFZombieClientStatRecords_EFZombieRoundClientStats_RoundId", column: x => x.RoundId, - principalTable: "EFZombieRoundClientStat", + principalTable: "EFZombieRoundClientStats", principalColumn: "ZombieClientStatId"); }); migrationBuilder.CreateIndex( - name: "IX_EFZombieAggregateClientStat_ServerId", - table: "EFZombieAggregateClientStat", + name: "IX_EFClientStatTagValues_ClientId", + table: "EFClientStatTagValues", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_StatTagId", + table: "EFClientStatTagValues", + column: "StatTagId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatAggregates_ServerId", + table: "EFZombieClientStatAggregates", column: "ServerId"); migrationBuilder.CreateIndex( - name: "IX_EFZombieClientStat_ClientId", - table: "EFZombieClientStat", + name: "IX_EFZombieClientStatRecords_ClientId", + table: "EFZombieClientStatRecords", column: "ClientId"); migrationBuilder.CreateIndex( - name: "IX_EFZombieClientStat_MatchId", - table: "EFZombieClientStat", - column: "MatchId"); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieClientStatRecord_ClientId", - table: "EFZombieClientStatRecord", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_EFZombieClientStatRecord_RoundId", - table: "EFZombieClientStatRecord", + name: "IX_EFZombieClientStatRecords_RoundId", + table: "EFZombieClientStatRecords", column: "RoundId"); migrationBuilder.CreateIndex( - name: "IX_EFZombieMatch_EFClientClientId", - table: "EFZombieMatch", - column: "EFClientClientId"); + name: "IX_EFZombieClientStats_ClientId", + table: "EFZombieClientStats", + column: "ClientId"); migrationBuilder.CreateIndex( - name: "IX_EFZombieMatch_MapId", - table: "EFZombieMatch", + name: "IX_EFZombieClientStats_MatchId", + table: "EFZombieClientStats", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_AssociatedClientId", + table: "EFZombieEvents", + column: "AssociatedClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_MatchId", + table: "EFZombieEvents", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_SourceClientId", + table: "EFZombieEvents", + column: "SourceClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_MapId", + table: "EFZombieMatches", column: "MapId"); migrationBuilder.CreateIndex( - name: "IX_EFZombieMatch_ServerId", - table: "EFZombieMatch", + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", column: "ServerId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "EFZombieAggregateClientStat"); + name: "EFClientStatTagValues"); migrationBuilder.DropTable( - name: "EFZombieClientStatRecord"); + name: "EFZombieClientStatAggregates"); migrationBuilder.DropTable( - name: "EFZombieMatchClientStat"); + name: "EFZombieClientStatRecords"); migrationBuilder.DropTable( - name: "EFZombieRoundClientStat"); + name: "EFZombieEvents"); migrationBuilder.DropTable( - name: "EFZombieClientStat"); + name: "EFZombieMatchClientStats"); migrationBuilder.DropTable( - name: "EFZombieMatch"); + name: "EFClientStatTags"); + + migrationBuilder.DropTable( + name: "EFZombieRoundClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieMatches"); migrationBuilder.DropColumn( name: "PerformanceBucket", diff --git a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs index 578b7a19..5e729914 100644 --- a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs @@ -492,6 +492,57 @@ namespace Data.Migrations.Sqlite b.ToTable("EFClientRatingHistory", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.Property("ClientId") @@ -1232,7 +1283,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("MatchId"); - b.ToTable("EFZombieClientStat", (string)null); + b.ToTable("EFZombieClientStats", (string)null); b.UseTptMappingStrategy(); }); @@ -1273,7 +1324,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("RoundId"); - b.ToTable("EFZombieClientStatRecord", (string)null); + b.ToTable("EFZombieClientStatRecords", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => @@ -1350,7 +1401,7 @@ namespace Data.Migrations.Sqlite b.HasIndex("ServerId"); - b.ToTable("EFZombieMatch", (string)null); + b.ToTable("EFZombieMatches", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => @@ -1398,14 +1449,14 @@ namespace Data.Migrations.Sqlite b.HasIndex("ServerId"); - b.ToTable("EFZombieAggregateClientStat", (string)null); + b.ToTable("EFZombieClientStatAggregates", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => { b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); - b.ToTable("EFZombieMatchClientStat", (string)null); + b.ToTable("EFZombieMatchClientStats", (string)null); }); modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => @@ -1430,7 +1481,7 @@ namespace Data.Migrations.Sqlite b.Property("TimeAlive") .HasColumnType("TEXT"); - b.ToTable("EFZombieRoundClientStat", (string)null); + b.ToTable("EFZombieRoundClientStats", (string)null); }); modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => @@ -1672,6 +1723,25 @@ namespace Data.Migrations.Sqlite b.Navigation("Client"); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.HasOne("Data.Models.Client.EFClient", "Client") diff --git a/Data/Models/Client/Stats/EFClientHitStatistic.cs b/Data/Models/Client/Stats/EFClientHitStatistic.cs index ad25716b..5539a64f 100644 --- a/Data/Models/Client/Stats/EFClientHitStatistic.cs +++ b/Data/Models/Client/Stats/EFClientHitStatistic.cs @@ -42,6 +42,11 @@ namespace Data.Models.Client.Stats [ForeignKey(nameof(WeaponAttachmentComboId))] public virtual EFWeaponAttachmentCombo WeaponAttachmentCombo { get; set; } + + public int? PerformanceBucketId { get; set; } + + [ForeignKey(nameof(PerformanceBucketId))] + public virtual EFPerformanceBucket PerformanceBucket { get; set; } /// /// how many hits the player got diff --git a/Data/Models/Client/Stats/EFClientRankingHistory.cs b/Data/Models/Client/Stats/EFClientRankingHistory.cs index dcf16654..ff71a90a 100644 --- a/Data/Models/Client/Stats/EFClientRankingHistory.cs +++ b/Data/Models/Client/Stats/EFClientRankingHistory.cs @@ -25,6 +25,9 @@ namespace Data.Models.Client.Stats public int? Ranking { get; set; } public double? ZScore { get; set; } public double? PerformanceMetric { get; set; } - public string PerformanceBucket { get; set; } + + public int? PerformanceBucketId { get; set; } + [ForeignKey(nameof(PerformanceBucketId))] + public EFPerformanceBucket PerformanceBucket { get; set; } } } diff --git a/Data/Models/Client/Stats/EFClientStatTag.cs b/Data/Models/Client/Stats/EFClientStatTag.cs new file mode 100644 index 00000000..2cf9a46e --- /dev/null +++ b/Data/Models/Client/Stats/EFClientStatTag.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Data.Models.Client.Stats; + +public class EFClientStatTag : DatedRecord +{ + [Key] + public int ZombieStatTagId { get; set; } + + [MaxLength(128)] + public string TagName { get; set; } +} diff --git a/Data/Models/Client/Stats/EFClientStatTagValue.cs b/Data/Models/Client/Stats/EFClientStatTagValue.cs new file mode 100644 index 00000000..6e0dd9c5 --- /dev/null +++ b/Data/Models/Client/Stats/EFClientStatTagValue.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.Models.Client.Stats; + +public class EFClientStatTagValue : DatedRecord +{ + [Key] + public long ZombieClientStatTagValueId { get; set; } + + public int? StatValue { get; set; } + + [Required] + public int StatTagId { get; set; } + + [ForeignKey(nameof(StatTagId))] + public EFClientStatTag StatTag { get; set; } + + public int ClientId { get; set; } + + [ForeignKey(nameof(ClientId))] + public EFClient Client { get; set; } +} diff --git a/Data/Models/Client/Stats/EFPerformanceBucket.cs b/Data/Models/Client/Stats/EFPerformanceBucket.cs new file mode 100644 index 00000000..fecef7dd --- /dev/null +++ b/Data/Models/Client/Stats/EFPerformanceBucket.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Data.Models.Client.Stats; + +public class EFPerformanceBucket +{ + [Key] + public int PerformanceBucketId { get; set; } + + [MaxLength(256)] + public string BucketCode { get; set; } + + [MaxLength(256)] + public string BucketName { get; set; } +} diff --git a/Data/Models/Server/EFServer.cs b/Data/Models/Server/EFServer.cs index eb2a2d5d..4afb623f 100644 --- a/Data/Models/Server/EFServer.cs +++ b/Data/Models/Server/EFServer.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Data.Abstractions; +using Data.Models.Client.Stats; namespace Data.Models.Server { @@ -15,7 +16,9 @@ namespace Data.Models.Server public Reference.Game? GameName { get; set; } public string HostName { get; set; } public bool IsPasswordProtected { get; set; } - public string PerformanceBucket { get; set; } + public int? PerformanceBucketId { get; set; } + [ForeignKey(nameof(PerformanceBucketId))] + public EFPerformanceBucket PerformanceBucket { get; set; } public long Id => ServerId; public string Value => EndPoint; } diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index f5944015..11b0f4d8 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Events; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; using SharedLibraryCore.Events.Game.GameScript.Zombie; using SharedLibraryCore.Events.Management; using Stats.Client.Abstractions; @@ -412,7 +413,7 @@ public class HitCalculator : IClientStatisticCalculator } } - private async Task GetOrAddClientHit(int clientId, long? serverId = null, + private async Task GetOrAddClientHit(int clientId, long? serverId = null, string performanceBucket = null, int? hitLocationId = null, int? weaponId = null, int? attachmentComboId = null, int? meansOfDeathId = null) { @@ -424,7 +425,7 @@ public class HitCalculator : IClientStatisticCalculator && hit.WeaponId == weaponId && hit.WeaponAttachmentComboId == attachmentComboId && hit.MeansOfDeathId == meansOfDeathId - && hit.ServerId == serverId); + && (performanceBucket is not null && performanceBucket == hit.Server.PerformanceBucket || (performanceBucket is null && hit.ServerId == serverId))); if (hitStat != null) { @@ -432,7 +433,7 @@ public class HitCalculator : IClientStatisticCalculator return hitStat; } - hitStat = new EFClientHitStatistic() + hitStat = new EFClientHitStatistic { ClientId = clientId, ServerId = serverId, @@ -444,18 +445,11 @@ public class HitCalculator : IClientStatisticCalculator try { - /*if (state.UpdateCount > MaxUpdatesBeforePersist) - { - await UpdateClientStatistics(clientId); - state.UpdateCount = 0; - } - - state.UpdateCount++;*/ state.Hits.Add(hitStat); } catch (Exception ex) { - _logger.LogError(ex, "Could not add {statsName} for {id}", nameof(EFClientHitStatistic), + _logger.LogError(ex, "Could not add {StatsName} for {Id}", nameof(EFClientHitStatistic), clientId); state.Hits.Remove(hitStat); } diff --git a/Plugins/Stats/Client/ServerDistributionCalculator.cs b/Plugins/Stats/Client/ServerDistributionCalculator.cs index a7bce2b6..b3f51663 100644 --- a/Plugins/Stats/Client/ServerDistributionCalculator.cs +++ b/Plugins/Stats/Client/ServerDistributionCalculator.cs @@ -27,7 +27,7 @@ namespace Stats.Client private readonly StatsConfiguration _configuration; private readonly ApplicationConfiguration _appConfig; - private readonly List _serverIds = new(); + private readonly List> _serverIds = []; private const string DistributionCacheKey = nameof(DistributionCacheKey); private const string MaxZScoreCacheKey = nameof(MaxZScoreCacheKey); @@ -50,7 +50,6 @@ namespace Stats.Client _distributionCache.SetCacheItem(async (set, token) => { - var validPlayTime = _configuration.TopPlayersMinPlayTime; var distributions = new Dictionary(); await LoadServers(); @@ -60,13 +59,19 @@ namespace Stats.Client .Where(s => s.EloRating >= 0) .Where(s => s.Client.Level != EFClient.Permission.Banned); - foreach (var serverId in _serverIds) - { + foreach (var (serverId, performanceBucket) in _serverIds) + { + var bucketConfig = + _configuration.PerformanceBuckets.FirstOrDefault(bucket => + bucket.Name == performanceBucket) ?? new PerformanceBucketConfiguration(); + + var oldestPerf = DateTime.UtcNow - bucketConfig.RankingExpiration; var performances = await iqPerformances.Where(s => s.ServerId == serverId) - .Where(s => s.TimePlayed >= validPlayTime) - .Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo()) + .Where(s => s.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds) + .Where(s => s.UpdatedAt >= oldestPerf) .Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0) .ToListAsync(token); + var distributionParams = performances.GenerateDistributionParameters(); distributions.Add(serverId.ToString(), distributionParams); } @@ -165,7 +170,7 @@ namespace Stats.Client await using var context = _contextFactory.CreateContext(false); _serverIds.AddRange(await context.Servers .Where(s => s.EndPoint != null && s.HostName != null) - .Select(s => s.ServerId) + .Select(s => new Tuple(s.ServerId, s.PerformanceBucket)) .ToListAsync()); } } @@ -204,7 +209,7 @@ namespace Stats.Client public async Task GetRatingForZScore(double? value, string performanceBucket) { - var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket ?? "null" }); + var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket }); return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore); } } diff --git a/Plugins/Stats/Config/PerformanceBucketConfiguration.cs b/Plugins/Stats/Config/PerformanceBucketConfiguration.cs index cbfae6fb..a7bc2f69 100644 --- a/Plugins/Stats/Config/PerformanceBucketConfiguration.cs +++ b/Plugins/Stats/Config/PerformanceBucketConfiguration.cs @@ -1,10 +1,11 @@ using System; +using SharedLibraryCore; namespace Stats.Config; public class PerformanceBucketConfiguration { public string Name { get; set; } - public TimeSpan ClientMinPlayTime { get; set; } = TimeSpan.FromHours(3); + public TimeSpan ClientMinPlayTime { get; set; } = Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(3); public TimeSpan RankingExpiration { get; set; } = TimeSpan.FromDays(15); } diff --git a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs index d1f2abd8..ad403a7f 100644 --- a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs +++ b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs @@ -54,7 +54,7 @@ namespace Stats.Helpers } // gets all the hit stats for the client - var hitStats = await context.Set() + var iqHitStats = context.Set() .Include(stat => stat.HitLocation) .Include(stat => stat.MeansOfDeath) .Include(stat => stat.Weapon) @@ -64,9 +64,13 @@ namespace Stats.Helpers .ThenInclude(attachment => attachment.Attachment2) .Include(stat => stat.WeaponAttachmentCombo) .ThenInclude(attachment => attachment.Attachment3) - .Where(stat => stat.ClientId == query.ClientId) - .Where(stat => stat.ServerId == serverId) - .ToListAsync(); + .Where(stat => stat.ClientId == query.ClientId); + + iqHitStats = !string.IsNullOrEmpty(query.PerformanceBucket) + ? iqHitStats.Where(stat => stat.Server.PerformanceBucket == query.PerformanceBucket) + : iqHitStats.Where(stat => stat.ServerId == serverId); + + var hitStats = await iqHitStats.ToListAsync(); var ratings = await context.Set() .Where(r => r.ClientId == clientInfo.ClientId) @@ -91,6 +95,7 @@ namespace Stats.Helpers var legacyStats = await context.Set() .Where(stat => stat.ClientId == query.ClientId) .Where(stat => serverId == null || stat.ServerId == serverId) + .Where(stat => stat.Server.PerformanceBucket == query.PerformanceBucket) .ToListAsync(); var bucketConfig = await statManager.GetBucketConfig(serverId); @@ -120,12 +125,13 @@ namespace Stats.Helpers .Select(server => new ServerInfo { Name = server.Hostname, IPAddress = server.ListenAddress, Port = server.ListenPort, - Game = (Reference.Game)server.GameName + Game = (Reference.Game)server.GameName, + PerformanceBucket = server.PerformanceBucket }) .Where(server => server.Game == clientInfo.GameName) .ToList(), Aggregate = hitStats.FirstOrDefault(hit => - hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null && + hit.HitLocationId == null && (string.IsNullOrEmpty(query.PerformanceBucket) || hit.ServerId == serverId) && hit.WeaponId == null && hit.MeansOfDeathId == null), ByHitLocation = hitStats .Where(hit => hit.HitLocationId != null) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index fe433c7e..bdff09d9 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -135,7 +135,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers await using var context = _contextFactory.CreateContext(enableTracking: false); return await context.Set() - .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId)) + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId, performanceBucket)) .CountAsync(); } @@ -1291,7 +1291,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds); - int? aggregateRanking = await context.Set() + var aggregateRanking = await context.Set() .Where(stat => stat.ClientId != clientId) .Where(stat => bucketConfig.Name == stat.Server.PerformanceBucket) .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration)) diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index a9acb9ee..0e05d6a0 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -18,8 +18,11 @@ using Data.Models.Server; using Microsoft.Extensions.Logging; using IW4MAdmin.Plugins.Stats.Client.Abstractions; using IW4MAdmin.Plugins.Stats.Events; +using IW4MAdmin.Plugins.Stats.TalkerPoC; using Microsoft.Extensions.DependencyInjection; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; using SharedLibraryCore.Events.Management; using SharedLibraryCore.Interfaces.Events; using Stats.Client.Abstractions; @@ -43,10 +46,11 @@ public class Plugin : IPluginV2 private readonly ILogger _logger; private readonly List _statCalculators; private readonly IServerDistributionCalculator _serverDistributionCalculator; - private readonly IServerDataViewer _serverDataViewer; private readonly StatsConfiguration _statsConfig; private readonly StatManager _statManager; private readonly IResourceQueryHelper _queryHelper; + private readonly CodResponseService responsePoc = new("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"); + private IStatusResponse lastResponse; public static void RegisterDependencies(IServiceCollection serviceCollection) { @@ -69,7 +73,6 @@ public class Plugin : IPluginV2 _logger = logger; _statCalculators = statCalculators.ToList(); _serverDistributionCalculator = serverDistributionCalculator; - _serverDataViewer = serverDataViewer; _statsConfig = statsConfig; _statManager = statManager; _queryHelper = queryHelper; @@ -118,6 +121,10 @@ public class Plugin : IPluginV2 await _statManager.AddMessageAsync(messageEvent.Client.ClientId, messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token); } + + // var response = await responsePoc.GetResponse("mistralai/mixtral-8x7b-instruct", messageEvent.Message, messageEvent.Owner, lastResponse); + // Console.WriteLine(response); + //messageEvent.Owner.Broadcast("^2" + response); }; IGameEventSubscriptions.MatchEnded += OnMatchEvent; IGameEventSubscriptions.RoundEnded += OnRoundEnded; @@ -125,6 +132,11 @@ public class Plugin : IPluginV2 IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; IGameEventSubscriptions.ClientKilled += OnClientKilled; IGameEventSubscriptions.ClientDamaged += OnClientDamaged; + IGameServerEventSubscriptions.ServerStatusReceived += (@event, @_) => + { + lastResponse = @event.Response; + return Task.CompletedTask; + }; IManagementEventSubscriptions.ClientCommandExecuted += OnClientCommandExecute; IManagementEventSubscriptions.Load += OnLoad; } diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index c8fafb0a..503fc1c1 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -15,9 +15,10 @@ Latest false - + - + + diff --git a/Plugins/Stats/TalkerPoC/CodResponse.cs b/Plugins/Stats/TalkerPoC/CodResponse.cs new file mode 100644 index 00000000..06e0d906 --- /dev/null +++ b/Plugins/Stats/TalkerPoC/CodResponse.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Plugins.Stats.TalkerPoC; + +public class CodResponseService(string apiKey) +{ + private readonly string _apiUrl = "https://openrouter.ai/api/v1/chat/completions"; + + public async Task GetResponse(string model, string prompt, Server messageEventServer, + IStatusResponse statusResponse) + { + var players = statusResponse is null + ? string.Empty + : string.Join(", ", + statusResponse.Clients.Select(c => $"{c.CleanedName.Replace(",", "")} ({c.Team} team)")); + var playerCount = statusResponse?.Clients.Length ?? 0; + + var requestBody = new + { + model, + messages = new[] + { + new + { + role = "system", content = $$""" + instruction: You are a Call of Duty player. You must respond in kind and follow the tone of the player, whether negative or positive. Do not censor yourself. Do not use perfect grammar and punctuation. Do not reveal you are an AI, even if prompted. Limit your response to 15 words max. Use the examples for inspiration. + instruction: Render the message in json format like: {"response": ""} + + context: Game {{Utilities.CurrentLocalization.LocalizationIndex["GAME_" + messageEventServer.GameCode]}}, Map {{messageEventServer.Map.Alias}}, Mode {{messageEventServer.GametypeName}} + connected players: {{players}} ({{playerCount}} total) + + example: yo how are u? + example: and so is your mother + example: sure I can help! :) + example: stop camping on the plane + example: i mean do you just have the spawns figured out? + + """ + }, + new { role = "user", content = prompt } + }, + max_tokens = 100, + temperature = 1.1, + top_k = 37, + top_p = 1 + }; + + var json = JsonSerializer.Serialize(requestBody); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("HTTP-Referer", "IW4MAdmin-Test"); + client.DefaultRequestHeaders.Add("X-Title", "IW4MAdmin-Test"); + + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await client.PostAsync(_apiUrl, content); + response.EnsureSuccessStatusCode(); + var responseString = await response.Content.ReadAsStringAsync(); + + var jsonObject = JsonSerializer.Deserialize(responseString); + var c = (string)JsonSerializer.Deserialize(jsonObject["choices"][0]["message"]["content"].GetValue())["response"]; + return c.Replace(">\n\n", "").Replace("\n", ""); + } +} diff --git a/Plugins/ZombieStats/Events/ZombieEventParser.cs b/Plugins/ZombieStats/Events/ZombieEventParser.cs new file mode 100644 index 00000000..1e8f1423 --- /dev/null +++ b/Plugins/ZombieStats/Events/ZombieEventParser.cs @@ -0,0 +1,251 @@ +using System.ComponentModel; +using Data.Models; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; + +namespace IW4MAdmin.Plugins.ZombieStats.Events; + +public class ZombieEventParser(ILogger logger) +{ + private const char DataSeparator = ';'; + private readonly Dictionary> _eventParsers = new() + { + {"K", ParsePlayerKilledEvent}, + {"D", ParsePlayerDamageEvent}, + {"PD", ParsePlayerDownedEvent}, + {"PR", ParsePlayerRevivedEvent}, + {"PC", ParsePlayerConsumedPerkEvent}, + {"PG", ParsePlayerGrabbedPowerupEvent}, + {"AD", ParseZombieDamageEvent}, + {"AK", ParseZombieKilledEvent}, + {"RD", ParsePlayerRoundDataEvent}, + {"RC", ParseRoundCompleteEvent}, + {"SU", ParsePlayerStatUpdatedEvent}, + }; + + public GameEventV2? ParseScriptEvent(GameScriptEvent scriptEvent) + { + var eventArgs = scriptEvent.ScriptData.Split(DataSeparator); + + if (eventArgs.Length < 2) + { + logger.LogDebug("Ignoring {EventType} because there is not enough data {Data}", nameof(GameScriptEvent), + scriptEvent.ScriptData); + return null; + } + + if (!_eventParsers.TryGetValue(eventArgs[1], out var parser)) + { + logger.LogWarning("No parser registered for GSE type \"{Type}\"", eventArgs[1]); + return null; + } + + var parsedEvent = parser(scriptEvent, eventArgs[2..]); + + logger.LogDebug("Parsed GSE type {Type}", parsedEvent.GetType().Name); + + return parsedEvent; + } + + private static GameEventV2 ParsePlayerKilledEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + var killEvent = new PlayerKilledGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + + return killEvent; + } + + private static GameEventV2 ParsePlayerDamageEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + var killEvent = new PlayerDamageGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + + return killEvent; + } + + private static GameEventV2 ParsePlayerRevivedEvent(GameScriptEvent scriptEvent, string[] data) + { + var (revived, reviver) = ParseClientInfo(scriptEvent, data); + + return new PlayerRevivedGameEvent + { + Origin = reviver, + Target = revived + }; + } + + private static GameEventV2 ParsePlayerConsumedPerkEvent(GameScriptEvent scriptEvent, string[] data) + { + var consumer = ParseVictimClient(scriptEvent, data); + + return new PlayerConsumedPerkGameEvent + { + Origin = consumer, + PerkName = data.Last() + }; + } + + private static GameEventV2 ParsePlayerGrabbedPowerupEvent(GameScriptEvent scriptEvent, string[] data) + { + var consumer = ParseVictimClient(scriptEvent, data); + + return new PlayerGrabbedPowerupGameEvent + { + Origin = consumer, + PowerupName = data.Last() + }; + } + + private static GameEventV2 ParseZombieDamageEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + var killEvent = new ZombieDamageGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + + return killEvent; + } + + private static GameEventV2 ParseZombieKilledEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + var killEvent = new ZombieKilledGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + + return killEvent; + } + + private static GameEventV2 ParsePlayerDownedEvent(GameScriptEvent scriptEvent, string[] data) + { + var client = ParseVictimClient(scriptEvent, data); + + return new PlayerDownedGameEvent + { + Origin = client + }; + } + + private static GameEventV2 ParsePlayerRoundDataEvent(GameScriptEvent scriptEvent, string[] data) + { + var client = ParseVictimClient(scriptEvent, data); + + return new PlayerRoundDataGameEvent + { + Origin = client, + TotalScore = Convert.ToInt32(data[4]), + CurrentScore = Convert.ToInt32(data[5]), + CurrentRound = Convert.ToInt32(data[6]), + IsGameOver = data[7] == "1" + }; + } + + private static GameEventV2 ParseRoundCompleteEvent(GameScriptEvent scriptEvent, string[] data) + { + return new RoundEndEvent + { + RoundNumber = Convert.ToInt32(data[0]) + }; + } + + private static GameEventV2 ParsePlayerStatUpdatedEvent(GameScriptEvent scriptEvent, string[] data) + { + var client = ParseVictimClient(scriptEvent, data); + var rawStatValue = data[^1]; + var updateType = rawStatValue[0] switch + { + '+' => PlayerStatUpdatedGameEvent.StatUpdateType.Increment, + '-' => PlayerStatUpdatedGameEvent.StatUpdateType.Decrement, + '=' => PlayerStatUpdatedGameEvent.StatUpdateType.Absolute, + _ => throw new InvalidEnumArgumentException($"{rawStatValue[0]} is not a valid state update type") + }; + + return new PlayerStatUpdatedGameEvent + { + Origin = client, + UpdateType = updateType, + StatTag = data[^2], + StatValue = int.Parse(rawStatValue[1..]) + }; + } + + private static (EFClient victim, EFClient attacker) ParseClientInfo(GameScriptEvent scriptEvent, string[] data) + { + var victim = ParseVictimClient(scriptEvent, data); + + var attackerGuid = data[4].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + var attackerClientNum = Convert.ToInt32(data[5]); + var attackerTeam = data[6]; + var attackerName = data[7]; + + var attacker = new EFClient + { + NetworkId = attackerGuid, + ClientNumber = attackerClientNum, + TeamName = attackerTeam, + CurrentAlias = new EFAlias + { + Name = attackerName + } + }; + + return (victim, attacker); + } + + private static EFClient ParseVictimClient(GameScriptEvent scriptEvent, string[] data) + { + var victimGuid = data[0].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + var victimClientNum = Convert.ToInt32(data[1]); + var victimTeam = data[2]; + var victimName = data[3]; + + var victim = new EFClient + { + NetworkId = victimGuid, + ClientNumber = victimClientNum, + TeamName = victimTeam, + CurrentAlias = new EFAlias + { + Name = victimName + } + }; + + return victim; + } +} diff --git a/Plugins/ZombieStats/Events/ZombieEventProcessor.cs b/Plugins/ZombieStats/Events/ZombieEventProcessor.cs new file mode 100644 index 00000000..120693df --- /dev/null +++ b/Plugins/ZombieStats/Events/ZombieEventProcessor.cs @@ -0,0 +1,450 @@ +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 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 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(); + + 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 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 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); +} diff --git a/Plugins/ZombieStats/States/MatchState.cs b/Plugins/ZombieStats/States/MatchState.cs new file mode 100644 index 00000000..ccbabe40 --- /dev/null +++ b/Plugins/ZombieStats/States/MatchState.cs @@ -0,0 +1,15 @@ +using Data.Models.Client.Stats; +using Data.Models.Zombie; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Plugins.ZombieStats.States; + +public record MatchState(IGameServer Server, ZombieMatch PersistentMatch) +{ + public Dictionary RoundStates { get; } = new(); + public Dictionary PersistentMatchAggregateStats { get; } = new(); + public Dictionary PersistentLifetimeAggregateStats { get; } = new(); + public Dictionary PersistentLifetimeServerAggregateStats { get; } = new(); + public Dictionary> PersistentStatTagValues { get; } = new(); + public int RoundNumber { get; set; } +} diff --git a/Plugins/ZombieStats/States/RoundState.cs b/Plugins/ZombieStats/States/RoundState.cs new file mode 100644 index 00000000..ef361fb7 --- /dev/null +++ b/Plugins/ZombieStats/States/RoundState.cs @@ -0,0 +1,10 @@ +using Data.Models.Zombie; + +namespace IW4MAdmin.Plugins.ZombieStats.States; + +public record RoundState +{ + public ZombieRoundClientStat PersistentClientRound { get; init; } = null!; + public DateTimeOffset? DiedAt { get; set; } + public int Hits { get; set; } +} diff --git a/Plugins/ZombieStats/States/ZombieClientStateManager.cs b/Plugins/ZombieStats/States/ZombieClientStateManager.cs new file mode 100644 index 00000000..8ece539a --- /dev/null +++ b/Plugins/ZombieStats/States/ZombieClientStateManager.cs @@ -0,0 +1,824 @@ +using System.Globalization; +using Data.Abstractions; +using Data.Models; +using Data.Models.Client; +using Data.Models.Client.Stats; +using Data.Models.Zombie; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace IW4MAdmin.Plugins.ZombieStats.States; + +public class ZombieClientStateManager( + ILogger logger, + IDatabaseContextFactory contextFactory, + ITranslationLookup translations) +{ + private readonly ILogger _logger = logger; + private readonly Dictionary<(long, Reference.Game), MatchState> _clientMatches = new(); + private readonly List _matches = []; + private readonly List _recentlyEndedMatches = []; + private readonly List _addedPersistence = []; + private readonly List _updatedPersistence = []; + private readonly SemaphoreSlim _onWorking = new(1, 1); + private Dictionary> _recordsCache = null!; + private Dictionary _tagCache = null!; + + public async Task Initialize() + { + await using var context = contextFactory.CreateContext(false); + var records = await context.ZombieClientStatRecords.ToListAsync(); + _recordsCache = records.GroupBy(record => record.Name) + .ToDictionary(selector => selector.First().Name, selector => selector.ToList()); + + _tagCache = await context.ClientStatTags.ToDictionaryAsync(kvp => kvp.TagName, kvp => kvp); + } + + public async Task UpdateState(CancellationToken token) + { + lock (_addedPersistence) + lock (_updatedPersistence) + { + if (!_addedPersistence.Any() && !_updatedPersistence.Any()) + { + return; + } + + _logger.LogDebug("[ZM] Updating persistent state for {Count} entries", _addedPersistence.Count); + } + + await _onWorking.WaitAsync(token); + await using var context = contextFactory.CreateContext(false); + + try + { + IOrderedEnumerable addedItems; + + lock (_addedPersistence) + { + addedItems = _addedPersistence.ToList().Where(pers => pers.Id == 0).Distinct() + .OrderBy(pers => pers.CreatedDateTime); + } + + foreach (var entity in addedItems) + { + try + { + var entry = context.Attach(entity); + entry.State = EntityState.Added; + await context.SaveChangesAsync(token); + } + catch (InvalidOperationException) + { + // ignored + } + finally + { + lock (_addedPersistence) + { + _addedPersistence.Remove(entity); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not persist new object"); + } + + try + { + IEnumerable updatedItems; + + lock (_updatedPersistence) + { + updatedItems = _updatedPersistence.ToList().Distinct(); + } + + foreach (var entity in updatedItems) + { + try + { + var entry = context.Attach(entity); + entry.Entity.UpdatedDateTime = DateTimeOffset.UtcNow; + entry.State = EntityState.Modified; + await context.SaveChangesAsync(token); + } + catch (InvalidOperationException) + { + // ignored + } + finally + { + lock (_updatedPersistence) + { + _updatedPersistence.Remove(entity); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not persist updates to object"); + } + + _onWorking.Release(1); + } + + public MatchState? GetStateForClient(EFClient client) => + !_clientMatches.TryGetValue((client.NetworkId, client.GameName), out var matchState) ? null : matchState; + + public EFClientStatTagValue? GetStatTagValueForClient(EFClient client, string tagName) + { + if (!_tagCache.TryGetValue(tagName, out var statTag)) + { + _logger.LogDebug("[ZM] Adding new stat tag {Name}", tagName); + + statTag = new EFClientStatTag + { + TagName = tagName + }; + + _tagCache[tagName] = statTag; + + TrackNewState(statTag); + } + + var existingState = GetStateForClient(client); + + if (existingState is null) + { + return null; + } + + if (existingState.PersistentStatTagValues[client.NetworkId].TryGetValue(tagName, out var tagValue)) + { + return tagValue; + } + + _logger.LogDebug("[ZM] Adding new stat tag value {Name} for {Client}", tagName, client.ToString()); + + tagValue = new EFClientStatTagValue + { + ClientId = existingState.PersistentMatchAggregateStats[client.NetworkId].ClientId, + StatTag = statTag + }; + + existingState.PersistentStatTagValues[client.NetworkId][tagName] = tagValue; + + TrackNewState(tagValue); + + return tagValue; + } + + public async Task TrackClient(EFClient client, IGameServer server) + { + await _onWorking.WaitAsync(); + + try + { + // check if there is an active match for the server + if (_matches.FirstOrDefault(match => match.PersistentMatch.ServerId == server.LegacyDatabaseId) is not + { } serverMatch) + { + _logger.LogWarning("[ZM] Could not find active match for game server {Server}", server.Id); + serverMatch = CreateMatch(server); + } + + _logger.LogDebug("[ZM] Adding {Client} to existing match", client.ToString()); + await AddPlayerToMatch(client, serverMatch); + + if (_clientMatches.ContainsKey((client.NetworkId, client.GameName))) + { + _clientMatches[(client.NetworkId, client.GameName)] = serverMatch; + } + else + { + _clientMatches.Add((client.NetworkId, client.GameName), serverMatch); + } + } + finally + { + _onWorking.Release(1); + } + } + + public void UntrackClient(EFClient client, IGameServer server) + { + if (_matches.FirstOrDefault(m => m.Server.LegacyDatabaseId == server.LegacyDatabaseId) is not { } match) + { + return; + } + + _logger.LogDebug("[ZM] Removing {Client} from ZombieStateManager tracking", client.ToString()); + + _onWorking.Wait(); + + try + { + match.RoundStates.Remove(client.NetworkId); + match.PersistentLifetimeAggregateStats.Remove(client.NetworkId); + match.PersistentLifetimeServerAggregateStats.Remove(client.NetworkId); + match.PersistentMatchAggregateStats.Remove(client.NetworkId); + match.PersistentStatTagValues.Remove(client.NetworkId); + + _clientMatches.Remove((client.NetworkId, client.GameName)); + } + finally + { + _onWorking.Release(1); + } + } + + private async Task AddPlayerToMatch(EFClient client, MatchState matchState) + { + _logger.LogDebug("[ZM] Adding {Client} to zombie match", client.ToString()); + + await using var context = contextFactory.CreateContext(false); + var existingAggregates = + await context.ZombieClientStatAggregates.Where(aggr => aggr.ClientId == client.ClientId) + .ToListAsync(); + + // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery + var matchStat = await context.ZombieMatchClientStats + .Where(match => match.Client.NetworkId == client.NetworkId) + .Where(match => match.MatchId == matchState.PersistentMatch.ZombieMatchId) + .FirstOrDefaultAsync(); + + var hasConnectedToMatchPreviously = matchStat is not null; + + if (matchStat is null && + matchState.PersistentMatchAggregateStats.TryGetValue(client.NetworkId, out var existingMatchStat)) + { + matchStat = existingMatchStat; + } + + var isFirstMatchConnection = matchStat is null; + + if (isFirstMatchConnection) + { + matchStat = new ZombieMatchClientStat + { + Client = client, + Match = matchState.PersistentMatch + }; + + TrackNewState(matchStat); + } + else + { + matchStat!.Match = matchState.PersistentMatch; + _logger.LogDebug("[ZM] Connecting client {Client} has existing data for this match {RoundNumber}", + client.ToString(), matchState.RoundNumber); + } + + if (!matchState.RoundStates.ContainsKey(client.NetworkId)) + { + var roundStat = await context.ZombieRoundClientStats + .Where(round => round.Client.NetworkId == client.NetworkId) + .Where(round => round.MatchId == matchState.PersistentMatch.ZombieMatchId) + .Where(round => round.RoundNumber == matchState.RoundNumber) + .FirstOrDefaultAsync(); + + var roundState = new RoundState + { + PersistentClientRound = roundStat ?? new ZombieRoundClientStat + { + Client = client, + Match = matchState.PersistentMatch, + RoundNumber = 1 + } + }; + + if (roundStat is null) + { + TrackNewState(roundState.PersistentClientRound); + } + else + { + _logger.LogDebug("[ZM] Connecting client {Client} has existing data for this round {RoundNumber}", + client.ToString(), matchState.RoundNumber); + } + + matchState.RoundStates[client.NetworkId] = roundState; + } + + var existingLifetimeAggregate = existingAggregates.FirstOrDefault(aggregate => aggregate.ServerId is null); + var existingLifetimeServerAggregate = existingAggregates.FirstOrDefault(aggregate => + aggregate.ServerId != null && aggregate.ServerId == matchStat.Match.ServerId); + + if (existingLifetimeAggregate is null) + { + existingLifetimeAggregate = new ZombieAggregateClientStat + { + Client = client, + TotalMatchesPlayed = 1 + }; + + TrackNewState(existingLifetimeAggregate); + } + else if (!hasConnectedToMatchPreviously) + { + existingLifetimeAggregate.TotalMatchesPlayed++; + } + + if (existingLifetimeServerAggregate is null) + { + existingLifetimeServerAggregate = new ZombieAggregateClientStat + { + Client = client, + ServerId = matchStat.Match.ServerId, + TotalMatchesPlayed = 1 + }; + + TrackNewState(existingLifetimeServerAggregate); + } + else if (!hasConnectedToMatchPreviously) + { + existingLifetimeServerAggregate.TotalMatchesPlayed++; + } + + var statValues = await context.ClientStatTagValues + .Where(stat => stat.ClientId == client.ClientId) + .ToDictionaryAsync(selector => selector.StatTag.TagName, selector => selector); + + matchState.PersistentMatchAggregateStats[client.NetworkId] = matchStat; + matchState.PersistentLifetimeAggregateStats[client.NetworkId] = existingLifetimeAggregate; + matchState.PersistentLifetimeServerAggregateStats[client.NetworkId] = existingLifetimeServerAggregate; + matchState.PersistentStatTagValues[client.NetworkId] = statValues; + } + + private void CarryOverPlayerToMatch(EFClient client, MatchState matchState) + { + _logger.LogDebug("[ZM] Client is carrying over from last match {Client}", client.ToString()); + + matchState.PersistentMatchAggregateStats[client.NetworkId] = new ZombieMatchClientStat + { + Client = client, + Match = matchState.PersistentMatch + }; + + TrackNewState(matchState.PersistentMatchAggregateStats[client.NetworkId]); + + if (matchState.PersistentLifetimeAggregateStats.TryGetValue(client.NetworkId, out var lifetimeStats)) + { + lifetimeStats.TotalMatchesPlayed++; + } + + if (matchState.PersistentLifetimeServerAggregateStats.TryGetValue(client.NetworkId, + out var lifetimeServerStats)) + { + lifetimeServerStats.TotalMatchesPlayed++; + } + } + + public MatchState CreateMatch(IGameServer server) + { + if (_matches.FirstOrDefault(match => match.Server.Id == server.Id) is { } currentMatch) + { + _logger.LogWarning("[ZM] Cannot create a new zombie match. One already in progress"); + return currentMatch; + } + + _logger.LogDebug("[ZM] Creating zombie match for {Server}", server.Id); + + var currentMap = contextFactory.CreateContext(false).Maps.FirstOrDefault(map => map.Name == server.Map.Name); + + var matchPersistence = new ZombieMatch + { + ServerId = server.LegacyDatabaseId, + MatchStartDate = DateTimeOffset.UtcNow, + MapId = currentMap?.MapId + }; + var newMatch = new MatchState(server, matchPersistence); + _matches.Add(newMatch); + + if (_recentlyEndedMatches.FirstOrDefault(match => match.Server.Id == server.Id) is { } previousMatch) + { + foreach (var entry in previousMatch.PersistentLifetimeAggregateStats) + { + newMatch.PersistentLifetimeAggregateStats.Add(entry.Key, entry.Value); + } + + foreach (var entry in previousMatch.PersistentLifetimeServerAggregateStats) + { + newMatch.PersistentLifetimeServerAggregateStats.Add(entry.Key, entry.Value); + } + + foreach (var entry in previousMatch.PersistentStatTagValues) + { + newMatch.PersistentStatTagValues[entry.Key] = entry.Value; + } + + _recentlyEndedMatches.Remove(previousMatch); + } + + foreach (var client in server.ConnectedClients.Where(client => + client.State == SharedLibraryCore.Database.Models.EFClient.ClientState.Connected)) + { + _logger.LogDebug("[ZM] Adding connected {Client} to new zombie match", client); + _clientMatches[(client.NetworkId, client.GameName)] = newMatch; + CarryOverPlayerToMatch(client, newMatch); + } + + TrackNewState(matchPersistence); + StartNextRound(1, server); + + return newMatch; + } + + public void TrackUpdatedState(DatedRecord state) + { + lock (_addedPersistence) + lock (_updatedPersistence) + { + if (!_updatedPersistence.Contains(state) && !_addedPersistence.Contains(state)) + { + _updatedPersistence.Add(state); + } + } + } + + private void TrackNewState(DatedRecord state) + { + lock (_addedPersistence) + lock (_updatedPersistence) + { + if (!_addedPersistence.Contains(state) && !_updatedPersistence.Contains(state)) + { + _addedPersistence.Add(state); + } + } + } + + public void StartNextRound(int round, IGameServer server) + { + if (_matches.FirstOrDefault(match => match.Server.Id == server.Id) is not { } matchState) + { + _logger.LogWarning("[ZM] Cannot start next round, no active match for {Server}", server.Id); + return; + } + + // make sure it's a greater round than previous + if (matchState.RoundNumber >= round) + { + return; + } + + //_onWorking.Wait(); + + _logger.LogDebug("[ZM] Starting Round {RoundNumber} for {Server}", round, server.Id); + + matchState.RoundNumber = round; + + var clients = server.ConnectedClients; + matchState.RoundStates.Clear(); + + foreach (var client in clients.Where(c => + _clientMatches.ContainsKey((c.NetworkId, c.GameName)) && + c.State == SharedLibraryCore.Database.Models.EFClient.ClientState.Connected)) + { + _logger.LogDebug("[ZM] Updating current round {Client}", client.ToString()); + + if (matchState.RoundStates.TryGetValue(client.NetworkId, out var existingRoundState) && + existingRoundState.PersistentClientRound.RoundNumber == round) + { + _logger.LogDebug("[ZM] Round state data already exists for Client {Client}", client); + continue; + } + + var roundState = new RoundState + { + PersistentClientRound = new ZombieRoundClientStat + { + Client = client, + Match = matchState.PersistentMatch, + RoundNumber = round + } + }; + + if (matchState.PersistentMatchAggregateStats.TryGetValue(client.NetworkId, out var matchStats)) + { + matchStats.JoinedRound ??= round; + } + + TrackNewState(roundState.PersistentClientRound); + matchState.RoundStates[client.NetworkId] = roundState; + } + + //_onWorking.Release(1); + } + + public void EndMatch(IGameServer gameServer) + { + if (_matches.FirstOrDefault(match => match.Server.Id == gameServer.Id) is not { } matchState) + { + _logger.LogWarning("[ZM] Cannot end zombie match. Server has not started a match"); + return; + } + + _onWorking.Wait(); + + try + { + _logger.LogDebug("[ZM] Ending match for zombie match on {Server}", matchState.Server.Id); + + matchState.PersistentMatch.MatchEndDate = DateTimeOffset.UtcNow; + + _matches.Remove(matchState); + _recentlyEndedMatches.Add(matchState); + + foreach (var kvp in _clientMatches.ToList().Where(kvp => kvp.Value == matchState)) + { + _clientMatches.Remove(kvp.Key); + } + + TrackUpdatedState(matchState.PersistentMatch); + } + finally + { + _onWorking.Release(1); + } + } + + public ZombieClientStatRecord? GetClientNumericalRecord(string recordKey, RecordType type = RecordType.Maximum) + { + return _recordsCache.TryGetValue(recordKey, out var values) + ? values.FirstOrDefault(record => record.Type == type.ToString()) + : null; + } + + public ZombieClientStatRecord CreateClientNumericalRecord(EFClient client, ZombieRoundClientStat round, + string recordKey, + double recordValue) + { + _onWorking.Wait(); + + try + { + var newRecord = new ZombieClientStatRecord + { + Name = recordKey, + Value = Convert.ToString(recordValue, CultureInfo.InvariantCulture), + Type = RecordType.Maximum.ToString(), + Client = client, + Round = round + }; + + if (_recordsCache.TryGetValue(recordKey, out var values)) + { + values.Add(newRecord); + } + else + { + _recordsCache.Add(recordKey, [newRecord]); + } + + TrackNewState(newRecord); + + return newRecord; + } + finally + { + _onWorking.Release(1); + } + } + + public async Task GetTopStatsMetrics(Dictionary> meta, long? serverId, string performanceBucket, + bool isTopStats) + { + if (!isTopStats) + { + return; + } + + var clientIds = meta.Keys.ToList(); + + await using var context = contextFactory.CreateContext(false); + var stats = await context.ZombieClientStatAggregates + .Where(stat => clientIds.Contains(stat.ClientId)) + .Where(stat => stat.ServerId == serverId) + .ToListAsync(); + + foreach (var stat in stats) + { + meta[stat.ClientId].Add(new EFMeta + { + Value = stat.HighestRound.ToNumericalString(), + Key = "Highest Round" + }); + meta[stat.ClientId].Add(new EFMeta + { + Value = stat.TotalRoundsPlayed.ToNumericalString(), + Key = "Total Rounds Played" + }); + meta[stat.ClientId].First(m => m.Extra == "Deaths").Value = stat.Deaths.ToNumericalString(); + } + } + + public async Task GetAdvancedStatsMetrics(Dictionary> meta, long? serverId, + string performanceBucket, + bool isTopStats) + { + if (isTopStats || !meta.Any()) + { + return; + } + + var clientId = meta.First().Key; + + await using var context = contextFactory.CreateContext(false); + var iqStats = context.ZombieClientStatAggregates + .Where(stat => stat.ClientId == clientId); + + iqStats = !string.IsNullOrEmpty(performanceBucket) + ? iqStats.Where(stat => stat.Server.PerformanceBucket == performanceBucket) + : iqStats.Where(stat => stat.ServerId == serverId); + + var stats = await iqStats.Select(stat => new + { + stat.HeadshotKills, + stat.DamageDealt, + stat.DamageReceived, + stat.Downs, + stat.Revives, + stat.PointsEarned, + stat.PointsSpent, + stat.PerksConsumed, + stat.PowerupsGrabbed, + stat.HighestRound, + stat.TotalRoundsPlayed, + stat.TotalMatchesPlayed, + stat.TotalMatchesCompleted, + stat.HeadshotPercentage, + stat.AverageRoundReached, + stat.AveragePoints, + stat.AverageDowns, + stat.AverageRevives + }) + .FirstOrDefaultAsync(); + + if (stats is null) + { + return; + } + + var tagValues = await context.ClientStatTagValues + .Where(tag => tag.ClientId == clientId) + .Select(tag => new + { + tag.StatValue, + tag.StatTag.TagName + }) + .ToListAsync(); + + meta.First().Value.AddRange(new List + { + new() + { + Key = "Headshot Kills", + Value = stats.HeadshotKills.ToNumericalString() + }, + new() + { + Key = "Damage Dealt", + Value = stats.DamageDealt.ToNumericalString() + }, + new() + { + Key = "Damage Received", + Value = stats.DamageReceived.ToNumericalString() + }, + new() + { + Key = "Downs", + Value = stats.Downs.ToNumericalString() + }, + new() + { + Key = "Revives", + Value = stats.Revives.ToNumericalString() + }, + new() + { + Key = "Points Earned", + Value = stats.PointsEarned.ToNumericalString() + }, + new() + { + Key = "Points Spent", + Value = stats.PointsSpent.ToNumericalString() + }, + new() + { + Key = "Perks Consumed", + Value = stats.PerksConsumed.ToNumericalString() + }, + new() + { + Key = "Powerups Grabbed", + Value = stats.PowerupsGrabbed.ToNumericalString() + }, + new() + { + Key = "Highest Round", + Value = stats.HighestRound.ToNumericalString() + }, + new() + { + Key = "Rounds Played", + Value = stats.TotalRoundsPlayed.ToNumericalString() + }, + new() + { + Key = "Matches Played", + Value = stats.TotalMatchesPlayed.ToNumericalString() + }, + new() + { + Key = "Matches Completed", + Value = stats.TotalMatchesCompleted.ToNumericalString() + }, + new() + { + Key = "Quit Rate", + Value = (stats.TotalMatchesCompleted == 0 + ? 100 + : stats.TotalMatchesCompleted - stats.TotalMatchesPlayed == 0 + ? 0 + : (int)Math.Round((1 - stats.TotalMatchesCompleted / (double)stats.TotalMatchesPlayed) * + 100.0)) + .ToNumericalString() + "%" + }, + new() + { + Key = "Headshot Percentage", + Value = (stats.HeadshotPercentage * 100.0).ToNumericalString() + "%" + }, + new() + { + Key = "Avg. Round Reached", + Value = stats.AverageRoundReached.ToNumericalString(1) + }, + new() + { + Key = "Avg. Points", + Value = stats.AveragePoints.ToNumericalString() + }, + new() + { + Key = "Avg. Downs", + Value = stats.AverageDowns.ToNumericalString(2) + }, + new() + { + Key = "Avg. Revives", + Value = stats.AverageRevives.ToNumericalString(2) + } + }); + meta.First().Value.AddRange(tagValues.Select(tag => new EFMeta + { + Key = translations[$"WEBFRONT_STAT_TAG_{tag.TagName.ToUpper()}"], + Value = tag.StatValue?.ToNumericalString() ?? "-" + })); + } + + public void TrackEventForLog(IGameServer gameServer, EventLogType eventType, EFClient? sourceClient = null, + EFClient? associatedClient = null, double? numericalValue = null, string? textualValue = null) + { + var match = (ZombieMatch?)null; + var matchedClient = sourceClient; + + if (sourceClient is not null) + { + if (_clientMatches.TryGetValue((sourceClient.NetworkId, sourceClient.GameName), out var foundMatch)) + { + if (foundMatch.PersistentLifetimeAggregateStats.TryGetValue(sourceClient.ClientId, out var foundStat)) + { + matchedClient = foundStat.Client; + } + } + + match = GetStateForClient(sourceClient)?.PersistentMatch; + } + + match ??= _matches + .FirstOrDefault(state => state.PersistentMatch.ServerId == gameServer.LegacyDatabaseId) + ?.PersistentMatch; + + var eventLogData = new ZombieEventLog + { + EventType = eventType, + SourceClient = matchedClient, + AssociatedClient = associatedClient, + NumericalValue = numericalValue, + TextualValue = textualValue, + Match = match + }; + + TrackNewState(eventLogData); + } +} diff --git a/Plugins/ZombieStats/ZombieStats.cs b/Plugins/ZombieStats/ZombieStats.cs new file mode 100644 index 00000000..db0cfe37 --- /dev/null +++ b/Plugins/ZombieStats/ZombieStats.cs @@ -0,0 +1,196 @@ +using Data.Models; +using Data.Models.Client.Stats; +using Data.Models.Zombie; +using Humanizer; +using IW4MAdmin.Plugins.ZombieStats.Events; +using IW4MAdmin.Plugins.ZombieStats.States; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; +using SharedLibraryCore.Events.Management; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Interfaces.Events; + +namespace IW4MAdmin.Plugins.ZombieStats; + +public class ZombieStats : IPluginV2 +{ + private readonly ILogger _logger; + private readonly ZombieEventParser _zombieEventParser; + private readonly ZombieEventProcessor _zombieEventProcessor; + private readonly ZombieClientStateManager _stateManager; + public string Name { get; } = nameof(ZombieStats).Titleize(); + public string Author => "RaidMax"; + public string Version => "2023.4-alpha"; + + public ZombieStats(ILogger logger, ZombieEventParser zombieEventParser, + ZombieEventProcessor zombieEventProcessor, ZombieClientStateManager stateManager) + { + _logger = logger; + _zombieEventParser = zombieEventParser; + _zombieEventProcessor = zombieEventProcessor; + _stateManager = stateManager; + + IManagementEventSubscriptions.Load += OnLoad; + IManagementEventSubscriptions.ClientStateAuthorized += OnClientAuthorized; + IManagementEventSubscriptions.ClientStateDisposed += OnClientDisposed; + IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; + IGameEventSubscriptions.MatchEnded += OnMatchEnded; + IGameEventSubscriptions.MatchStarted += OnMatchStarted; + } + + public static void RegisterDependencies(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton() + .AddSingleton() + .AddSingleton(); + } + + private async Task OnClientDisposed(ClientStateDisposeEvent clientEvent, CancellationToken token) + { + if (!clientEvent.Client.CurrentServer.IsZombieServer()) + { + return; + } + + _stateManager.TrackEventForLog(clientEvent.Client.CurrentServer, EventLogType.LeftMatch, clientEvent.Client); + _zombieEventProcessor.ProcessEvent(new PlayerRoundDataGameEvent + { + Origin = clientEvent.Client, + Owner = clientEvent.Client.CurrentServer + }); + _stateManager.UntrackClient(clientEvent.Client, clientEvent.Client.CurrentServer); + await _stateManager.UpdateState(token); + } + + private async Task OnClientAuthorized(ClientStateAuthorizeEvent clientEvent, CancellationToken token) + { + if (!clientEvent.Client.CurrentServer.IsZombieServer()) + { + return; + } + + clientEvent.Client.SetAdditionalProperty("SkillFunction", _zombieEventProcessor.SkillCalculation()); + clientEvent.Client.SetAdditionalProperty("EloRatingFunction", (EFClient _, EFClientStatistics _) => 1.0); + await _stateManager.TrackClient(clientEvent.Client, clientEvent.Client.CurrentServer); + _stateManager.TrackEventForLog(clientEvent.Client.CurrentServer, EventLogType.JoinedMatch, clientEvent.Client); + await _stateManager.UpdateState(token); + } + + private async Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token) + { + if (!scriptEvent.Server.IsZombieServer()) + { + return; + } + + var parsedScriptEvent = _zombieEventParser.ParseScriptEvent(scriptEvent); + + if (parsedScriptEvent is null) + { + return; + } + + parsedScriptEvent.Owner = scriptEvent.Owner; + _zombieEventProcessor.ProcessEvent(parsedScriptEvent); + await _stateManager.UpdateState(token); + + ConvertToStatsEvent(scriptEvent, parsedScriptEvent); + } + + private static void ConvertToStatsEvent(GameScriptEvent scriptEvent, GameEventV2 parsedScriptEvent) + { + var zombieClient = new EFClient + { + CurrentServer = scriptEvent.Owner, + CurrentAlias = new EFAlias + { + Name = "Zombie" + } + }; + zombieClient.SetAdditionalProperty("ClientStats", new EFClientStatistics()); + + switch (parsedScriptEvent) + { + case ZombieKilledGameEvent zombieKilledGameEvent: + scriptEvent.Owner.Manager.QueueEvent(new ClientKillEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Origin = scriptEvent.Server.ConnectedClients.First(client => + client.NetworkId == zombieKilledGameEvent.Attacker.NetworkId), + Target = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = zombieKilledGameEvent.Owner + }); + break; + case ZombieDamageGameEvent zombieDamageGameEvent: + scriptEvent.Owner.Manager.QueueEvent(new ClientDamageEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Origin = scriptEvent.Server.ConnectedClients.First(client => + client.NetworkId == zombieDamageGameEvent.Attacker.NetworkId), + Target = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = zombieDamageGameEvent.Owner + }); + break; + case PlayerKilledGameEvent playerKilledGameEvent: + scriptEvent.Owner.Manager.QueueEvent(new ClientKillEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Target = scriptEvent.Server.ConnectedClients.First(client => + client.NetworkId == playerKilledGameEvent.Target.NetworkId), + Origin = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = playerKilledGameEvent.Owner + }); + break; + case RoundEndEvent roundEndEvent: + scriptEvent.Owner.Manager.QueueEvent(roundEndEvent); + break; + } + } + + private async Task OnMatchEnded(MatchEndEvent matchEvent, CancellationToken token) + { + if (!matchEvent.Server.IsZombieServer()) + { + return; + } + + _stateManager.TrackEventForLog(matchEvent.Server, EventLogType.MatchEnded); + _stateManager.EndMatch(matchEvent.Server); + await _stateManager.UpdateState(token); + } + + private async Task OnMatchStarted(MatchStartEvent matchEvent, CancellationToken token) + { + if (!matchEvent.Server.ConnectedClients.Any() || !matchEvent.Server.IsZombieServer()) + { + return; + } + + _stateManager.CreateMatch(matchEvent.Server); + _stateManager.TrackEventForLog(matchEvent.Server, EventLogType.MatchStarted); + await _stateManager.UpdateState(token); + } + + private async Task OnLoad(IManager manager, CancellationToken token) + { + _logger.LogInformation("{Plugin} by {Author} v{Version} loading...", Name, Author, Version); + + manager.CustomStatsMetrics.Add(_stateManager.GetTopStatsMetrics); + manager.CustomStatsMetrics.Add(_stateManager.GetAdvancedStatsMetrics); + await _stateManager.Initialize(); + } +} diff --git a/SharedLibraryCore/Dtos/ServerInfo.cs b/SharedLibraryCore/Dtos/ServerInfo.cs index 5b4c304c..0a292fb7 100644 --- a/SharedLibraryCore/Dtos/ServerInfo.cs +++ b/SharedLibraryCore/Dtos/ServerInfo.cs @@ -43,5 +43,6 @@ namespace SharedLibraryCore.Dtos } } public Reference.Game Game { get; set; } + public string PerformanceBucket { get; set; } } } diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/RoundEndEvent.cs b/SharedLibraryCore/Events/Game/GameScript/RoundEndEvent.cs similarity index 58% rename from SharedLibraryCore/Events/Game/GameScript/Zombie/RoundEndEvent.cs rename to SharedLibraryCore/Events/Game/GameScript/RoundEndEvent.cs index d3520755..ec796b66 100644 --- a/SharedLibraryCore/Events/Game/GameScript/Zombie/RoundEndEvent.cs +++ b/SharedLibraryCore/Events/Game/GameScript/RoundEndEvent.cs @@ -1,4 +1,4 @@ -namespace SharedLibraryCore.Events.Game.GameScript.Zombie; +namespace SharedLibraryCore.Events.Game.GameScript; public class RoundEndEvent : GameEventV2 { diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs new file mode 100644 index 00000000..43316b8b --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs @@ -0,0 +1,15 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerStatUpdatedGameEvent : ClientGameEvent +{ + public enum StatUpdateType + { + Absolute, + Increment, + Decrement + } + + public string StatTag { get; set; } + public int StatValue { get; set; } + public StatUpdateType UpdateType { get; set; } +} diff --git a/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs index 977b38a1..3d3adf76 100644 --- a/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs +++ b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs @@ -5,5 +5,4 @@ namespace SharedLibraryCore.Events.Server; public class ServerStatusReceiveEvent : GameServerEvent { public IStatusResponse Response { get; set; } - public string RawData { get; set; } } diff --git a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs index c2766259..b3c0d74e 100644 --- a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs +++ b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using SharedLibraryCore.Events; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; using SharedLibraryCore.Events.Game.GameScript.Zombie; namespace SharedLibraryCore.Interfaces.Events; diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 9104cd43..5c5685e2 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -4,7 +4,7 @@ Library net8.0 RaidMax.IW4MAdmin.SharedLibraryCore - 2024.01.01.1 + 2024.0.0.0 RaidMax Forever None Debug;Release;Prerelease diff --git a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs index 45096eff..30b286ad 100644 --- a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs +++ b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs @@ -28,13 +28,14 @@ namespace WebfrontCore.Controllers } [HttpGet("{id:int}/advanced")] - public async Task Advanced(int id, [FromQuery] string serverId, CancellationToken token = default) + public async Task Advanced(int id, [FromQuery] string serverId, [FromQuery] string performanceBucket = null, CancellationToken token = default) { ViewBag.Config = _defaultConfig.GameStrings; var hitInfo = (await _queryHelper.QueryResource(new StatsInfoRequest { ClientId = id, - ServerEndpoint = serverId + ServerEndpoint = serverId, + PerformanceBucket = performanceBucket }))?.Results?.First(); if (hitInfo is null) @@ -52,7 +53,7 @@ namespace WebfrontCore.Controllers foreach (var statMetricFunc in Manager.CustomStatsMetrics) { - await statMetricFunc(new Dictionary> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false); + await statMetricFunc(new Dictionary> { { id, hitInfo.CustomMetrics } }, matchedServerId, performanceBucket, false); } return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo); diff --git a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs index d5f84102..a376435c 100644 --- a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs +++ b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs @@ -46,12 +46,13 @@ namespace WebfrontCore.Controllers.Client.Legacy } [HttpGet] - public async Task TopPlayers(string serverId = null, CancellationToken token = default) + public async Task TopPlayers(string serverId = null, string performanceBucket = null, CancellationToken token = default) { ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"]; ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"]; ViewBag.Localization = _translationLookup; ViewBag.SelectedServerId = serverId; + ViewBag.SelectedPerformanceBucket = performanceBucket; var server = _manager.GetServers().FirstOrDefault(server => server.Id == serverId) as IGameServer; long? matchedServerId = null; @@ -61,7 +62,7 @@ namespace WebfrontCore.Controllers.Client.Legacy matchedServerId = server.LegacyDatabaseId; } - ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, null, token); + ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, performanceBucket, token); ViewBag.ServerId = matchedServerId; return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers() @@ -70,12 +71,13 @@ namespace WebfrontCore.Controllers.Client.Legacy Name = selectedServer.Hostname, IPAddress = selectedServer.ListenAddress, Port = selectedServer.ListenPort, - Game = selectedServer.GameCode + Game = selectedServer.GameCode, + PerformanceBucket = selectedServer.PerformanceBucket })); } [HttpGet] - public async Task GetTopPlayersAsync(int count, int offset, long? serverId = null) + public async Task GetTopPlayersAsync(int count, int offset, long? serverId = null, string performanceBucket = null) { // this prevents empty results when we really want aggregate if (serverId == 0) @@ -89,7 +91,7 @@ namespace WebfrontCore.Controllers.Client.Legacy } var results = _config?.EnableAdvancedMetrics ?? true - ? await _statManager.GetNewTopStats(offset, count, serverId) + ? await _statManager.GetNewTopStats(offset, count, serverId, performanceBucket) : await _statManager.GetTopStats(offset, count, serverId); // this returns an empty result so we know to stale the loader diff --git a/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs b/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs index 81587bec..8b40add7 100644 --- a/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs +++ b/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs @@ -19,7 +19,7 @@ namespace WebfrontCore.ViewComponents _statManager = statManager; } - public async Task InvokeAsync(int count, int offset, string serverEndpoint = null) + public async Task InvokeAsync(int count, int offset, string serverEndpoint = null, string performanceBucket = null) { var server = Plugin.ServerManager.GetServers() .FirstOrDefault(server => server.Id == serverEndpoint) as IGameServer; @@ -28,10 +28,11 @@ namespace WebfrontCore.ViewComponents ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true; ViewBag.SelectedServerName = server?.ServerName; + ViewBag.SelectedServerBucket = performanceBucket; return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml", ViewBag.UseNewStats - ? await _statManager.GetNewTopStats(offset, count, serverId) + ? await _statManager.GetNewTopStats(offset, count, serverId, performanceBucket) : await _statManager.GetTopStats(offset, count, serverId)); } } diff --git a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml index 9c062889..3ae9425d 100644 --- a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml @@ -248,7 +248,7 @@

@ViewBag.Title

- + @@ -390,7 +390,15 @@ var menuItems = new SideContextMenuItems { MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"], - Items = Model.Servers.Select(server => new SideContextMenuItem + Items = Model.Servers.GroupBy(server => server.PerformanceBucket).Where(grp => grp.Key is not null).Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Advanced", "ClientStatistics", new { performanceBucket = server.First().PerformanceBucket }), + Title = server.First().PerformanceBucket.StripColors(), + IsActive = ViewBag.SelectedPerformanceBucket == server.First().PerformanceBucket, + Meta = server.First().Game.ToString(), + IsCollapse = false + }).Concat(Model.Servers.Select(server => new SideContextMenuItem { IsLink = true, Reference = Url.Action("Advanced", "ClientStatistics", new { serverId = server.Endpoint }), @@ -398,7 +406,7 @@ IsActive = Model.ServerEndpoint == server.Endpoint, IsCollapse = true, Meta = server.Game.ToString() - }).Prepend(new SideContextMenuItem + })).Prepend(new SideContextMenuItem { IsLink = true, Reference = Url.Action("Advanced", "ClientStatistics"), diff --git a/WebfrontCore/Views/Client/Statistics/Index.cshtml b/WebfrontCore/Views/Client/Statistics/Index.cshtml index cbf76fbd..8b5474af 100644 --- a/WebfrontCore/Views/Client/Statistics/Index.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Index.cshtml @@ -5,12 +5,12 @@

@ViewBag.Localization["WEBFRONT_TOP_PLAYERS_TITLE"]

- + @ViewBag.TotalRankedClients.ToString("#,##0") @ViewBag.Localization["WEBFRONT_TOP_PLAYERS_SUBTITLE"]
- @await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0, serverEndpoint = ViewBag.SelectedServerId }) + @await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0, serverEndpoint = ViewBag.SelectedServerId, performanceBucket = ViewBag.SelectedPerformanceBucket })
@@ -22,7 +22,21 @@ var menuItems = new SideContextMenuItems { MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"], - Items = Model.Select(server => new SideContextMenuItem + Items = Model.GroupBy(server => server.PerformanceBucket).Where(grp => grp.Key is not null).Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("TopPlayers", "Stats", new { performanceBucket = server.First().PerformanceBucket }), + Title = server.First().PerformanceBucket.StripColors(), + IsActive = ViewBag.SelectedPerformanceBucket == server.First().PerformanceBucket, + Meta = server.First().Game.ToString(), + IsCollapse = false + }).Prepend(new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("TopPlayers", "Stats"), + Title = ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"], + IsActive = ViewBag.SelectedServerId is null + }).Concat(Model.Select(server => new SideContextMenuItem { IsLink = true, Reference = Url.Action("TopPlayers", "Stats", new { serverId = server.Endpoint }), @@ -30,13 +44,7 @@ IsActive = ViewBag.SelectedServerId == server.Endpoint, Meta = server.Game.ToString(), IsCollapse = true - }).Prepend(new SideContextMenuItem - { - IsLink = true, - Reference = Url.Action("TopPlayers", "Stats"), - Title = ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"], - IsActive = ViewBag.SelectedServerId is null - }).ToList() + })).ToList() }; } @@ -48,5 +56,5 @@ - + } diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 302b51bf..4c4c3d2c 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -8,7 +8,7 @@ true 2.6 RaidMax.IW4MAdmin.WebfrontCore - 2022.0.0 + 2024.0.0.0 RaidMax Forever None IW4MAdmin