1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-07 21:58:06 -05:00

Additional zombie stast work

This commit is contained in:
RaidMax 2024-02-11 22:10:12 -06:00
parent 122b6dc79d
commit e1461582fa
45 changed files with 7663 additions and 292 deletions

View File

@ -136,6 +136,9 @@ namespace IW4MAdmin.Application
public IEnumerable<IPlugin> Plugins { get; } public IEnumerable<IPlugin> Plugins { get; }
public IInteractionRegistration InteractionRegistration { get; } public IInteractionRegistration InteractionRegistration { get; }
public IList<Func<Dictionary<int, List<EFMeta>>, long?, string, bool, Task>> CustomStatsMetrics { get; } =
new List<Func<Dictionary<int, List<EFMeta>>, long?, string, bool, Task>>();
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent); ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
@ -249,7 +252,13 @@ namespace IW4MAdmin.Application
{ {
var thisIndex = index; var thisIndex = index;
Interlocked.Increment(ref index); Interlocked.Increment(ref index);
return ProcessUpdateHandler(server, thisIndex); return ProcessUpdateHandler(server, thisIndex).ContinueWith(result =>
{
if (result.IsFaulted)
{
_logger.LogError(result.Exception, "Encountered unexpected error processing updates");
}
}, CancellationToken);
})); }));
} }
@ -599,18 +608,21 @@ namespace IW4MAdmin.Application
{ {
_eventHandlerTokenSource = new CancellationTokenSource(); _eventHandlerTokenSource = new CancellationTokenSource();
var eventHandlerThread = new Thread(() => var eventHandlerTask = Task.Run(() =>
{ {
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token); _coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
}) }, _eventHandlerTokenSource.Token);
{
Name = nameof(CoreEventHandler)
};
eventHandlerThread.Start();
await UpdateServerStates(); await UpdateServerStates();
_eventHandlerTokenSource.Cancel(); _eventHandlerTokenSource.Cancel();
eventHandlerThread.Join();
try
{
await eventHandlerTask;
}
catch (OperationCanceledException)
{
}
} }
public async Task Stop() public async Task Stop()

View File

@ -45,15 +45,13 @@ namespace IW4MAdmin.Application
public void StartProcessing(CancellationToken token) public void StartProcessing(CancellationToken token)
{ {
_cancellationToken = token; while (!token.IsCancellationRequested)
while (!_cancellationToken.IsCancellationRequested)
{ {
_onEventReady.Reset(); _onEventReady.Reset();
try try
{ {
_onProcessingEvents.Wait(_cancellationToken); _onProcessingEvents.Wait(token);
if (!_runningEventTasks.TryDequeue(out var coreEvent)) if (!_runningEventTasks.TryDequeue(out var coreEvent))
{ {
@ -62,7 +60,7 @@ namespace IW4MAdmin.Application
_onProcessingEvents.Release(1); _onProcessingEvents.Release(1);
} }
_onEventReady.Wait(_cancellationToken); _onEventReady.Wait(token);
continue; continue;
} }

View File

@ -2439,6 +2439,7 @@
"left_foot": "Left Foot", "left_foot": "Left Foot",
"left_arm_upper": "Upper Left Arm", "left_arm_upper": "Upper Left Arm",
"left_arm_lower": "Lower Left Arm", "left_arm_lower": "Lower Left Arm",
"head": "Head",
"gl": "Rifle Grenade", "gl": "Rifle Grenade",
"bigammo": "Round Drum", "bigammo": "Round Drum",
"scoped": "Sniper Scope", "scoped": "Sniper Scope",
@ -2446,34 +2447,238 @@
"aperture": "Aperture Sight", "aperture": "Aperture Sight",
"flash": "Flash Hider", "flash": "Flash Hider",
"silenced": "Silencer", "silenced": "Silencer",
"molotov": "Molotov Cocktail",
"sticky": "N° 74 ST", "sticky": "N° 74 ST",
"m2": "M2 Flamethrower", "m2": "M2 Flamethrower",
"artillery": "Artillery Strike", "artillery": "Artillery Strike",
"dog": "Attack Dogs", "dog": "Attack Dogs",
"colt": "Colt M1911", "colt": "Colt M1911",
"357magnum": ".357 Magnum", "sw_357": ".357 Magnum",
"walther": "Walther P38", "walther": "Walther P38",
"tokarev": "Tokarev TT-33",
"shotgun": "M1897 Trench Gun",
"doublebarreledshotgun": "Double-Barreled Shotgun", "doublebarreledshotgun": "Double-Barreled Shotgun",
"mp40": "MP40", "doublebarrel": "Double-Barreled Shotgun",
"type100smg": "Type 100", "30cal": "Browning M1919",
"ppsh": "PPSh-41", "bar": "BAR",
"svt40": "SVT-40", "fg42": "FG42",
"gewehr43": "Gewehr 43",
"m1garand": "M1 Garand", "m1garand": "M1 Garand",
"mp40": "MP40",
"ppsh": "PPSh-41",
"gewehr43": "Gewehr 43",
"svt40": "SVT-40",
"nambu": "Nambu",
"m1garand_bayonet": "M1 Garand Bayonet",
"m1a1carbine_bayonet": "M1A1 Carbine Bayonet",
"kar98k_bayonet": "Kar98k Bayonet",
"mosin_rifle_bayonet": "Mosin-Nagant Bayonet",
"mg42": "MG42",
"colt45": "Colt M1911",
"sten_silenced": "Silenced Sten",
"type99_lmg": "Type 99",
"dp28": "DP-28",
"mine_shoebox": "PMD-6",
"mine_bouncing_betty": "Bouncing Betty",
"357magnum": ".357 Magnum",
"ptrs41": "PTRS-41",
"remingtonmodel11": "Remington Model 11",
"tabun_grenade": "Tabun Gas",
"signal_flare": "Signal Flare",
"shotgun_double_barreled": "Double-Barreled Shotgun",
"m7_launcher": "M7 Grenade Launcher",
"fg42_telescopic": "Telescopic Sight",
"springfield_scoped": "Scoped Springfield",
"m1garand_gl": "M1 Garand w/ Launcher",
"sticky_grenade": "N\u00ba 74 ST",
"tokarev_tt30": "Tokarev TT-33",
"mg42_bipod": "Deployable MG42",
"dp28_bipod": "Deployable DP-28",
"fg42_bipod": "Deployable FG42",
"bar_bipod": "Deployable BAR",
"30cal_bipod": "Deployable Browning M1919",
"type99_lmg_bipod": "Deployable Type 99",
"type100smg": "Type 100",
"stg44": "STG-44", "stg44": "STG-44",
"m1carbine": "M1A1 Carbine", "m1carbine": "M1A1 Carbine",
"type99lmg": "Type 99", "type99lmg": "Type 99",
"bar": "BAR", "syrette": "Syrette",
"dp28": "DP-28", "supportgunner": "Support Gunner",
"mg42": "MG42", "bren": "Bren LMG",
"fg42": "FG42", "rifleman": "Rifleman",
"30cal": "Browning M1919", "lee_enfield": "Lee-Enfield",
"type99rifle": "Arisaka", "kar98k": "Kar98k",
"mosinrifle": "Mosin-Nagant", "luger": "Luger",
"ptrs41": "PTRS-41" "m1a1carbine": "M1A1 Carbine",
"mosin_rifle": "Mosin-Nagant",
"mosin_rifle_scoped": "Scoped Mosin-Nagant",
"sniper": "Sniper",
"submachinegunner": "Submachine Gunner",
"mp44": "MP44",
"springfield": "Springfield",
"mosinnagantammo": "Mosin-Nagant Ammo",
"sten": "Sten",
"armyengineer": "Army Engineer",
"thompson": "Thompson",
"fastauto": "Fast-Auto",
"slowauto": "Slow-Auto",
"fullauto": "Full-Auto",
"semiauto": "Semi-Auto",
"m2fraggrenade": "M2 Frag Grenade",
"mk1_frag_grenade": "MK1 Frag Grenade",
"russiangrenade": "RGD-33 Stick Grenade",
"germangrenade": "Stielhandgranate",
"panzerschrek": "Panzerschrek",
"panzerfaust": "Panzerfaust 60",
"scopedkar98k": "Scoped Kar98k",
"holdpin": "Hold-Pin",
"cookoff": "Cook-Off",
"medicplaceholder": "Medic",
"fraggrenade": "Frag",
"m8_white_smoke": "Smoke",
"shotgun": "M1897 Trench Gun",
"greasegun": "Grease Gun",
"pps42": "PPS42",
"webley": "Webley",
"scopedg43": "Scoped Gewehr 43",
"defaultweapon": "Default Weapon",
"satchel": "Satchel Charge",
"anm8_smoke_grenade": "AN-M8 Smoke Grenade",
"no77_wp_smoke_grenade": "No.77 WP Smoke Grenade",
"nebelhandgranate": "Nebelhandgranate",
"rgd1_smoke_grenade": "RGD-1 Smoke Grenade",
"potato": "Potato",
"no_ammo": "No Ammo",
"no_frag_grenade": "No Primary Grenades Remaining",
"no_smoke_grenade": "No Smoke Grenades Remaining",
"no_flash_grenade": "No Flashbang Grenades Remaining",
"noecial_grenade": "No Special Grenades Remaining",
"location_selector": "Select a location",
"smoke_grenade": "Smoke Grenade",
"flash_grenade": "Flash Grenade",
"concussion_grenade": "Stun Grenade",
"smgs": "Submachine Guns",
"assaultrifles": "Assault Rifles",
"shotguns": "Shotguns",
"sniperrifles": "Sniper Rifles",
"target_too_close": "Too Close to Target",
"lockon_required": "Lock-On Required",
"target_not_enough_clearance": "Not Enough Room To Fire",
"no_attachment": "No Attachment",
"silencer": "Suppressor",
"grenade_launcher": "Grenade Launcher",
"no_camo": "No Camo",
"golden_camo": "Golden",
"prestige_camo": "Prestige",
"binoculars": "Binoculars",
"grip": "Grip",
"m16a4_grenadier": "M16A4 Grenadier",
"panzershrek": "Panzershrek",
"bazooka": "M9A1 Bazooka",
"bazooka_man": "Bazooka",
"tokarev": "Tokarev TT-33",
"russian_flag": "Red Army Banner",
"stg-44": "STG-44",
"mortar_round": "Mortar Round",
"molotov": "Molotov Cocktail",
"fireblob": "Napalm Blob (Fire on Ground)",
"m2_flamethrower": "M2 Flamethrower",
"flamethrower_gunner": "Flamethrower",
"kar98k_scoped": "Scoped Kar98k",
"lee_enfield_scoped": "Scoped Lee-Enfield",
"type100_smg": "Type 100",
"type99_rifle": "Arisaka",
"type99_rifle_bayonet": "Arisaka Bayonet",
"type99_rifle_scoped": "Scoped Arisaka",
"walther_p38": "Walther P38",
"shotgunner": "Shotgunner",
"doublebarrel_sawed_grip": "Sawed-Off Double-Barreled Shotgun w/ Grip",
"antitank_gunner": "Anti-Tank Gunner",
"springfield_no_attachment": "No Attachment",
"springfield_bayonet": "Springfield Bayonet",
"springfield_rifle_grenade": "Rifle Grenade",
"type99_rifle_rifle_grenade": "Rifle Grenade",
"type99_rifle_no_attachment": "No Attachment",
"kar98k_no_attachment": "No Attachment",
"kar98k_rifle_grenade": "Rifle Grenade",
"mosin_rifle_no_attachment": "No Attachment",
"mosin_rifle_rifle_grenade": "Rifle Grenade",
"svt40_no_attachment": "No Attachment",
"svt40_flash": "Flash Hider",
"svt40_aperture": "Aperture Sight",
"svt40_telescopic": "Telescopic Sight",
"svt40_select_fire": "Select Fire",
"gewehr43_no_attachment": "No Attachment",
"gewehr43_silenced": "Suppressor",
"gewehr43_aperture": "Aperture Sight",
"gewehr43_telescopic": "Telescopic Sight",
"gewehr43_rifle_grenade": "Rifle Grenade",
"m1garand_no_attachment": "No Attachment",
"m1garand_rifle_grenade": "Rifle Grenade",
"m1garand_scoped": "Sniper Scope",
"m1garand_flash": "Flash Hider",
"m1a1carbine_no_attachment": "No Attachment",
"m1a1carbine_flash": "Flash Hider",
"m1a1carbine_bigammo": "Box Magazine",
"m1a1carbine_aperture": "Aperture Sight",
"stg-44_no_attachment": "No Attachment",
"stg-44_flash": "Flash Hider",
"stg-44_aperture": "Aperture Sight",
"stg-44_telescopic": "Telescopic Sight",
"stg-44_select_fire": "Select Fire",
"thompson_no_attachment": "No Attachment",
"thompson_silenced": "Suppressor",
"thompson_aperture": "Aperture Sight",
"thompson_bigammo": "Round Drum",
"type100_smg_no_attachment": "No Attachment",
"type100_smg_silenced": "Suppressor",
"type100_smg_bigammo": "Box Magazine",
"type100_smg_aperture": "Aperture Sight",
"mp40_no_attachment": "No Attachment",
"mp40_silenced": "Suppressor",
"mp40_aperture": "Aperture Sight",
"mp40_bigammo": "Dual Magazines",
"ppsh_no_attachment": "No Attachment",
"ppsh_aperture": "Aperture Sight",
"ppsh_bigammo": "Round Drum",
"shotgun_no_attachment": "No Attachment",
"shotgun_grip": "Grip",
"shotgun_bayonet": "Bayonet",
"shotgun_double_barreled_no_attachment": "No Attachment",
"shotgun_double_barreled_grip": "Grip",
"doublebarrel_sawed": "Sawed-Off Shotgun",
"sailor": "Crewman",
"fg42_scoped": "Scoped FG42",
"mosin_launcher": "Mosin-Nagant w/ Launcher",
"zombie_melee": "BRAAAINS...",
"ray_gun": "Ray Gun",
"nomad": "Nomad",
"tesla_gun": "Wunderwaffe DG-2",
"30cal_upgraded": "B115 accelerator",
"bar_upgraded": "The Widow Maker",
"colt_upgraded": "C-3000 b1at-ch35",
"shotgun_double_barreled_sawed_grip_upgraded": "The Snuff Box",
"shotgun_double_barreled_upgraded": "24 Bore long range",
"fg42_upgraded": "420 Impeller",
"gewehr43_upgraded": "G115 Compressor",
"m1a1carbine_upgraded": "Widdershins RC-1",
"m1garand_upgraded": "M1000",
"mg42_upgraded": "Barracuda FU-A11",
"mp40_upgraded": "The Afterburner",
"ppsh_upgraded": "The Reaper",
"shotgun_upgraded": "Gut Shot",
"stg-44_upgraded": "Spatz-447 +",
"sw_357_upgraded": ".357 Plus 1 K1L-u",
"thompson_upgraded": "Gibs-o-matic",
"type100_smg_upgraded": "1001 Samurais",
"type99_rifle_upgraded": "The Eviscerator",
"panzerschrek_upgraded": "Longinus",
"ray_gun_upgraded": "Porter's X2 Ray Gun",
"tesla_gun_upgraded": "Wunderwaffe DG-3 JZ",
"m2_flamethrower_upgraded": "FIW Nitrogen cooled",
"ptrs41_upgraded": "The Penetrator",
"m7_launcher_upgraded": "The Imploder",
"nazi_zombies_cap": "NAZI ZOMBIES",
"kar98k_upgraded": "Armageddon",
"m7_launcher_upgraded_nonade": "The Imploder",
"zombie_knuckle_crack": "Pack A Punch Knuckle Crack",
"cymbal_monkey": "Cymbal Monkey"
}, },
"T6" : { "T6" : {

View File

@ -98,10 +98,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber); ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName); var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
var foundClient = true;
// first time client is connecting to server // first time client is connecting to server
if (client == null) if (client == null)
{ {
foundClient = false;
ServerLogger.LogDebug("Client {client} first time connecting", clientFromLog.ToString()); ServerLogger.LogDebug("Client {client} first time connecting", clientFromLog.ToString());
clientFromLog.CurrentServer = this; clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog); client = await Manager.GetClientService().Create(clientFromLog);
@ -109,12 +111,16 @@ namespace IW4MAdmin
client.CopyAdditionalProperties(clientFromLog); client.CopyAdditionalProperties(clientFromLog);
// this is only a temporary version until the IPAddress is transmitted if (foundClient)
client.CurrentAlias = new EFAlias()
{ {
Name = clientFromLog.Name, client.CurrentAlias = new EFAlias
IPAddress = clientFromLog.IPAddress {
}; AliasId = client.CurrentAliasId,
LinkId = client.AliasLinkId,
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
}
// Do the player specific stuff // Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber; client.ClientNumber = clientFromLog.ClientNumber;
@ -413,10 +419,7 @@ namespace IW4MAdmin
{ {
if (E.Origin.State != ClientState.Connected) if (E.Origin.State != ClientState.Connected)
{ {
E.Origin.State = ClientState.Connected; ChatHistory.Add(new ChatInfo
E.Origin.Connections += 1;
ChatHistory.Add(new ChatInfo()
{ {
Name = E.Origin.Name, Name = E.Origin.Name,
Message = "CONNECTED", Message = "CONNECTED",
@ -431,6 +434,10 @@ namespace IW4MAdmin
E.Origin.Tag = clientTag.Value; E.Origin.Tag = clientTag.Value;
} }
await E.Origin.OnJoin(E.Origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
E.Origin.State = ClientState.Connected;
E.Origin.Connections += 1;
try try
{ {
var factory = _serviceProvider.GetRequiredService<IDatabaseContextFactory>(); var factory = _serviceProvider.GetRequiredService<IDatabaseContextFactory>();
@ -449,8 +456,6 @@ namespace IW4MAdmin
ServerLogger.LogError(ex, "Could not get offline message count for {Client}", E.Origin.ToString()); ServerLogger.LogError(ex, "Could not get offline message count for {Client}", E.Origin.ToString());
throw; throw;
} }
await E.Origin.OnJoin(E.Origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
} }
} }
@ -907,6 +912,12 @@ namespace IW4MAdmin
context.Entry(gameServer).Property(property => property.HostName).IsModified = true; context.Entry(gameServer).Property(property => property.HostName).IsModified = true;
} }
if (gameServer.PerformanceBucket != PerformanceBucket)
{
gameServer.PerformanceBucket = PerformanceBucket;
context.Entry(gameServer).Property(property => property.PerformanceBucket).IsModified = true;
}
if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword)) if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword))
{ {
gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword); gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword);

View File

@ -57,6 +57,7 @@ namespace Data.Context
public DbSet<ZombieRoundClientStat> ZombieRoundClientStats { get; set; } public DbSet<ZombieRoundClientStat> ZombieRoundClientStats { get; set; }
public DbSet<ZombieAggregateClientStat> ZombieClientStatAggregates { get; set; } public DbSet<ZombieAggregateClientStat> ZombieClientStatAggregates { get; set; }
public DbSet<ZombieClientStatRecord> ZombieClientStatRecords { get; set; } public DbSet<ZombieClientStatRecord> ZombieClientStatRecords { get; set; }
public DbSet<ZombieEventLog> ZombieEvents { get; set; }
#endregion #endregion
@ -102,6 +103,19 @@ namespace Data.Context
client.NetworkId, client.NetworkId,
client.GameName 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 => modelBuilder.Entity<EFPenalty>(entity =>
@ -171,12 +185,34 @@ namespace Data.Context
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot)); modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory)); modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
modelBuilder.Entity(typeof(ZombieMatch)).ToTable($"EF{nameof(ZombieMatch)}"); modelBuilder.Entity<ZombieMatch>().ToTable($"EF{nameof(ZombieMatch)}");
modelBuilder.Entity(typeof(ZombieMatchClientStat)).ToTable($"EF{nameof(ZombieMatchClientStat)}");
modelBuilder.Entity(typeof(ZombieRoundClientStat)).ToTable($"EF{nameof(ZombieRoundClientStat)}"); modelBuilder.Entity<ZombieClientStat>(ent =>
modelBuilder.Entity(typeof(ZombieAggregateClientStat)).ToTable($"EF{nameof(ZombieAggregateClientStat)}"); {
modelBuilder.Entity(typeof(ZombieClientStat)).ToTable($"EF{nameof(ZombieClientStat)}"); ent.ToTable($"EF{nameof(ZombieClientStat)}");
modelBuilder.Entity(typeof(ZombieClientStatRecord)).ToTable($"EF{nameof(ZombieClientStatRecord)}"); ent.HasOne(prop => prop.Client)
.WithMany(prop => prop.ZombieClientStats)
.HasForeignKey(prop => prop.ClientId);
});
modelBuilder.Entity<ZombieMatchClientStat>(ent =>
{
ent.ToTable($"EF{nameof(ZombieMatchClientStat)}");
});
modelBuilder.Entity<ZombieRoundClientStat>(ent =>
{
ent.ToTable($"EF{nameof(ZombieRoundClientStat)}");
});
modelBuilder.Entity<ZombieAggregateClientStat>(ent =>
{
ent.ToTable($"EF{nameof(ZombieAggregateClientStat)}");
});
modelBuilder.Entity<ZombieEventLog>().ToTable($"EF{nameof(ZombieEvents)}");
modelBuilder.Entity<ZombieClientStatRecord>().ToTable($"EF{nameof(ZombieClientStatRecord)}");
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder); Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,319 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Data.Migrations.Postgresql
{
/// <inheritdoc />
public partial class InitialZombieStats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PerformanceBucket",
table: "EFServers",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PerformanceBucket",
table: "EFClientRankingHistory",
type: "text",
nullable: true);
migrationBuilder.CreateTable(
name: "EFZombieMatch",
columns: table => new
{
ZombieMatchId = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
MapId = table.Column<int>(type: "integer", nullable: true),
ServerId = table.Column<long>(type: "bigint", nullable: true),
ClientsCompleted = table.Column<int>(type: "integer", nullable: false),
MatchStartDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
MatchEndDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CreatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieMatch", x => x.ZombieMatchId);
table.ForeignKey(
name: "FK_EFZombieMatch_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId");
table.ForeignKey(
name: "FK_EFZombieMatch_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId");
});
migrationBuilder.CreateTable(
name: "EFZombieClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
MatchId = table.Column<int>(type: "integer", nullable: true),
ClientId = table.Column<int>(type: "integer", nullable: false),
Kills = table.Column<int>(type: "integer", nullable: false),
Deaths = table.Column<int>(type: "integer", nullable: false),
DamageDealt = table.Column<long>(type: "bigint", 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),
PointsEarned = table.Column<long>(type: "bigint", nullable: false),
PointsSpent = table.Column<long>(type: "bigint", nullable: false),
PerksConsumed = table.Column<int>(type: "integer", nullable: false),
PowerupsGrabbed = table.Column<int>(type: "integer", nullable: false),
CreatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieClientStat_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFZombieClientStat_EFZombieMatch_MatchId",
column: x => x.MatchId,
principalTable: "EFZombieMatch",
principalColumn: "ZombieMatchId");
});
migrationBuilder.CreateTable(
name: "EFZombieEvents",
columns: table => new
{
ZombieEventLogId = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
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: "double precision", nullable: true),
TextualValue = table.Column<string>(type: "text", nullable: true),
MatchId = table.Column<int>(type: "integer", nullable: true),
CreatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", 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.CreateTable(
name: "EFZombieAggregateClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "bigint", nullable: false),
ServerId = table.Column<long>(type: "bigint", nullable: true),
AverageKillsPerDown = table.Column<double>(type: "double precision", nullable: false),
AverageDowns = table.Column<double>(type: "double precision", nullable: false),
AverageRevives = table.Column<double>(type: "double precision", nullable: false),
HeadshotPercentage = table.Column<double>(type: "double precision", nullable: false),
AlivePercentage = table.Column<double>(type: "double precision", nullable: false),
AverageMelees = table.Column<double>(type: "double precision", nullable: false),
AverageRoundReached = table.Column<double>(type: "double precision", nullable: false),
AveragePoints = table.Column<double>(type: "double precision", nullable: false),
HighestRound = table.Column<int>(type: "integer", nullable: false),
TotalRoundsPlayed = table.Column<int>(type: "integer", nullable: false),
TotalMatchesPlayed = table.Column<int>(type: "integer", nullable: false),
TotalMatchesCompleted = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieAggregateClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId");
table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFZombieClientStat_ZombieClient~",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieMatchClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieMatchClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieMatchClientStat_EFZombieClientStat_ZombieClientStat~",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieRoundClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "bigint", nullable: false),
StartTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Duration = table.Column<TimeSpan>(type: "interval", nullable: true),
TimeAlive = table.Column<TimeSpan>(type: "interval", nullable: true),
RoundNumber = table.Column<int>(type: "integer", nullable: false),
Points = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieRoundClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieRoundClientStat_EFZombieClientStat_ZombieClientStat~",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieClientStatRecord",
columns: table => new
{
ZombieClientStatRecordId = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Type = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: false),
ClientId = table.Column<int>(type: "integer", nullable: true),
RoundId = table.Column<long>(type: "bigint", nullable: true),
CreatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieClientStatRecord", x => x.ZombieClientStatRecordId);
table.ForeignKey(
name: "FK_EFZombieClientStatRecord_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieClientStatRecord_EFZombieRoundClientStat_RoundId",
column: x => x.RoundId,
principalTable: "EFZombieRoundClientStat",
principalColumn: "ZombieClientStatId");
});
migrationBuilder.CreateIndex(
name: "IX_EFZombieAggregateClientStat_ServerId",
table: "EFZombieAggregateClientStat",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStat_ClientId",
table: "EFZombieClientStat",
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",
column: "RoundId");
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_EFZombieMatch_MapId",
table: "EFZombieMatch",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatch_ServerId",
table: "EFZombieMatch",
column: "ServerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFZombieAggregateClientStat");
migrationBuilder.DropTable(
name: "EFZombieClientStatRecord");
migrationBuilder.DropTable(
name: "EFZombieEvents");
migrationBuilder.DropTable(
name: "EFZombieMatchClientStat");
migrationBuilder.DropTable(
name: "EFZombieRoundClientStat");
migrationBuilder.DropTable(
name: "EFZombieClientStat");
migrationBuilder.DropTable(
name: "EFZombieMatch");
migrationBuilder.DropColumn(
name: "PerformanceBucket",
table: "EFServers");
migrationBuilder.DropColumn(
name: "PerformanceBucket",
table: "EFClientRankingHistory");
}
}
}

View File

@ -17,7 +17,7 @@ namespace Data.Migrations.Postgresql
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "6.0.1") .HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -459,6 +459,9 @@ namespace Data.Migrations.Postgresql
b.Property<bool>("Newest") b.Property<bool>("Newest")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("PerformanceBucket")
.HasColumnType("text");
b.Property<double?>("PerformanceMetric") b.Property<double?>("PerformanceMetric")
.HasColumnType("double precision"); .HasColumnType("double precision");
@ -1125,6 +1128,9 @@ namespace Data.Migrations.Postgresql
b.Property<bool>("IsPasswordProtected") b.Property<bool>("IsPasswordProtected")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("PerformanceBucket")
.HasColumnType("text");
b.Property<int>("Port") b.Property<int>("Port")
.HasColumnType("integer"); .HasColumnType("integer");
@ -1222,6 +1228,278 @@ namespace Data.Migrations.Postgresql
b.ToTable("Vector3", (string)null); b.ToTable("Vector3", (string)null);
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.Property<long>("ZombieClientStatId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ZombieClientStatId"));
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("DamageDealt")
.HasColumnType("bigint");
b.Property<int>("DamageReceived")
.HasColumnType("integer");
b.Property<int>("Deaths")
.HasColumnType("integer");
b.Property<int>("Downs")
.HasColumnType("integer");
b.Property<int>("HeadshotKills")
.HasColumnType("integer");
b.Property<int>("Headshots")
.HasColumnType("integer");
b.Property<int>("Kills")
.HasColumnType("integer");
b.Property<int?>("MatchId")
.HasColumnType("integer");
b.Property<int>("Melees")
.HasColumnType("integer");
b.Property<int>("PerksConsumed")
.HasColumnType("integer");
b.Property<long>("PointsEarned")
.HasColumnType("bigint");
b.Property<long>("PointsSpent")
.HasColumnType("bigint");
b.Property<int>("PowerupsGrabbed")
.HasColumnType("integer");
b.Property<int>("Revives")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("ZombieClientStatId");
b.HasIndex("ClientId");
b.HasIndex("MatchId");
b.ToTable("EFZombieClientStat", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.Property<int>("ZombieClientStatRecordId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ZombieClientStatRecordId"));
b.Property<int?>("ClientId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("RoundId")
.HasColumnType("bigint");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("ZombieClientStatRecordId");
b.HasIndex("ClientId");
b.HasIndex("RoundId");
b.ToTable("EFZombieClientStatRecord", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b =>
{
b.Property<long>("ZombieEventLogId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ZombieEventLogId"));
b.Property<int?>("AssociatedClientId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("EventType")
.HasColumnType("integer");
b.Property<int?>("MatchId")
.HasColumnType("integer");
b.Property<double?>("NumericalValue")
.HasColumnType("double precision");
b.Property<int?>("SourceClientId")
.HasColumnType("integer");
b.Property<string>("TextualValue")
.HasColumnType("text");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("ZombieEventLogId");
b.HasIndex("AssociatedClientId");
b.HasIndex("MatchId");
b.HasIndex("SourceClientId");
b.ToTable("EFZombieEvents", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Property<int>("ZombieMatchId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ZombieMatchId"));
b.Property<int>("ClientsCompleted")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("timestamp with time zone");
b.Property<int?>("MapId")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("MatchEndDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("MatchStartDate")
.HasColumnType("timestamp with time zone");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("ZombieMatchId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFZombieMatch", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<double>("AlivePercentage")
.HasColumnType("double precision");
b.Property<double>("AverageDowns")
.HasColumnType("double precision");
b.Property<double>("AverageKillsPerDown")
.HasColumnType("double precision");
b.Property<double>("AverageMelees")
.HasColumnType("double precision");
b.Property<double>("AveragePoints")
.HasColumnType("double precision");
b.Property<double>("AverageRevives")
.HasColumnType("double precision");
b.Property<double>("AverageRoundReached")
.HasColumnType("double precision");
b.Property<double>("HeadshotPercentage")
.HasColumnType("double precision");
b.Property<int>("HighestRound")
.HasColumnType("integer");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<int>("TotalMatchesCompleted")
.HasColumnType("integer");
b.Property<int>("TotalMatchesPlayed")
.HasColumnType("integer");
b.Property<int>("TotalRoundsPlayed")
.HasColumnType("integer");
b.HasIndex("ServerId");
b.ToTable("EFZombieAggregateClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.ToTable("EFZombieMatchClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<TimeSpan?>("Duration")
.HasColumnType("interval");
b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Points")
.HasColumnType("integer");
b.Property<int>("RoundNumber")
.HasColumnType("integer");
b.Property<DateTimeOffset>("StartTime")
.HasColumnType("timestamp with time zone");
b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("interval");
b.ToTable("EFZombieRoundClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b =>
{ {
b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot")
@ -1663,6 +1941,107 @@ namespace Data.Migrations.Postgresql
b.Navigation("Server"); b.Navigation("Server");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany("ZombieClientStats")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany()
.HasForeignKey("MatchId");
b.Navigation("Client");
b.Navigation("Match");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId");
b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round")
.WithMany()
.HasForeignKey("RoundId");
b.Navigation("Client");
b.Navigation("Round");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b =>
{
b.HasOne("Data.Models.Client.EFClient", "AssociatedClient")
.WithMany()
.HasForeignKey("AssociatedClientId");
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany()
.HasForeignKey("MatchId");
b.HasOne("Data.Models.Client.EFClient", "SourceClient")
.WithMany()
.HasForeignKey("SourceClientId");
b.Navigation("AssociatedClient");
b.Navigation("Match");
b.Navigation("SourceClient");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId");
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Map");
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClient", b => modelBuilder.Entity("Data.Models.Client.EFClient", b =>
{ {
b.Navigation("AdministeredPenalties"); b.Navigation("AdministeredPenalties");
@ -1670,6 +2049,8 @@ namespace Data.Migrations.Postgresql
b.Navigation("Meta"); b.Navigation("Meta");
b.Navigation("ReceivedPenalties"); b.Navigation("ReceivedPenalties");
b.Navigation("ZombieClientStats");
}); });
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>

View File

@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Data.Migrations.Sqlite namespace Data.Migrations.Sqlite
{ {
[DbContext(typeof(SqliteDatabaseContext))] [DbContext(typeof(SqliteDatabaseContext))]
[Migration("20230507181011_AddZombieStatsInitial")] [Migration("20230905194120_IntitialZombieStats")]
partial class AddZombieStatsInitial partial class IntitialZombieStats
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
@ -1117,6 +1117,8 @@ namespace Data.Migrations.Sqlite
b.HasKey("ServerSnapshotId"); b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId"); b.HasIndex("MapId");
b.HasIndex("ServerId"); b.HasIndex("ServerId");
@ -1181,7 +1183,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTimeOffset>("CreatedDateTime") b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("DamageDealt") b.Property<long>("DamageDealt")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("DamageReceived") b.Property<int>("DamageReceived")
@ -1337,15 +1339,15 @@ namespace Data.Migrations.Sqlite
b.Property<double>("AverageRoundReached") b.Property<double>("AverageRoundReached")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<double>("HeadshotPercentage") b.Property<double>("HeadshotPercentage")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<int>("HighestRound") b.Property<int>("HighestRound")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesCompleted") b.Property<int>("TotalMatchesCompleted")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1355,7 +1357,7 @@ namespace Data.Migrations.Sqlite
b.Property<int>("TotalRoundsPlayed") b.Property<int>("TotalRoundsPlayed")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.HasIndex("EFClientClientId"); b.HasIndex("ServerId");
b.ToTable("EFZombieAggregateClientStat", (string)null); b.ToTable("EFZombieAggregateClientStat", (string)null);
}); });
@ -1364,11 +1366,6 @@ namespace Data.Migrations.Sqlite
{ {
b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieMatchClientStat", (string)null); b.ToTable("EFZombieMatchClientStat", (string)null);
}); });
@ -1379,9 +1376,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("Duration") b.Property<TimeSpan?>("Duration")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("EndTime") b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1397,8 +1391,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("TimeAlive") b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieRoundClientStat", (string)null); b.ToTable("EFZombieRoundClientStat", (string)null);
}); });
@ -1852,7 +1844,7 @@ namespace Data.Migrations.Sqlite
.IsRequired(); .IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany("ClientStats") .WithMany()
.HasForeignKey("MatchId"); .HasForeignKey("MatchId");
b.Navigation("Client"); b.Navigation("Client");
@ -1896,23 +1888,21 @@ namespace Data.Migrations.Sqlite
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null) b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany("ZombieAggregateClientStats") .WithMany()
.HasForeignKey("EFClientClientId"); .HasForeignKey("ServerId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Server");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatchClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
@ -1922,10 +1912,6 @@ namespace Data.Migrations.Sqlite
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieRoundClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
@ -1941,15 +1927,9 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties"); b.Navigation("ReceivedPenalties");
b.Navigation("ZombieAggregateClientStats");
b.Navigation("ZombieClientStats"); b.Navigation("ZombieClientStats");
b.Navigation("ZombieMatchClientStats");
b.Navigation("ZombieMatches"); b.Navigation("ZombieMatches");
b.Navigation("ZombieRoundClientStats");
}); });
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>
@ -1973,11 +1953,6 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties"); b.Navigation("ReceivedPenalties");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Navigation("ClientStats");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite namespace Data.Migrations.Sqlite
{ {
public partial class AddZombieStatsInitial : Migration public partial class IntitialZombieStats : Migration
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
@ -66,7 +66,7 @@ namespace Data.Migrations.Sqlite
ClientId = table.Column<int>(type: "INTEGER", nullable: false), ClientId = table.Column<int>(type: "INTEGER", nullable: false),
Kills = table.Column<int>(type: "INTEGER", nullable: false), Kills = table.Column<int>(type: "INTEGER", nullable: false),
Deaths = table.Column<int>(type: "INTEGER", nullable: false), Deaths = table.Column<int>(type: "INTEGER", nullable: false),
DamageDealt = table.Column<int>(type: "INTEGER", nullable: false), DamageDealt = table.Column<long>(type: "INTEGER", nullable: false),
DamageReceived = table.Column<int>(type: "INTEGER", nullable: false), DamageReceived = table.Column<int>(type: "INTEGER", nullable: false),
Headshots = table.Column<int>(type: "INTEGER", nullable: false), Headshots = table.Column<int>(type: "INTEGER", nullable: false),
Melees = table.Column<int>(type: "INTEGER", nullable: false), Melees = table.Column<int>(type: "INTEGER", nullable: false),
@ -101,6 +101,7 @@ namespace Data.Migrations.Sqlite
{ {
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false) ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
ServerId = table.Column<long>(type: "INTEGER", nullable: true),
AverageKillsPerDown = table.Column<double>(type: "REAL", nullable: false), AverageKillsPerDown = table.Column<double>(type: "REAL", nullable: false),
AverageDowns = table.Column<double>(type: "REAL", nullable: false), AverageDowns = table.Column<double>(type: "REAL", nullable: false),
AverageRevives = table.Column<double>(type: "REAL", nullable: false), AverageRevives = table.Column<double>(type: "REAL", nullable: false),
@ -112,17 +113,16 @@ namespace Data.Migrations.Sqlite
HighestRound = table.Column<int>(type: "INTEGER", nullable: false), HighestRound = table.Column<int>(type: "INTEGER", nullable: false),
TotalRoundsPlayed = table.Column<int>(type: "INTEGER", nullable: false), TotalRoundsPlayed = table.Column<int>(type: "INTEGER", nullable: false),
TotalMatchesPlayed = table.Column<int>(type: "INTEGER", nullable: false), TotalMatchesPlayed = table.Column<int>(type: "INTEGER", nullable: false),
TotalMatchesCompleted = table.Column<int>(type: "INTEGER", nullable: false), TotalMatchesCompleted = table.Column<int>(type: "INTEGER", nullable: false)
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_EFZombieAggregateClientStat", x => x.ZombieClientStatId); table.PrimaryKey("PK_EFZombieAggregateClientStat", x => x.ZombieClientStatId);
table.ForeignKey( table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFClients_EFClientClientId", name: "FK_EFZombieAggregateClientStat_EFServers_ServerId",
column: x => x.EFClientClientId, column: x => x.ServerId,
principalTable: "EFClients", principalTable: "EFServers",
principalColumn: "ClientId"); principalColumn: "ServerId");
table.ForeignKey( table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFZombieClientStat_ZombieClientStatId", name: "FK_EFZombieAggregateClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId, column: x => x.ZombieClientStatId,
@ -136,17 +136,11 @@ namespace Data.Migrations.Sqlite
columns: table => new columns: table => new
{ {
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false) ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true)
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_EFZombieMatchClientStat", x => x.ZombieClientStatId); table.PrimaryKey("PK_EFZombieMatchClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieMatchClientStat_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey( table.ForeignKey(
name: "FK_EFZombieMatchClientStat_EFZombieClientStat_ZombieClientStatId", name: "FK_EFZombieMatchClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId, column: x => x.ZombieClientStatId,
@ -166,17 +160,11 @@ namespace Data.Migrations.Sqlite
Duration = table.Column<TimeSpan>(type: "TEXT", nullable: true), Duration = table.Column<TimeSpan>(type: "TEXT", nullable: true),
TimeAlive = table.Column<TimeSpan>(type: "TEXT", nullable: true), TimeAlive = table.Column<TimeSpan>(type: "TEXT", nullable: true),
RoundNumber = table.Column<int>(type: "INTEGER", nullable: false), RoundNumber = table.Column<int>(type: "INTEGER", nullable: false),
Points = table.Column<int>(type: "INTEGER", nullable: false), Points = table.Column<int>(type: "INTEGER", nullable: false)
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_EFZombieRoundClientStat", x => x.ZombieClientStatId); table.PrimaryKey("PK_EFZombieRoundClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieRoundClientStat_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey( table.ForeignKey(
name: "FK_EFZombieRoundClientStat_EFZombieClientStat_ZombieClientStatId", name: "FK_EFZombieRoundClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId, column: x => x.ZombieClientStatId,
@ -215,9 +203,9 @@ namespace Data.Migrations.Sqlite
}); });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_EFZombieAggregateClientStat_EFClientClientId", name: "IX_EFZombieAggregateClientStat_ServerId",
table: "EFZombieAggregateClientStat", table: "EFZombieAggregateClientStat",
column: "EFClientClientId"); column: "ServerId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStat_ClientId", name: "IX_EFZombieClientStat_ClientId",
@ -253,16 +241,6 @@ namespace Data.Migrations.Sqlite
name: "IX_EFZombieMatch_ServerId", name: "IX_EFZombieMatch_ServerId",
table: "EFZombieMatch", table: "EFZombieMatch",
column: "ServerId"); column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatchClientStat_EFClientClientId",
table: "EFZombieMatchClientStat",
column: "EFClientClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieRoundClientStat_EFClientClientId",
table: "EFZombieRoundClientStat",
column: "EFClientClientId");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
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");
}
}
}

View File

@ -15,7 +15,7 @@ namespace Data.Migrations.Sqlite
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b =>
{ {
@ -1181,7 +1181,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTimeOffset>("CreatedDateTime") b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("DamageDealt") b.Property<long>("DamageDealt")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("DamageReceived") b.Property<int>("DamageReceived")
@ -1193,6 +1193,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("Downs") b.Property<int>("Downs")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("HeadshotKills")
.HasColumnType("INTEGER");
b.Property<int>("Headshots") b.Property<int>("Headshots")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1230,6 +1233,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("MatchId"); b.HasIndex("MatchId");
b.ToTable("EFZombieClientStat", (string)null); b.ToTable("EFZombieClientStat", (string)null);
b.UseTptMappingStrategy();
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
@ -1271,6 +1276,47 @@ namespace Data.Migrations.Sqlite
b.ToTable("EFZombieClientStatRecord", (string)null); b.ToTable("EFZombieClientStatRecord", (string)null);
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b =>
{
b.Property<long>("ZombieEventLogId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("AssociatedClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<int?>("MatchId")
.HasColumnType("INTEGER");
b.Property<double?>("NumericalValue")
.HasColumnType("REAL");
b.Property<int?>("SourceClientId")
.HasColumnType("INTEGER");
b.Property<string>("TextualValue")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.HasKey("ZombieEventLogId");
b.HasIndex("AssociatedClientId");
b.HasIndex("MatchId");
b.HasIndex("SourceClientId");
b.ToTable("EFZombieEvents", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{ {
b.Property<int>("ZombieMatchId") b.Property<int>("ZombieMatchId")
@ -1283,9 +1329,6 @@ namespace Data.Migrations.Sqlite
b.Property<DateTimeOffset>("CreatedDateTime") b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<int?>("MapId") b.Property<int?>("MapId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1303,8 +1346,6 @@ namespace Data.Migrations.Sqlite
b.HasKey("ZombieMatchId"); b.HasKey("ZombieMatchId");
b.HasIndex("EFClientClientId");
b.HasIndex("MapId"); b.HasIndex("MapId");
b.HasIndex("ServerId"); b.HasIndex("ServerId");
@ -1337,15 +1378,15 @@ namespace Data.Migrations.Sqlite
b.Property<double>("AverageRoundReached") b.Property<double>("AverageRoundReached")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<double>("HeadshotPercentage") b.Property<double>("HeadshotPercentage")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<int>("HighestRound") b.Property<int>("HighestRound")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesCompleted") b.Property<int>("TotalMatchesCompleted")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1355,7 +1396,7 @@ namespace Data.Migrations.Sqlite
b.Property<int>("TotalRoundsPlayed") b.Property<int>("TotalRoundsPlayed")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.HasIndex("EFClientClientId"); b.HasIndex("ServerId");
b.ToTable("EFZombieAggregateClientStat", (string)null); b.ToTable("EFZombieAggregateClientStat", (string)null);
}); });
@ -1364,11 +1405,6 @@ namespace Data.Migrations.Sqlite
{ {
b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieMatchClientStat", (string)null); b.ToTable("EFZombieMatchClientStat", (string)null);
}); });
@ -1379,9 +1415,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("Duration") b.Property<TimeSpan?>("Duration")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("EndTime") b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1397,8 +1430,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("TimeAlive") b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieRoundClientStat", (string)null); b.ToTable("EFZombieRoundClientStat", (string)null);
}); });
@ -1852,7 +1883,7 @@ namespace Data.Migrations.Sqlite
.IsRequired(); .IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany("ClientStats") .WithMany()
.HasForeignKey("MatchId"); .HasForeignKey("MatchId");
b.Navigation("Client"); b.Navigation("Client");
@ -1875,12 +1906,29 @@ namespace Data.Migrations.Sqlite
b.Navigation("Round"); b.Navigation("Round");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b =>
{
b.HasOne("Data.Models.Client.EFClient", "AssociatedClient")
.WithMany()
.HasForeignKey("AssociatedClientId");
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany()
.HasForeignKey("MatchId");
b.HasOne("Data.Models.Client.EFClient", "SourceClient")
.WithMany()
.HasForeignKey("SourceClientId");
b.Navigation("AssociatedClient");
b.Navigation("Match");
b.Navigation("SourceClient");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatches")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany() .WithMany()
.HasForeignKey("MapId"); .HasForeignKey("MapId");
@ -1896,23 +1944,21 @@ namespace Data.Migrations.Sqlite
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null) b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany("ZombieAggregateClientStats") .WithMany()
.HasForeignKey("EFClientClientId"); .HasForeignKey("ServerId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Server");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatchClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
@ -1922,10 +1968,6 @@ namespace Data.Migrations.Sqlite
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{ {
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieRoundClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null) b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne() .WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
@ -1941,15 +1983,7 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties"); b.Navigation("ReceivedPenalties");
b.Navigation("ZombieAggregateClientStats");
b.Navigation("ZombieClientStats"); b.Navigation("ZombieClientStats");
b.Navigation("ZombieMatchClientStats");
b.Navigation("ZombieMatches");
b.Navigation("ZombieRoundClientStats");
}); });
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>
@ -1973,11 +2007,6 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties"); b.Navigation("ReceivedPenalties");
}); });
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Navigation("ClientStats");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -84,10 +84,6 @@ namespace Data.Models.Client
public virtual ICollection<EFMeta> Meta { get; set; } public virtual ICollection<EFMeta> Meta { get; set; }
public virtual ICollection<EFPenalty> ReceivedPenalties { get; set; } public virtual ICollection<EFPenalty> ReceivedPenalties { get; set; }
public virtual ICollection<EFPenalty> AdministeredPenalties { get; set; } public virtual ICollection<EFPenalty> AdministeredPenalties { get; set; }
public virtual ICollection<ZombieAggregateClientStat> ZombieAggregateClientStats { get; set; }
public virtual ICollection<ZombieClientStat> ZombieClientStats { get; set; } public virtual ICollection<ZombieClientStat> ZombieClientStats { get; set; }
public virtual ICollection<ZombieMatch> ZombieMatches { get; set; }
public virtual ICollection<ZombieMatchClientStat> ZombieMatchClientStats { get; set; }
public virtual ICollection<ZombieRoundClientStat> ZombieRoundClientStats { get; set; }
} }
} }

View File

@ -2,8 +2,9 @@
namespace Data.Models; namespace Data.Models;
public class DatedRecord public class DatedRecord : IdentifierRecord
{ {
public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? UpdatedDateTime { get; set; } public DateTimeOffset? UpdatedDateTime { get; set; }
public override long Id { get; }
} }

View File

@ -0,0 +1,6 @@
namespace Data.Models;
public abstract class IdentifierRecord
{
public abstract long Id { get; }
}

View File

@ -1,9 +1,15 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Data.Models.Server;
namespace Data.Models.Zombie; namespace Data.Models.Zombie;
public class ZombieAggregateClientStat : ZombieClientStat public class ZombieAggregateClientStat : ZombieClientStat
{ {
public long? ServerId { get; set; }
[ForeignKey(nameof(ServerId))]
public EFServer Server { get; set; }
#region Average #region Average
public double AverageKillsPerDown { get; set; } public double AverageKillsPerDown { get; set; }
@ -41,4 +47,8 @@ public class ZombieAggregateClientStat : ZombieClientStat
nameof(TotalRoundsPlayed), nameof(TotalRoundsPlayed),
nameof(TotalMatchesPlayed) nameof(TotalMatchesPlayed)
}; };
public static readonly string[] SkillKeys =
RecordsKeys.Except(new[] { nameof(TotalMatchesPlayed), nameof(TotalRoundsPlayed), nameof(AverageDowns) })
.ToArray();
} }

View File

@ -10,6 +10,8 @@ public abstract class ZombieClientStat : DatedRecord
[Key] [Key]
public long ZombieClientStatId { get; set; } public long ZombieClientStatId { get; set; }
[NotMapped] public override long Id => ZombieClientStatId;
public int? MatchId { get; set; } public int? MatchId { get; set; }
[ForeignKey(nameof(MatchId))] [ForeignKey(nameof(MatchId))]
@ -17,13 +19,14 @@ public abstract class ZombieClientStat : DatedRecord
public int ClientId { get; set; } public int ClientId { get; set; }
[ForeignKey(nameof(ClientId))] [ForeignKey(nameof(ClientId))]
public virtual EFClient? Client { get; set; } public virtual EFClient Client { get; set; }
public int Kills { get; set; } public int Kills { get; set; }
public int Deaths { get; set; } public int Deaths { get; set; }
public int DamageDealt { get; set; } public long DamageDealt { get; set; }
public int DamageReceived { get; set; } public int DamageReceived { get; set; }
public int Headshots { get; set; } public int Headshots { get; set; }
public int HeadshotKills { get; set; }
public int Melees { get; set; } public int Melees { get; set; }
public int Downs { get; set; } public int Downs { get; set; }
public int Revives { get; set; } public int Revives { get; set; }

View File

@ -15,6 +15,9 @@ public class ZombieClientStatRecord : DatedRecord
{ {
[Key] [Key]
public int ZombieClientStatRecordId { get; set; } public int ZombieClientStatRecordId { get; set; }
[NotMapped] public override long Id => ZombieClientStatRecordId;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty; public string Value { get; set; } = string.Empty;

View File

@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client;
namespace Data.Models.Zombie;
public enum EventLogType
{
Default = 0,
PerformanceCluster = 1,
DamageTaken = 2,
Downed = 3,
Died = 4,
Revived = 5,
WasRevived = 6,
PerkConsumed = 7,
PowerupGrabbed = 8,
RoundCompleted = 9,
JoinedMatch = 10,
LeftMatch = 11,
MatchStarted = 12,
MatchEnded = 13
}
public class ZombieEventLog : DatedRecord
{
[Key]
public long ZombieEventLogId { get; set; }
[NotMapped] public override long Id => ZombieEventLogId;
public EventLogType EventType { get; set; }
public int? SourceClientId { get; set; }
[ForeignKey(nameof(SourceClientId))]
public EFClient SourceClient { get; set; }
public int? AssociatedClientId { get; set; }
[ForeignKey(nameof(AssociatedClientId))]
public EFClient AssociatedClient { get; set; }
public double? NumericalValue { get; set; }
public string TextualValue { get; set; }
public int? MatchId { get; set; }
[ForeignKey(nameof(MatchId))]
public ZombieMatch Match { get; set; }
}

View File

@ -1,6 +1,5 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client.Stats.Reference; using Data.Models.Client.Stats.Reference;
@ -13,6 +12,8 @@ public class ZombieMatch : DatedRecord
[Key] [Key]
public int ZombieMatchId { get; set; } public int ZombieMatchId { get; set; }
[NotMapped] public override long Id => ZombieMatchId;
public int? MapId { get; set; } public int? MapId { get; set; }
[ForeignKey(nameof(MapId))] [ForeignKey(nameof(MapId))]
public virtual EFMap? Map { get; set; } public virtual EFMap? Map { get; set; }
@ -23,8 +24,6 @@ public class ZombieMatch : DatedRecord
public int ClientsCompleted { get; set; } public int ClientsCompleted { get; set; }
public virtual ICollection<ZombieClientStat>? ClientStats { get; set; }
public DateTimeOffset MatchStartDate { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset MatchStartDate { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? MatchEndDate { get; set; } public DateTimeOffset? MatchEndDate { get; set; }
} }

View File

@ -1,6 +1,8 @@
namespace Data.Models.Zombie; using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Models.Zombie;
public class ZombieMatchClientStat : ZombieClientStat public class ZombieMatchClientStat : ZombieClientStat
{ {
[NotMapped] public int? JoinedRound { get; set; }
} }

View File

@ -57,7 +57,7 @@ namespace Stats.Client
var iqPerformances = set var iqPerformances = set
.Where(s => s.Skill > 0) .Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0) .Where(s => s.EloRating >= 0)
.Where(s => s.Client.Level != EFClient.Permission.Banned); .Where(s => s.Client.Level != EFClient.Permission.Banned);
foreach (var serverId in _serverIds) foreach (var serverId in _serverIds)
@ -71,30 +71,33 @@ namespace Stats.Client
distributions.Add(serverId.ToString(), distributionParams); distributions.Add(serverId.ToString(), distributionParams);
} }
foreach (var server in _appConfig.Servers) foreach (var performanceBucketGroup in _appConfig.Servers.GroupBy(server => server.PerformanceBucket))
{ {
if (string.IsNullOrWhiteSpace(server.PerformanceBucket)) if (string.IsNullOrWhiteSpace(performanceBucketGroup.Key))
{ {
continue; continue;
} }
var performanceBucket = performanceBucketGroup.Key;
var bucketConfig = var bucketConfig =
_configuration.PerformanceBuckets.FirstOrDefault(bucket => _configuration.PerformanceBuckets.FirstOrDefault(bucket =>
bucket.Name == server.PerformanceBucket) ?? new PerformanceBucketConfiguration(); bucket.Name == performanceBucket) ?? new PerformanceBucketConfiguration();
var oldestPerf = DateTimeOffset.UtcNow - bucketConfig.RankingExpiration; var oldestPerf = DateTime.UtcNow - bucketConfig.RankingExpiration;
var performances = await iqPerformances var performances = await iqPerformances
.Where(perf => perf.Server.PerformanceBucket == server.PerformanceBucket) .Where(perf => perf.Server.PerformanceBucket == performanceBucket)
.Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds) .Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
.Where(perf => perf.UpdatedAt >= oldestPerf) .Where(perf => perf.UpdatedAt >= oldestPerf)
.Where(perf => perf.Skill < 999999)
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0) .Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
.ToListAsync(token); .ToListAsync(token);
var distributionParams = performances.GenerateDistributionParameters(); var distributionParams = performances.GenerateDistributionParameters();
distributions.Add(server.PerformanceBucket, distributionParams); distributions.Add(performanceBucket, distributionParams);
} }
return distributions; return distributions;
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1)); }, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
foreach (var server in _appConfig.Servers) foreach (var server in _appConfig.Servers)
{ {
@ -117,7 +120,7 @@ namespace Stats.Client
var zScore = await set var zScore = await set
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat)) .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
.Where(s => s.Skill > 0) .Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0) .Where(s => s.EloRating >= 1)
.Where(stat => .Where(stat =>
performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket) performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket)
.GroupBy(stat => stat.ClientId) .GroupBy(stat => stat.ClientId)
@ -127,7 +130,7 @@ namespace Stats.Client
return zScore ?? 0; return zScore ?? 0;
}, MaxZScoreCacheKey, new[] { server.PerformanceBucket }, }, MaxZScoreCacheKey, new[] { server.PerformanceBucket },
Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30)); Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30));
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket }); await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket });
} }
@ -199,6 +202,8 @@ namespace Stats.Client
return 0.0; return 0.0;
} }
value = Math.Max(1, value);
var zScore = (Math.Log(value) - sdParams.Mean) / sdParams.Sigma; var zScore = (Math.Log(value) - sdParams.Mean) / sdParams.Sigma;
return zScore; return zScore;
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
@ -25,5 +26,6 @@ namespace Stats.Dtos
public List<EFClientHitStatistic> ByAttachmentCombo { get; set; } public List<EFClientHitStatistic> ByAttachmentCombo { get; set; }
public List<EFClientRankingHistory> Ratings { get; set; } public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; } public List<EFClientStatistics> LegacyStats { get; set; }
public List<EFMeta> CustomMetrics { get; set; } = new();
} }
} }

View File

@ -1,7 +1,7 @@
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using Data.Models;
namespace IW4MAdmin.Plugins.Stats.Web.Dtos namespace IW4MAdmin.Plugins.Stats.Web.Dtos
{ {
@ -22,6 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public List<PerformanceHistory> PerformanceHistory { get; set; } public List<PerformanceHistory> PerformanceHistory { get; set; }
public double? ZScore { get; set; } public double? ZScore { get; set; }
public long? ServerId { get; set; } public long? ServerId { get; set; }
public List<EFMeta> Metrics { get; } = new();
} }
public class PerformanceHistory public class PerformanceHistory

View File

@ -110,25 +110,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0; return 0;
} }
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, private Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null)
long? serverId = null)
{ {
return (ranking) => ranking.ServerId == serverId var oldestDate = DateTime.UtcNow - oldestStat;
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned return ranking => ranking.ServerId == serverId
&& ranking.CreatedDateTime >= Extensions.FifteenDaysAgo() && ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
&& ranking.ZScore != null && ranking.CreatedDateTime >= oldestDate
&& ranking.PerformanceMetric != null && ranking.ZScore != null
&& ranking.Newest && ranking.PerformanceMetric != null
&& ranking.Client.TotalConnectionTime >= && ranking.Newest
_config.TopPlayersMinPlayTime; && ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds;
} }
public async Task<int> GetTotalRankedPlayers(long serverId) public async Task<int> GetTotalRankedPlayers(long serverId)
{ {
var bucketConfig = await GetBucketConfig(serverId);
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.Set<EFClientRankingHistory>() return await context.Set<EFClientRankingHistory>()
.Where(GetNewRankingFunc(serverId: serverId)) .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
.CountAsync(); .CountAsync();
} }
@ -143,12 +144,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public DateTime CreatedDateTime { get; set; } public DateTime CreatedDateTime { get; set; }
} }
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null) public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null, string performanceBucket = null)
{ {
await using var context = _contextFactory.CreateContext(false); var bucketConfig = await GetBucketConfig(serverId);
await using var context = _contextFactory.CreateContext(false);
var clientIdsList = await context.Set<EFClientRankingHistory>() var clientIdsList = await context.Set<EFClientRankingHistory>()
.Where(GetNewRankingFunc(serverId: serverId)) .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
.OrderByDescending(ranking => ranking.PerformanceMetric) .OrderByDescending(ranking => ranking.PerformanceMetric)
.Select(ranking => ranking.ClientId) .Select(ranking => ranking.ClientId)
.Skip(start) .Skip(start)
@ -233,9 +235,77 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.OrderBy(r => r.Ranking) .OrderBy(r => r.Ranking)
.ToList(); .ToList();
foreach (var topStatsInfo in finished)
{
topStatsInfo.Metrics.AddRange(new EFMeta[]
{
new()
{
Extra = "Kills",
Value = topStatsInfo.Kills.ToNumericalString(),
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"]
},
new()
{
Extra = "Deaths",
Value = topStatsInfo.Deaths.ToNumericalString(),
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"]
},
new()
{
Extra = "KDR",
Value = topStatsInfo.KDR.ToNumericalString(),
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"]
},
new()
{
Extra = "TimePlayed",
Value = topStatsInfo.TimePlayedValue.HumanizeForCurrentCulture(),
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]
},
new()
{
Extra = "LastSeen",
Value = topStatsInfo.LastSeenValue.HumanizeForCurrentCulture(),
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"]
}
});
}
foreach (var customMetricFunc in Plugin.ServerManager.CustomStatsMetrics)
{
await customMetricFunc(finished.ToDictionary(kvp => kvp.ClientId, kvp => kvp.Metrics), serverId,
performanceBucket, true);
}
return finished; return finished;
} }
private async Task<PerformanceBucketConfiguration> GetBucketConfig(long? serverId)
{
var defaultConfig = new PerformanceBucketConfiguration
{
ClientMinPlayTime = TimeSpan.FromSeconds(_config.TopPlayersMinPlayTime),
RankingExpiration = DateTime.UtcNow - Extensions.FifteenDaysAgo()
};
if (serverId is null)
{
return defaultConfig;
}
var performanceBucket =
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
if (string.IsNullOrEmpty(performanceBucket))
{
return defaultConfig;
}
return _config.PerformanceBuckets.FirstOrDefault(bucket => bucket.Name == performanceBucket) ??
defaultConfig;
}
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null) public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
{ {
if (_config.EnableAdvancedMetrics) if (_config.EnableAdvancedMetrics)
@ -1179,54 +1249,41 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId) public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId)
{ {
var bucketConfig = await GetBucketConfig(serverId);
await using var context = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var minPlayTime = _config.TopPlayersMinPlayTime; var oldestStateDate = DateTime.UtcNow - bucketConfig.RankingExpiration;
var oldestStat = DateTimeOffset.UtcNow - Extensions.FifteenDaysAgo();
var performanceBucket =
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
if (!string.IsNullOrEmpty(performanceBucket))
{
var bucketConfig = _config.PerformanceBuckets.FirstOrDefault(cfg => cfg.Name == performanceBucket) ??
new PerformanceBucketConfiguration();
minPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
oldestStat = bucketConfig.RankingExpiration;
}
var oldestStateDate = DateTime.UtcNow - oldestStat;
var performances = await context.Set<EFClientStatistics>() var performances = await context.Set<EFClientStatistics>()
.AsNoTracking() .AsNoTracking()
.Include(stat => stat.Server)
.Where(stat => stat.ClientId == clientId) .Where(stat => stat.ClientId == clientId)
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking .Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
.Where(stats => stats.UpdatedAt >= oldestStateDate) .Where(stats => stats.UpdatedAt >= oldestStateDate)
.Where(stats => stats.TimePlayed >= minPlayTime) .Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)
.ToListAsync(); .ToListAsync();
if (clientStats.TimePlayed >= minPlayTime) if (clientStats.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
{ {
await UpdateForServer(clientId, clientStats, context, minPlayTime, oldestStat, serverId); await UpdateForServer(clientId, clientStats, context, (int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration, serverId);
clientStats.Server = await _serverCache.FirstAsync(server => server.Id == serverId);
performances.Add(clientStats); performances.Add(clientStats);
} }
if (performances.Any(performance => performance.TimePlayed >= minPlayTime)) if (performances.Any(performance => performance.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds))
{ {
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, minPlayTime, await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, bucketConfig);
oldestStat, performanceBucket);
} }
} }
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, PerformanceBucketConfiguration bucketConfig)
int minPlayTime, TimeSpan oldestStat, string performanceBucket)
{ {
var aggregateZScore = var aggregateZScore =
performances.Where(performance => performance.Server.PerformanceBucket == performanceBucket) performances.Where(performance => performance.Server.PerformanceBucket == bucketConfig.Name)
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime); .WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds);
int? aggregateRanking = await context.Set<EFClientStatistics>() int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId) .Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat)) .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
.GroupBy(stat => stat.ClientId) .GroupBy(stat => stat.ClientId)
.Where(group => .Where(group =>
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) > group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
@ -1234,7 +1291,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Select(c => c.Key) .Select(c => c.Key)
.CountAsync(); .CountAsync();
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, performanceBucket); var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, bucketConfig.Name);
if (newPerformanceMetric == null) if (newPerformanceMetric == null)
{ {
@ -1249,13 +1306,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ZScore = aggregateZScore, ZScore = aggregateZScore,
Ranking = aggregateRanking, Ranking = aggregateRanking,
PerformanceMetric = newPerformanceMetric, PerformanceMetric = newPerformanceMetric,
PerformanceBucket = performanceBucket, PerformanceBucket = bucketConfig.Name,
Newest = true, Newest = true,
}; };
context.Add(aggregateRankingSnapshot); context.Add(aggregateRankingSnapshot);
await PruneOldRankings(context, clientId); await PruneOldRankings(context, clientId, performanceBucket: bucketConfig.Name);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
@ -1364,6 +1421,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2)); attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2));
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2)); victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
var attackerEloRatingFunc =
attacker.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("EloRatingFunction");
attackerStats.EloRating =
attackerEloRatingFunc?.Invoke(attacker, attackerStats) ?? attackerStats.EloRating;
var victimEloRatingFunc =
victim.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("EloRatingFunction");
victimStats.EloRating =
attackerEloRatingFunc?.Invoke(victim, victimStats) ?? victimStats.EloRating;
// update after calculation // update after calculation
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds; attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds; victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
@ -1428,7 +1497,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds ? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
: clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds; : clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0)); double SPMAgainstPlayWeight = totalPlayTime == 0 ? killSpm : timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
// calculate the new weight against average times the weight against play time // calculate the new weight against average times the weight against play time
clientStats.SPM = (killSpm * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); clientStats.SPM = (killSpm * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
@ -1446,7 +1515,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
skillFunction?.Invoke(client, clientStats) ?? Math.Round(clientStats.SPM * KDRWeight, 3); skillFunction?.Invoke(client, clientStats) ?? Math.Round(clientStats.SPM * KDRWeight, 3);
// fixme: how does this happen? // fixme: how does this happen?
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill)) if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill) || double.IsInfinity(clientStats.Skill))
{ {
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}", _log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
new new

View File

@ -1,10 +1,20 @@
using Data.Models.Client.Stats; using System.Linq;
using Data.Models.Client.Stats;
namespace Stats.Helpers namespace Stats.Helpers
{ {
public static class WeaponNameExtensions public static class WeaponNameExtensions
{ {
public static string RebuildWeaponName(this EFClientHitStatistic stat) => public static string RebuildWeaponName(this EFClientHitStatistic stat)
$"{stat.Weapon?.Name}{string.Join("_", stat.WeaponAttachmentCombo?.Attachment1?.Name, stat.WeaponAttachmentCombo?.Attachment2?.Name, stat.WeaponAttachmentCombo?.Attachment3?.Name)}"; {
var attachments =
new[]
{
stat.WeaponAttachmentCombo?.Attachment1?.Name, stat.WeaponAttachmentCombo?.Attachment2?.Name,
stat.WeaponAttachmentCombo?.Attachment3?.Name
}.Where(a => !string.IsNullOrEmpty(a));
return $"{stat.Weapon?.Name?.Replace("zombie_", "").Replace("_zombie", "")}{string.Join("_", attachments)}";
}
} }
} }

View File

@ -117,6 +117,7 @@ public class Plugin : IPluginV2
} }
}; };
IGameEventSubscriptions.MatchEnded += OnMatchEvent; IGameEventSubscriptions.MatchEnded += OnMatchEvent;
IGameEventSubscriptions.RoundEnded += (roundEndedEvent, token) => _statManager.Sync(roundEndedEvent.Server, token);
IGameEventSubscriptions.MatchStarted += OnMatchEvent; IGameEventSubscriptions.MatchStarted += OnMatchEvent;
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
IGameEventSubscriptions.ClientKilled += OnClientKilled; IGameEventSubscriptions.ClientKilled += OnClientKilled;

View File

@ -1,21 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>IW4MAdmin.Plugins.ZombieStats</RootNamespace> <RootNamespace>IW4MAdmin.Plugins.ZombieStats</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.16"> <ProjectReference Include="..\..\Data\Data.csproj" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.15.3" />-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" /> <ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -15,11 +15,11 @@ namespace SharedLibraryCore.Configuration
if (!ContainsKey(game.Value)) if (!ContainsKey(game.Value))
{ {
return key.Transform(To.TitleCase); return key;
} }
var strings = this[game.Value]; var strings = this[game.Value];
return !strings.ContainsKey(key) ? key.Transform(To.TitleCase) : strings[key]; return !strings.TryGetValue(key, out var value) ? key : value;
} }
} }
} }

View File

@ -24,8 +24,16 @@ public static class EventExtensions
{ {
if (token == CancellationToken.None) if (token == CancellationToken.None)
{ {
// special case to allow tasks like request after delay to run longer try
await handler(eventArgType, token); {
// special case to allow tasks like request after delay to run longer
await handler(eventArgType, token);
}
catch (Exception ex)
{
// todo: static logger
Console.WriteLine("InvokeAsync: " + ex);
}
} }
using var timeoutToken = new CancellationTokenSource(Utilities.DefaultCommandTimeout); using var timeoutToken = new CancellationTokenSource(Utilities.DefaultCommandTimeout);
@ -36,9 +44,10 @@ public static class EventExtensions
{ {
await handler(eventArgType, tokenSource.Token); await handler(eventArgType, tokenSource.Token);
} }
catch (Exception) catch (Exception ex)
{ {
// ignored // todo: static logger
Console.WriteLine("InvokeAsync: " + ex);
} }
} }
} }

View File

@ -4,5 +4,6 @@ public class PlayerRoundDataGameEvent : ClientGameEvent
{ {
public int TotalScore { get; init; } public int TotalScore { get; init; }
public int CurrentScore { get; init; } public int CurrentScore { get; init; }
public int CurrentRound { get; init; }
public bool IsGameOver { get; init; } public bool IsGameOver { get; init; }
} }

View File

@ -1,6 +1,6 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie; namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class RoundCompleteGameEvent : GameEventV2 public class RoundEndEvent : GameEventV2
{ {
public int RoundNumber { get; init; } public int RoundNumber { get; init; }
} }

View File

@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Game; using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Events.Game.GameScript.Zombie;
namespace SharedLibraryCore.Interfaces.Events; namespace SharedLibraryCore.Interfaces.Events;
@ -22,6 +23,12 @@ public interface IGameEventSubscriptions
/// </summary> /// </summary>
static event Func<MatchEndEvent, CancellationToken, Task> MatchEnded; static event Func<MatchEndEvent, CancellationToken, Task> MatchEnded;
/// <summary>
/// Raised when game log prints round ended
/// <remarks>typically only triggered when using a script integration</remarks>
/// </summary>
static event Func<RoundEndEvent, CancellationToken, Task> RoundEnded;
/// <summary> /// <summary>
/// Raised when game log printed that client has entered the match /// Raised when game log printed that client has entered the match
/// <remarks>J;clientNetworkId;clientSlotNumber;clientName</remarks> /// <remarks>J;clientNetworkId;clientSlotNumber;clientName</remarks>
@ -97,6 +104,7 @@ public interface IGameEventSubscriptions
{ {
MatchStartEvent matchStartEvent => MatchStarted?.InvokeAsync(matchStartEvent, token) ?? Task.CompletedTask, MatchStartEvent matchStartEvent => MatchStarted?.InvokeAsync(matchStartEvent, token) ?? Task.CompletedTask,
MatchEndEvent matchEndEvent => MatchEnded?.InvokeAsync(matchEndEvent, token) ?? Task.CompletedTask, MatchEndEvent matchEndEvent => MatchEnded?.InvokeAsync(matchEndEvent, token) ?? Task.CompletedTask,
RoundEndEvent roundEndEvent => RoundEnded?.InvokeAsync(roundEndEvent, token) ?? Task.CompletedTask,
ClientEnterMatchEvent clientEnterMatchEvent => ClientEnteredMatch?.InvokeAsync(clientEnterMatchEvent, token) ?? Task.CompletedTask, ClientEnterMatchEvent clientEnterMatchEvent => ClientEnteredMatch?.InvokeAsync(clientEnterMatchEvent, token) ?? Task.CompletedTask,
ClientExitMatchEvent clientExitMatchEvent => ClientExitedMatch?.InvokeAsync(clientExitMatchEvent, token) ?? Task.CompletedTask, ClientExitMatchEvent clientExitMatchEvent => ClientExitedMatch?.InvokeAsync(clientExitMatchEvent, token) ?? Task.CompletedTask,
ClientJoinTeamEvent clientJoinTeamEvent => ClientJoinedTeam?.InvokeAsync(clientJoinTeamEvent, token) ?? Task.CompletedTask, ClientJoinTeamEvent clientJoinTeamEvent => ClientJoinedTeam?.InvokeAsync(clientJoinTeamEvent, token) ?? Task.CompletedTask,

View File

@ -4,6 +4,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
@ -113,5 +114,6 @@ namespace SharedLibraryCore.Interfaces
IAlertManager AlertManager { get; } IAlertManager AlertManager { get; }
IInteractionRegistration InteractionRegistration { get; } IInteractionRegistration InteractionRegistration { get; }
IList<Func<Dictionary<int, List<EFMeta>>, long?, string, bool, Task>> CustomStatsMetrics { get; }
} }
} }

View File

@ -83,6 +83,7 @@ namespace SharedLibraryCore
RConConnectionFactory = rconConnectionFactory; RConConnectionFactory = rconConnectionFactory;
ServerLogger = logger; ServerLogger = logger;
DefaultSettings = serviceProvider.GetRequiredService<DefaultSettings>(); DefaultSettings = serviceProvider.GetRequiredService<DefaultSettings>();
PerformanceBucket = ServerConfig.PerformanceBucket;
InitializeTokens(); InitializeTokens();
InitializeAutoMessages(); InitializeAutoMessages();
} }
@ -163,6 +164,7 @@ namespace SharedLibraryCore
public bool IsInitialized { get; set; } public bool IsInitialized { get; set; }
public int Port { get; protected set; } public int Port { get; protected set; }
public int ListenPort => Port; public int ListenPort => Port;
public string PerformanceBucket { get; init; }
public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty); public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty);
public abstract Task<string[]> ExecuteCommandAsync(string command, CancellationToken token = default); public abstract Task<string[]> ExecuteCommandAsync(string command, CancellationToken token = default);
public abstract Task SetDvarAsync(string name, object value, CancellationToken token = default); public abstract Task SetDvarAsync(string name, object value, CancellationToken token = default);

View File

@ -40,7 +40,8 @@ namespace SharedLibraryCore.Services
AliasLink = client.AliasLink, AliasLink = client.AliasLink,
Password = client.Password, Password = client.Password,
PasswordSalt = client.PasswordSalt, PasswordSalt = client.PasswordSalt,
GameName = client.GameName GameName = client.GameName,
CurrentAliasId = client.CurrentAliasId
}) })
.FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game) .FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
); );
@ -595,7 +596,6 @@ namespace SharedLibraryCore.Services
entity.CurrentAlias = newAlias; entity.CurrentAlias = newAlias;
await context.SaveChangesAsync(); await context.SaveChangesAsync();
entity.CurrentAliasId = newAlias.AliasId;
} }
/// <summary> /// <summary>

View File

@ -46,7 +46,7 @@ namespace SharedLibraryCore
public static Encoding EncodingType; public static Encoding EncodingType;
public static Layout CurrentLocalization = new Layout(new Dictionary<string, string>()); public static Layout CurrentLocalization = new Layout(new Dictionary<string, string>());
public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, /*Utilities.IsDevelopment ? 360 : */25); public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, Utilities.IsDevelopment ? 360 : 25);
public static char[] DirectorySeparatorChars = { '\\', '/' }; public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!'; public static char CommandPrefix { get; set; } = '!';
@ -1224,6 +1224,16 @@ namespace SharedLibraryCore
} }
public static string ToNumericalString(this int value) public static string ToNumericalString(this int value)
{
return ToNumericalString((long)value);
}
public static string ToNumericalString(this long? value)
{
return value?.ToNumericalString();
}
public static string ToNumericalString(this long value)
{ {
return value.ToString("#,##0", CurrentLocalization.Culture); return value.ToString("#,##0", CurrentLocalization.Culture);
} }

View File

@ -1,6 +1,8 @@
using System.Linq; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
@ -48,6 +50,11 @@ namespace WebfrontCore.Controllers
matchedServerId = server.LegacyDatabaseId; matchedServerId = server.LegacyDatabaseId;
} }
foreach (var statMetricFunc in Manager.CustomStatsMetrics)
{
await statMetricFunc(new Dictionary<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false);
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token); hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo); return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);

View File

@ -21,7 +21,7 @@
} }
</td> </td>
<td colspan="20%" class="text-break"> <td colspan="20%" class="text-break">
<color-code value="@(message.ServerName ?? "--")"></color-code> <color-code value="@(message.ServerName ?? "-")"></color-code>
</td> </td>
<td colspan="15%" class="text-right text-break"> <td colspan="15%" class="text-right text-break">
@message.When.ToStandardFormat() @message.When.ToStandardFormat()
@ -51,7 +51,7 @@
} }
</div> </div>
<div> <div>
<color-code value="@(message.ServerName ?? "--")"></color-code> <color-code value="@(message.ServerName ?? "-")"></color-code>
</div> </div>
<div>@message.When.ToStandardFormat()</div> <div>@message.When.ToStandardFormat()</div>
</td> </td>

View File

@ -31,9 +31,15 @@
} }
var rebuiltName = stat.RebuildWeaponName(); var rebuiltName = stat.RebuildWeaponName();
var name = config.GetStringForGame(rebuiltName, stat.Weapon?.Game); var name = config.GetStringForGame(rebuiltName, stat.Weapon?.Game);
return !rebuiltName.Equals(name, StringComparison.InvariantCultureIgnoreCase)
? name if (!rebuiltName.Equals(name))
: config.GetStringForGame(stat.Weapon.Name, stat.Weapon.Game); {
return name;
}
rebuiltName = config.GetStringForGame(stat.Weapon?.Name, stat.Weapon.Game);
return rebuiltName.Equals(name) ? name.Transform(To.TitleCase) : rebuiltName;
} }
string GetWeaponAttachmentName(EFWeaponAttachmentCombo attachment) string GetWeaponAttachmentName(EFWeaponAttachmentCombo attachment)
@ -230,7 +236,11 @@
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_ACTIVE_TIME"] as string, Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_ACTIVE_TIME"] as string,
Value = activeTime?.HumanizeForCurrentCulture() Value = activeTime?.HumanizeForCurrentCulture()
} }
}; }.Concat(Model.CustomMetrics.Select(metric => new
{
Name = metric.Key,
metric.Value
}));
} }
<div class="content row mt-20"> <div class="content row mt-20">
@ -362,7 +372,7 @@
}).WithRows(weapons, weapon => new[] }).WithRows(weapons, weapon => new[]
{ {
GetWeaponNameForHit(weapon), GetWeaponNameForHit(weapon),
GetWeaponAttachmentName(weapon.WeaponAttachmentCombo) ?? "--", GetWeaponAttachmentName(weapon.WeaponAttachmentCombo) ?? "-",
weapon.KillCount.ToNumericalString(), weapon.KillCount.ToNumericalString(),
weapon.HitCount.ToNumericalString(), weapon.HitCount.ToNumericalString(),
weapon.DamageInflicted.ToNumericalString(), weapon.DamageInflicted.ToNumericalString(),

View File

@ -67,21 +67,13 @@
</div> </div>
<div class="d-flex flex-column font-size-12 text-right text-md-left"> <div class="d-flex flex-column font-size-12 text-right text-md-left">
<div> @foreach (var meta in stat.Metrics)
<span class="text-primary">@stat.Kills.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span> {
</div> <div>
<div> <span class="text-primary">@meta.Value</span>
<span class="text-primary">@stat.Deaths.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br/> <span class="text-muted">@meta.Key</span>
</div> </div>
<div> }
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
</div>
<div>
<span class="text-primary">@stat.TimePlayedValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span>
</div>
<div>
<span class="text-primary"> @stat.LastSeenValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span>
</div>
</div> </div>
</div> </div>
<div class="w-full w-md-half client-rating-graph pt-10 pb-10"> <div class="w-full w-md-half client-rating-graph pt-10 pb-10">

View File

@ -69,7 +69,7 @@ else
<td>@(client.Deaths ?? 0)</td> <td>@(client.Deaths ?? 0)</td>
<td>@Math.Round(client.Kdr ?? 0, 2)</td> <td>@Math.Round(client.Kdr ?? 0, 2)</td>
<td>@Math.Round(client.ScorePerMinute ?? 0)</td> <td>@Math.Round(client.ScorePerMinute ?? 0)</td>
<td>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</td> <td>@(client.ZScore is null or 0 ? "-" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</td>
<td class="text-right">@client.Ping</td> <td class="text-right">@client.Ping</td>
</tr> </tr>
@ -94,7 +94,7 @@ else
<div>@(client.Deaths ?? 0)</div> <div>@(client.Deaths ?? 0)</div>
<div>@Math.Round(client.Kdr ?? 0, 2)</div> <div>@Math.Round(client.Kdr ?? 0, 2)</div>
<div>@Math.Round(client.ScorePerMinute ?? 0)</div> <div>@Math.Round(client.ScorePerMinute ?? 0)</div>
<div>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</div> <div>@(client.ZScore is null or 0 ? "-" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</div>
<div>@client.Ping</div> <div>@client.Ping</div>
</td> </td>
</tr> </tr>