mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-08 06:08:20 -05:00
zombie stats code
This commit is contained in:
parent
962abcf833
commit
79bd6ca8e1
@ -5,7 +5,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
||||
<Version>2020.0.0.0</Version>
|
||||
<Version>2024.0.0.0</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Product>IW4MAdmin</Product>
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -59,7 +59,7 @@ namespace IW4MAdmin.Application
|
||||
/// entrypoint of the application
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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) =>
|
||||
|
@ -38,6 +38,9 @@ namespace Data.Context
|
||||
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
|
||||
public DbSet<EFWeapon> Weapons { get; set; }
|
||||
public DbSet<EFWeaponAttachment> WeaponAttachments { get; set; }
|
||||
|
||||
public DbSet<EFClientStatTag> ClientStatTags { get; set; }
|
||||
public DbSet<EFClientStatTagValue> ClientStatTagValues { get; set; }
|
||||
public DbSet<EFMap> Maps { get; set; }
|
||||
|
||||
#endregion
|
||||
@ -58,7 +61,7 @@ namespace Data.Context
|
||||
public DbSet<ZombieAggregateClientStat> ZombieClientStatAggregates { get; set; }
|
||||
public DbSet<ZombieClientStatRecord> ZombieClientStatRecords { get; set; }
|
||||
public DbSet<ZombieEventLog> 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<EFPenalty>(entity =>
|
||||
@ -185,11 +175,11 @@ namespace Data.Context
|
||||
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
|
||||
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
|
||||
|
||||
modelBuilder.Entity<ZombieMatch>().ToTable($"EF{nameof(ZombieMatch)}");
|
||||
modelBuilder.Entity<ZombieMatch>().ToTable($"EF{nameof(ZombieMatches)}");
|
||||
|
||||
modelBuilder.Entity<ZombieClientStat>(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<ZombieMatchClientStat>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ZombieMatchClientStat)}");
|
||||
ent.ToTable($"EF{nameof(ZombieMatchClientStats)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ZombieRoundClientStat>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ZombieRoundClientStat)}");
|
||||
ent.ToTable($"EF{nameof(ZombieRoundClientStats)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ZombieAggregateClientStat>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ZombieAggregateClientStat)}");
|
||||
ent.ToTable($"EF{nameof(ZombieClientStatAggregates)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ZombieEventLog>().ToTable($"EF{nameof(ZombieEvents)}");
|
||||
|
||||
modelBuilder.Entity<ZombieClientStatRecord>().ToTable($"EF{nameof(ZombieClientStatRecord)}");
|
||||
modelBuilder.Entity<ZombieEventLog>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ZombieEvents)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ZombieClientStatRecord>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ZombieClientStatRecords)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EFClientStatTag>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ClientStatTags)}");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EFClientStatTagValue>(ent =>
|
||||
{
|
||||
ent.ToTable($"EF{nameof(ClientStatTagValues)}");
|
||||
});
|
||||
|
||||
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
EventType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SourceClientId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AssociatedClientId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
NumericalValue = table.Column<double>(type: "REAL", nullable: true),
|
||||
TextualValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MatchId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedDateTime = table.Column<DateTimeOffset>(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<int>(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHeadshotKillsToZombieClientState : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "HeadshotKills",
|
||||
table: "EFZombieClientStat",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HeadshotKills",
|
||||
table: "EFZombieClientStat");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("ZombieStatTagId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TagName")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UpdatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ZombieStatTagId");
|
||||
|
||||
b.ToTable("EFClientStatTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b =>
|
||||
{
|
||||
b.Property<long>("ZombieClientStatTagValueId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("StatTagId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StatValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("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<int>("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<TimeSpan?>("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")
|
@ -5,8 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class IntitialZombieStats : Migration
|
||||
/// <inheritdoc />
|
||||
public partial class InitialZombieStats : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@ -22,7 +24,22 @@ namespace Data.Migrations.Sqlite
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EFZombieMatch",
|
||||
name: "EFClientStatTags",
|
||||
columns: table => new
|
||||
{
|
||||
ZombieStatTagId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
TagName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
|
||||
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EFClientStatTags", x => x.ZombieStatTagId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EFZombieMatches",
|
||||
columns: table => new
|
||||
{
|
||||
ZombieMatchId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
@ -32,32 +49,55 @@ namespace Data.Migrations.Sqlite
|
||||
ClientsCompleted = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MatchStartDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
MatchEndDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedDateTime = table.Column<DateTimeOffset>(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<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
StatValue = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
StatTagId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedDateTime = table.Column<DateTimeOffset>(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<long>(type: "INTEGER", nullable: false)
|
||||
@ -69,6 +109,7 @@ namespace Data.Migrations.Sqlite
|
||||
DamageDealt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
DamageReceived = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Headshots = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
HeadshotKills = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Melees = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Downs = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Revives = table.Column<int>(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<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
EventType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SourceClientId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AssociatedClientId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
NumericalValue = table.Column<double>(type: "REAL", nullable: true),
|
||||
TextualValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MatchId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UpdatedDateTime = table.Column<DateTimeOffset>(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<long>(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<long>(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<long>(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<int>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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",
|
@ -492,6 +492,57 @@ namespace Data.Migrations.Sqlite
|
||||
b.ToTable("EFClientRatingHistory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b =>
|
||||
{
|
||||
b.Property<int>("ZombieStatTagId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TagName")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UpdatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ZombieStatTagId");
|
||||
|
||||
b.ToTable("EFClientStatTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b =>
|
||||
{
|
||||
b.Property<long>("ZombieClientStatTagValueId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("StatTagId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("StatValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("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<int>("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<TimeSpan?>("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")
|
||||
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// how many hits the player got
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
12
Data/Models/Client/Stats/EFClientStatTag.cs
Normal file
12
Data/Models/Client/Stats/EFClientStatTag.cs
Normal file
@ -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; }
|
||||
}
|
23
Data/Models/Client/Stats/EFClientStatTagValue.cs
Normal file
23
Data/Models/Client/Stats/EFClientStatTagValue.cs
Normal file
@ -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; }
|
||||
}
|
15
Data/Models/Client/Stats/EFPerformanceBucket.cs
Normal file
15
Data/Models/Client/Stats/EFPerformanceBucket.cs
Normal file
@ -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; }
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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<EFClientHitStatistic> GetOrAddClientHit(int clientId, long? serverId = null,
|
||||
private async Task<EFClientHitStatistic> 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);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace Stats.Client
|
||||
|
||||
private readonly StatsConfiguration _configuration;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly List<long> _serverIds = new();
|
||||
private readonly List<Tuple<long, string>> _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<string, Extensions.LogParams>();
|
||||
|
||||
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<long, string>(s.ServerId, s.PerformanceBucket))
|
||||
.ToListAsync());
|
||||
}
|
||||
}
|
||||
@ -204,7 +209,7 @@ namespace Stats.Client
|
||||
|
||||
public async Task<double?> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace Stats.Helpers
|
||||
}
|
||||
|
||||
// gets all the hit stats for the client
|
||||
var hitStats = await context.Set<EFClientHitStatistic>()
|
||||
var iqHitStats = context.Set<EFClientHitStatistic>()
|
||||
.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<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientInfo.ClientId)
|
||||
@ -91,6 +95,7 @@ namespace Stats.Helpers
|
||||
var legacyStats = await context.Set<EFClientStatistics>()
|
||||
.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)
|
||||
|
@ -135,7 +135,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
return await context.Set<EFClientRankingHistory>()
|
||||
.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<EFClientStatistics>()
|
||||
var aggregateRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => stat.ClientId != clientId)
|
||||
.Where(stat => bucketConfig.Name == stat.Server.PerformanceBucket)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
|
||||
|
@ -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<Plugin> _logger;
|
||||
private readonly List<IClientStatisticCalculator> _statCalculators;
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
private readonly IServerDataViewer _serverDataViewer;
|
||||
private readonly StatsConfiguration _statsConfig;
|
||||
private readonly StatManager _statManager;
|
||||
private readonly IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> _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;
|
||||
}
|
||||
|
@ -15,9 +15,10 @@
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
|
||||
<ProjectReference Include="..\..\Data\Data.csproj" />
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
73
Plugins/Stats/TalkerPoC/CodResponse.cs
Normal file
73
Plugins/Stats/TalkerPoC/CodResponse.cs
Normal file
@ -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<string> 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": "<responseHere>"}
|
||||
|
||||
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<JsonObject>(responseString);
|
||||
var c = (string)JsonSerializer.Deserialize<JsonObject>(jsonObject["choices"][0]["message"]["content"].GetValue<string>())["response"];
|
||||
return c.Replace("</SYS>>\n\n", "").Replace("\n", "");
|
||||
}
|
||||
}
|
251
Plugins/ZombieStats/Events/ZombieEventParser.cs
Normal file
251
Plugins/ZombieStats/Events/ZombieEventParser.cs
Normal file
@ -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<ZombieEventParser> logger)
|
||||
{
|
||||
private const char DataSeparator = ';';
|
||||
private readonly Dictionary<string, Func<GameScriptEvent, string[], GameEventV2>> _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;
|
||||
}
|
||||
}
|
450
Plugins/ZombieStats/Events/ZombieEventProcessor.cs
Normal file
450
Plugins/ZombieStats/Events/ZombieEventProcessor.cs
Normal file
@ -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<ZombieEventProcessor> logger, ZombieClientStateManager stateManager)
|
||||
{
|
||||
private const int RoundsConsidered = 200;
|
||||
|
||||
public void ProcessEvent(GameEventV2 gameEvent)
|
||||
{
|
||||
if (gameEvent.Origin is not null)
|
||||
{
|
||||
gameEvent.Origin.GameName = (Reference.Game)gameEvent.Owner.GameName;
|
||||
}
|
||||
|
||||
if (gameEvent.Target is not null)
|
||||
{
|
||||
gameEvent.Target.GameName = (Reference.Game)gameEvent.Owner.GameName;
|
||||
}
|
||||
|
||||
switch (gameEvent.GetType().Name)
|
||||
{
|
||||
case nameof(PlayerKilledGameEvent):
|
||||
OnPlayerKilled((PlayerKilledGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerDamageGameEvent):
|
||||
OnPlayerDamaged((PlayerDamageGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(ZombieKilledGameEvent):
|
||||
OnZombieKilled((ZombieKilledGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(ZombieDamageGameEvent):
|
||||
OnZombieDamaged((ZombieDamageGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerConsumedPerkGameEvent):
|
||||
OnPlayerConsumedPerk((PlayerConsumedPerkGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerGrabbedPowerupGameEvent):
|
||||
OnPlayerGrabbedPowerup((PlayerGrabbedPowerupGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerRevivedGameEvent):
|
||||
OnPlayerRevived((PlayerRevivedGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerDownedGameEvent):
|
||||
OnPlayerDowned((PlayerDownedGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerRoundDataGameEvent):
|
||||
OnPlayerRoundDataReceived((PlayerRoundDataGameEvent)gameEvent);
|
||||
break;
|
||||
case nameof(RoundEndEvent):
|
||||
OnRoundCompleted((RoundEndEvent)gameEvent);
|
||||
break;
|
||||
case nameof(PlayerStatUpdatedGameEvent):
|
||||
OnPlayerStatUpdated((PlayerStatUpdatedGameEvent)gameEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Func<EFClient, EFClientStatistics, double> SkillCalculation()
|
||||
{
|
||||
return (client, clientStats) =>
|
||||
{
|
||||
var state = stateManager.GetStateForClient(client);
|
||||
|
||||
if (state is null)
|
||||
{
|
||||
return clientStats.Skill;
|
||||
}
|
||||
|
||||
if (!state.PersistentLifetimeAggregateStats.TryGetValue(client.NetworkId, out var aggregateStats))
|
||||
{
|
||||
return clientStats.Skill;
|
||||
}
|
||||
|
||||
var currentClientRound = state.RoundStates[client.NetworkId];
|
||||
var normalizedValues = new List<double>();
|
||||
|
||||
foreach (var key in ZombieAggregateClientStat.RecordsKeys)
|
||||
{
|
||||
var clientValue =
|
||||
Convert.ToDouble(aggregateStats.GetType().GetProperty(key)!.GetValue(aggregateStats)?.ToString(),
|
||||
CultureInfo.InvariantCulture);
|
||||
var maxRecord = stateManager.GetClientNumericalRecord(key) ??
|
||||
stateManager.CreateClientNumericalRecord(
|
||||
client, currentClientRound.PersistentClientRound, key, clientValue);
|
||||
|
||||
var maxValue = Convert.ToDouble(maxRecord.Value, CultureInfo.InvariantCulture);
|
||||
|
||||
if (clientValue > maxValue)
|
||||
{
|
||||
maxRecord.Value = clientValue.ToString(CultureInfo.InvariantCulture);
|
||||
maxRecord.Client = client;
|
||||
maxRecord.Round = currentClientRound.PersistentClientRound;
|
||||
stateManager.TrackUpdatedState(maxRecord);
|
||||
}
|
||||
|
||||
if (!ZombieAggregateClientStat.SkillKeys.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedValue = clientValue / maxValue;
|
||||
|
||||
if (double.IsNaN(normalizedValue))
|
||||
{
|
||||
normalizedValue = 1;
|
||||
}
|
||||
|
||||
normalizedValues.Add(normalizedValue);
|
||||
}
|
||||
|
||||
var avg = normalizedValues.Any() ? normalizedValues.Average() : 0.0;
|
||||
avg *= 1000.0;
|
||||
|
||||
var roundWeightFactor = Math.Max(1, aggregateStats.TotalRoundsPlayed) <= RoundsConsidered
|
||||
? 1.0 / Math.Max(1, aggregateStats.TotalRoundsPlayed)
|
||||
: 2.0 / (RoundsConsidered + 1);
|
||||
|
||||
var average = CalculateAverage(clientStats.Skill, avg, roundWeightFactor);
|
||||
|
||||
if (double.IsInfinity(average))
|
||||
{
|
||||
average = 0;
|
||||
}
|
||||
|
||||
return average;
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPlayerKilled(PlayerKilledGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Victim, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.Deaths++;
|
||||
curr.PersistentClientRound.DamageReceived += gameEvent.Damage;
|
||||
curr.DiedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Died, curr.PersistentClientRound.Client,
|
||||
numericalValue: gameEvent.Damage);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlayerDamaged(PlayerDamageGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Victim, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.DamageReceived += gameEvent.Damage;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.DamageTaken, curr.PersistentClientRound.Client,
|
||||
numericalValue: gameEvent.Damage);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnZombieKilled(ZombieKilledGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Attacker, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.Kills++;
|
||||
curr.PersistentClientRound.DamageDealt += gameEvent.Damage;
|
||||
|
||||
if (gameEvent.HitLocation.StartsWith("head") || gameEvent.MeansOfDeath == "MOD_HEADSHOT")
|
||||
{
|
||||
curr.PersistentClientRound.Headshots++;
|
||||
curr.PersistentClientRound.HeadshotKills++;
|
||||
}
|
||||
|
||||
if (gameEvent.MeansOfDeath == "MOD_MELEE")
|
||||
{
|
||||
curr.PersistentClientRound.Melees++;
|
||||
}
|
||||
|
||||
curr.Hits++;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnZombieDamaged(ZombieDamageGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Attacker, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.DamageDealt += gameEvent.Damage;
|
||||
|
||||
if (gameEvent.HitLocation == "head" || gameEvent.MeansOfDeath == "MOD_HEADSHOT")
|
||||
{
|
||||
curr.PersistentClientRound.Headshots++;
|
||||
}
|
||||
|
||||
if (gameEvent.MeansOfDeath == "MOD_MELEE")
|
||||
{
|
||||
curr.PersistentClientRound.Melees++;
|
||||
}
|
||||
|
||||
curr.Hits++;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlayerConsumedPerk(PlayerConsumedPerkGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Client, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.PerksConsumed++;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.PerkConsumed, curr.PersistentClientRound.Client,
|
||||
textualValue: gameEvent.PerkName);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlayerGrabbedPowerup(PlayerGrabbedPowerupGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Client, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.PowerupsGrabbed++;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.PowerupGrabbed, curr.PersistentClientRound.Client,
|
||||
textualValue: gameEvent.PowerupName);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlayerRevived(PlayerRevivedGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Reviver, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.Revives++;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Revived, curr.PersistentClientRound.Client);
|
||||
|
||||
//_stateManager.TrackEventForLog(gameEvent, EventLogType.WasRevived, gameEvent.Revived,
|
||||
// gameEvent.Reviver);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPlayerDowned(PlayerDownedGameEvent gameEvent)
|
||||
{
|
||||
RunCalculation(gameEvent.Client, curr =>
|
||||
{
|
||||
curr.PersistentClientRound.Downs++;
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.Downed, curr.PersistentClientRound.Client);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnRoundCompleted(RoundEndEvent roundEndEvent)
|
||||
{
|
||||
stateManager.StartNextRound(roundEndEvent.RoundNumber, roundEndEvent.Owner);
|
||||
}
|
||||
|
||||
private void OnPlayerStatUpdated(PlayerStatUpdatedGameEvent gameEvent)
|
||||
{
|
||||
var tagValue = stateManager.GetStatTagValueForClient(gameEvent.Client, gameEvent.StatTag);
|
||||
|
||||
if (tagValue is null)
|
||||
{
|
||||
logger.LogWarning("[ZM] Cannot update stat value {Key} for {Client} because no entry exists",
|
||||
gameEvent.StatTag, gameEvent.Client.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
tagValue.StatValue ??= 0;
|
||||
|
||||
switch (gameEvent.UpdateType)
|
||||
{
|
||||
case PlayerStatUpdatedGameEvent.StatUpdateType.Absolute:
|
||||
tagValue.StatValue = gameEvent.StatValue;
|
||||
break;
|
||||
case PlayerStatUpdatedGameEvent.StatUpdateType.Increment:
|
||||
tagValue.StatValue += gameEvent.StatValue;
|
||||
break;
|
||||
case PlayerStatUpdatedGameEvent.StatUpdateType.Decrement:
|
||||
tagValue.StatValue -= gameEvent.StatValue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tagValue.ZombieClientStatTagValueId != 0)
|
||||
{
|
||||
stateManager.TrackUpdatedState(tagValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerRoundDataReceived(PlayerRoundDataGameEvent gameEvent)
|
||||
{
|
||||
RunAggregateCalculation(gameEvent.Client, (matchState, roundState, matchStat, lifetimeStat, lifetimeServerStat) =>
|
||||
{
|
||||
var currentScore = gameEvent.CurrentScore;
|
||||
var isForfeit = gameEvent is { CurrentScore: 0, TotalScore: 0 };
|
||||
|
||||
// in T4 on the last round the current score is set to total score, so we need to
|
||||
// undo that
|
||||
if (gameEvent.IsGameOver && !isForfeit)
|
||||
{
|
||||
var previousCumulativePoints = (int)matchStat.PointsEarned;
|
||||
var lastRoundPoints = gameEvent.TotalScore - previousCumulativePoints;
|
||||
currentScore = lastRoundPoints + (previousCumulativePoints - (int)matchStat.PointsSpent);
|
||||
}
|
||||
|
||||
roundState.PersistentClientRound.Points = currentScore;
|
||||
roundState.PersistentClientRound.EndTime = DateTimeOffset.UtcNow;
|
||||
roundState.PersistentClientRound.Duration =
|
||||
roundState.PersistentClientRound.EndTime - roundState.PersistentClientRound.StartTime;
|
||||
roundState.PersistentClientRound.TimeAlive =
|
||||
roundState.PersistentClientRound.EndTime.Value - roundState.PersistentClientRound.StartTime;
|
||||
|
||||
if (roundState.DiedAt is not null)
|
||||
{
|
||||
// subtract the time since they died from the total round time
|
||||
roundState.PersistentClientRound.TimeAlive =
|
||||
roundState.PersistentClientRound.TimeAlive.Value.Subtract(
|
||||
roundState.PersistentClientRound.EndTime.Value - roundState.DiedAt.Value);
|
||||
}
|
||||
|
||||
var earnedPoints = isForfeit ? 0 : gameEvent.TotalScore - matchStat.PointsEarned;
|
||||
var spentPoints = isForfeit ? 0 : Math.Abs(currentScore - earnedPoints - (matchStat.PointsEarned - matchStat.PointsSpent));
|
||||
|
||||
roundState.PersistentClientRound.PointsSpent = spentPoints;
|
||||
roundState.PersistentClientRound.PointsEarned = earnedPoints;
|
||||
|
||||
// add to the match aggregates
|
||||
foreach (var set in new ZombieClientStat[] { matchStat, lifetimeStat, lifetimeServerStat })
|
||||
{
|
||||
set.Kills += roundState.PersistentClientRound.Kills;
|
||||
set.Deaths += roundState.PersistentClientRound.Deaths;
|
||||
set.DamageDealt += roundState.PersistentClientRound.DamageDealt;
|
||||
set.DamageReceived += roundState.PersistentClientRound.DamageReceived;
|
||||
set.Headshots += roundState.PersistentClientRound.Headshots;
|
||||
set.HeadshotKills += roundState.PersistentClientRound.HeadshotKills;
|
||||
set.Melees += roundState.PersistentClientRound.Melees;
|
||||
set.Downs += roundState.PersistentClientRound.Downs;
|
||||
set.Revives += roundState.PersistentClientRound.Revives;
|
||||
set.PointsEarned += roundState.PersistentClientRound.PointsEarned;
|
||||
set.PointsSpent += roundState.PersistentClientRound.PointsSpent;
|
||||
set.PerksConsumed += roundState.PersistentClientRound.PerksConsumed;
|
||||
set.PowerupsGrabbed += roundState.PersistentClientRound.PowerupsGrabbed;
|
||||
}
|
||||
|
||||
#region maximums and averages
|
||||
|
||||
if (gameEvent.IsGameOver)
|
||||
{
|
||||
// make it easier to track how many players made it to the end
|
||||
matchState.PersistentMatch.ClientsCompleted++;
|
||||
lifetimeStat.TotalMatchesCompleted++;
|
||||
lifetimeServerStat.TotalMatchesCompleted++;
|
||||
}
|
||||
|
||||
CalculateAveragesAndTotals(matchState, lifetimeStat, roundState, matchStat);
|
||||
CalculateAveragesAndTotals(matchState, lifetimeServerStat, roundState, matchStat);
|
||||
|
||||
stateManager.TrackEventForLog(gameEvent.Server, EventLogType.RoundCompleted, matchStat.Client,
|
||||
numericalValue: gameEvent.CurrentRound);
|
||||
|
||||
#endregion
|
||||
});
|
||||
}
|
||||
|
||||
private static void CalculateAveragesAndTotals(MatchState matchState,
|
||||
ZombieAggregateClientStat lifetimeStat, RoundState roundState, ZombieMatchClientStat matchStat)
|
||||
{
|
||||
var currentRoundNumber = roundState.PersistentClientRound.RoundNumber;
|
||||
// don't credit if played less than 50%
|
||||
var shouldCountHighestRound = matchState.RoundNumber > lifetimeStat.HighestRound &&
|
||||
matchStat.JoinedRound is not null &&
|
||||
currentRoundNumber - matchStat.JoinedRound.Value >=
|
||||
currentRoundNumber / 2.0;
|
||||
|
||||
if (shouldCountHighestRound)
|
||||
{
|
||||
lifetimeStat.HighestRound = matchState.RoundNumber;
|
||||
}
|
||||
|
||||
lifetimeStat.TotalRoundsPlayed++;
|
||||
|
||||
var roundWeightFactor = lifetimeStat.TotalRoundsPlayed <= RoundsConsidered
|
||||
? 1.0 / lifetimeStat.TotalRoundsPlayed
|
||||
: 2.0 / (RoundsConsidered + 1);
|
||||
|
||||
var roundKpd = roundState.PersistentClientRound.Kills / Math.Max(1,
|
||||
roundState.PersistentClientRound.Deaths + roundState.PersistentClientRound.Downs);
|
||||
lifetimeStat.AverageKillsPerDown =
|
||||
CalculateAverage(lifetimeStat.AverageKillsPerDown, roundKpd, roundWeightFactor);
|
||||
lifetimeStat.AverageDowns =
|
||||
CalculateAverage(lifetimeStat.AverageDowns, matchStat.Downs, roundWeightFactor);
|
||||
lifetimeStat.AverageRevives =
|
||||
CalculateAverage(lifetimeStat.AverageRevives, matchStat.Revives, roundWeightFactor);
|
||||
lifetimeStat.AverageRoundReached = CalculateAverage(lifetimeStat.AverageRoundReached,
|
||||
matchState.RoundNumber, roundWeightFactor);
|
||||
lifetimeStat.AverageMelees =
|
||||
CalculateAverage(lifetimeStat.AverageMelees, matchStat.Melees, roundWeightFactor);
|
||||
|
||||
var hsp = roundState.PersistentClientRound.Headshots / (double)Math.Max(1, roundState.Hits);
|
||||
lifetimeStat.HeadshotPercentage = CalculateAverage(lifetimeStat.HeadshotPercentage, hsp, roundWeightFactor);
|
||||
|
||||
lifetimeStat.AlivePercentage = CalculateAverage(lifetimeStat.AlivePercentage,
|
||||
roundState.PersistentClientRound.TimeAlive!.Value.TotalMilliseconds /
|
||||
roundState.PersistentClientRound.Duration!.Value.TotalMilliseconds, roundWeightFactor);
|
||||
|
||||
lifetimeStat.AveragePoints = CalculateAverage(lifetimeStat.AveragePoints,
|
||||
roundState.PersistentClientRound.PointsEarned, roundWeightFactor);
|
||||
}
|
||||
|
||||
private void RunCalculation(EFClient client, Action<RoundState> calculation)
|
||||
{
|
||||
if (stateManager.GetStateForClient(client) is not { } match)
|
||||
{
|
||||
logger.LogWarning("[ZM] No active zombie match for {Client}", client.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match.RoundStates.TryGetValue(client.NetworkId, out var roundState))
|
||||
{
|
||||
logger.LogWarning("[ZM] No active zombie round for {Client}", client.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
calculation(roundState);
|
||||
|
||||
stateManager.TrackUpdatedState(roundState.PersistentClientRound);
|
||||
}
|
||||
|
||||
private void RunAggregateCalculation(EFClient client,
|
||||
Action<MatchState, RoundState, ZombieMatchClientStat, ZombieAggregateClientStat, ZombieAggregateClientStat> action)
|
||||
{
|
||||
if (stateManager.GetStateForClient(client) is not { } match)
|
||||
{
|
||||
logger.LogWarning("[ZM] No active zombie match for {Client}", client.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
var currentRoundState = match.RoundStates[client.NetworkId];
|
||||
var matchStat = match.PersistentMatchAggregateStats[client.NetworkId];
|
||||
var lifetimeAggregateStat = match.PersistentLifetimeAggregateStats[client.NetworkId];
|
||||
var lifetimeServerAggregateState = match.PersistentLifetimeServerAggregateStats[client.NetworkId];
|
||||
|
||||
action(match, currentRoundState, matchStat, lifetimeAggregateStat, lifetimeServerAggregateState);
|
||||
|
||||
stateManager.TrackUpdatedState(currentRoundState.PersistentClientRound);
|
||||
stateManager.TrackUpdatedState(matchStat);
|
||||
stateManager.TrackUpdatedState(lifetimeAggregateStat);
|
||||
stateManager.TrackUpdatedState(lifetimeServerAggregateState);
|
||||
}
|
||||
|
||||
private static double CalculateAverage(double previousAverage, double currentValue, double factor) =>
|
||||
Math.Round(currentValue * factor + previousAverage * (1 - factor), 2);
|
||||
}
|
15
Plugins/ZombieStats/States/MatchState.cs
Normal file
15
Plugins/ZombieStats/States/MatchState.cs
Normal file
@ -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<long, RoundState> RoundStates { get; } = new();
|
||||
public Dictionary<long, ZombieMatchClientStat> PersistentMatchAggregateStats { get; } = new();
|
||||
public Dictionary<long, ZombieAggregateClientStat> PersistentLifetimeAggregateStats { get; } = new();
|
||||
public Dictionary<long, ZombieAggregateClientStat> PersistentLifetimeServerAggregateStats { get; } = new();
|
||||
public Dictionary<long, Dictionary<string, EFClientStatTagValue>> PersistentStatTagValues { get; } = new();
|
||||
public int RoundNumber { get; set; }
|
||||
}
|
10
Plugins/ZombieStats/States/RoundState.cs
Normal file
10
Plugins/ZombieStats/States/RoundState.cs
Normal file
@ -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; }
|
||||
}
|
824
Plugins/ZombieStats/States/ZombieClientStateManager.cs
Normal file
824
Plugins/ZombieStats/States/ZombieClientStateManager.cs
Normal file
@ -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<ZombieClientStateManager> logger,
|
||||
IDatabaseContextFactory contextFactory,
|
||||
ITranslationLookup translations)
|
||||
{
|
||||
private readonly ILogger _logger = logger;
|
||||
private readonly Dictionary<(long, Reference.Game), MatchState> _clientMatches = new();
|
||||
private readonly List<MatchState> _matches = [];
|
||||
private readonly List<MatchState> _recentlyEndedMatches = [];
|
||||
private readonly List<DatedRecord> _addedPersistence = [];
|
||||
private readonly List<DatedRecord> _updatedPersistence = [];
|
||||
private readonly SemaphoreSlim _onWorking = new(1, 1);
|
||||
private Dictionary<string, List<ZombieClientStatRecord>> _recordsCache = null!;
|
||||
private Dictionary<string, EFClientStatTag> _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<DatedRecord> 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<DatedRecord> 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<int, List<EFMeta>> 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<int, List<EFMeta>> 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<EFMeta>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
196
Plugins/ZombieStats/ZombieStats.cs
Normal file
196
Plugins/ZombieStats/ZombieStats.cs
Normal file
@ -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<ZombieStats> _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<ZombieStats> 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<ZombieEventParser>()
|
||||
.AddSingleton<ZombieEventProcessor>()
|
||||
.AddSingleton<ZombieClientStateManager>();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -43,5 +43,6 @@ namespace SharedLibraryCore.Dtos
|
||||
}
|
||||
}
|
||||
public Reference.Game Game { get; set; }
|
||||
public string PerformanceBucket { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
|
||||
namespace SharedLibraryCore.Events.Game.GameScript;
|
||||
|
||||
public class RoundEndEvent : GameEventV2
|
||||
{
|
@ -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; }
|
||||
}
|
@ -5,5 +5,4 @@ namespace SharedLibraryCore.Events.Server;
|
||||
public class ServerStatusReceiveEvent : GameServerEvent
|
||||
{
|
||||
public IStatusResponse Response { get; set; }
|
||||
public string RawData { get; set; }
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2024.01.01.1</Version>
|
||||
<Version>2024.0.0.0</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
|
@ -28,13 +28,14 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/advanced")]
|
||||
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId, CancellationToken token = default)
|
||||
public async Task<IActionResult> 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<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false);
|
||||
await statMetricFunc(new Dictionary<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, performanceBucket, false);
|
||||
}
|
||||
|
||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
|
||||
|
@ -46,12 +46,13 @@ namespace WebfrontCore.Controllers.Client.Legacy
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TopPlayers(string serverId = null, CancellationToken token = default)
|
||||
public async Task<IActionResult> 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<IActionResult> GetTopPlayersAsync(int count, int offset, long? serverId = null)
|
||||
public async Task<IActionResult> 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
|
||||
|
@ -19,7 +19,7 @@ namespace WebfrontCore.ViewComponents
|
||||
_statManager = statManager;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, string serverEndpoint = null)
|
||||
public async Task<IViewComponentResult> 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));
|
||||
}
|
||||
}
|
||||
|
@ -248,7 +248,7 @@
|
||||
<div class="col-12 col-lg-9 mt-0">
|
||||
<h2 class="content-title mb-0">@ViewBag.Title</h2>
|
||||
<span class="text-muted">
|
||||
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
<color-code value="@(Model.PerformanceBucket ?? Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
</span>
|
||||
|
||||
<!-- top card -->
|
||||
@ -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"),
|
||||
|
@ -5,12 +5,12 @@
|
||||
<div class="col-12 col-lg-9 mt-0">
|
||||
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_TOP_PLAYERS_TITLE"]</h2>
|
||||
<span class="text-muted">
|
||||
<color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
<color-code value="@(ViewBag.SelectedPerformanceBucket ?? Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
— <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> @ViewBag.Localization["WEBFRONT_TOP_PLAYERS_SUBTITLE"]
|
||||
</span>
|
||||
|
||||
<div id="topPlayersContainer">
|
||||
@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 })
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<i id="loaderLoad" class="oi oi-chevron-bottom loader-load-more text-primary mt-5" aria-hidden="true"></i>
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||
@ -48,5 +56,5 @@
|
||||
<script type="text/javascript" src="~/js/stats.js"></script>
|
||||
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25, 25, [{ 'name': 'serverId', 'value' : () => @(ViewBag.ServerId ?? 0) }]);</script>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25, 25, [{ 'name': 'serverId', 'value' : () => @(ViewBag.ServerId ?? 0) }, { 'name': 'performanceBucket', 'value' : () => '@ViewBag.SelectedPerformanceBucket' }]);</script>
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<TypeScriptToolsVersion>2.6</TypeScriptToolsVersion>
|
||||
<PackageId>RaidMax.IW4MAdmin.WebfrontCore</PackageId>
|
||||
<Version>2022.0.0</Version>
|
||||
<Version>2024.0.0.0</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Product>IW4MAdmin</Product>
|
||||
|
Loading…
x
Reference in New Issue
Block a user