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 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)
{
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
@ -249,7 +252,13 @@ namespace IW4MAdmin.Application
{
var thisIndex = 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();
var eventHandlerThread = new Thread(() =>
var eventHandlerTask = Task.Run(() =>
{
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
})
{
Name = nameof(CoreEventHandler)
};
}, _eventHandlerTokenSource.Token);
eventHandlerThread.Start();
await UpdateServerStates();
_eventHandlerTokenSource.Cancel();
eventHandlerThread.Join();
try
{
await eventHandlerTask;
}
catch (OperationCanceledException)
{
}
}
public async Task Stop()

View File

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

View File

@ -2439,6 +2439,7 @@
"left_foot": "Left Foot",
"left_arm_upper": "Upper Left Arm",
"left_arm_lower": "Lower Left Arm",
"head": "Head",
"gl": "Rifle Grenade",
"bigammo": "Round Drum",
"scoped": "Sniper Scope",
@ -2446,34 +2447,238 @@
"aperture": "Aperture Sight",
"flash": "Flash Hider",
"silenced": "Silencer",
"molotov": "Molotov Cocktail",
"sticky": "N° 74 ST",
"m2": "M2 Flamethrower",
"artillery": "Artillery Strike",
"dog": "Attack Dogs",
"colt": "Colt M1911",
"357magnum": ".357 Magnum",
"sw_357": ".357 Magnum",
"walther": "Walther P38",
"tokarev": "Tokarev TT-33",
"shotgun": "M1897 Trench Gun",
"doublebarreledshotgun": "Double-Barreled Shotgun",
"mp40": "MP40",
"type100smg": "Type 100",
"ppsh": "PPSh-41",
"svt40": "SVT-40",
"gewehr43": "Gewehr 43",
"doublebarrel": "Double-Barreled Shotgun",
"30cal": "Browning M1919",
"bar": "BAR",
"fg42": "FG42",
"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",
"m1carbine": "M1A1 Carbine",
"type99lmg": "Type 99",
"bar": "BAR",
"dp28": "DP-28",
"mg42": "MG42",
"fg42": "FG42",
"30cal": "Browning M1919",
"type99rifle": "Arisaka",
"mosinrifle": "Mosin-Nagant",
"ptrs41": "PTRS-41"
"syrette": "Syrette",
"supportgunner": "Support Gunner",
"bren": "Bren LMG",
"rifleman": "Rifleman",
"lee_enfield": "Lee-Enfield",
"kar98k": "Kar98k",
"luger": "Luger",
"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" : {

View File

@ -98,10 +98,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
var foundClient = true;
// first time client is connecting to server
if (client == null)
{
foundClient = false;
ServerLogger.LogDebug("Client {client} first time connecting", clientFromLog.ToString());
clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog);
@ -109,12 +111,16 @@ namespace IW4MAdmin
client.CopyAdditionalProperties(clientFromLog);
// this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
if (foundClient)
{
client.CurrentAlias = new EFAlias
{
AliasId = client.CurrentAliasId,
LinkId = client.AliasLinkId,
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
}
// Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber;
@ -413,10 +419,7 @@ namespace IW4MAdmin
{
if (E.Origin.State != ClientState.Connected)
{
E.Origin.State = ClientState.Connected;
E.Origin.Connections += 1;
ChatHistory.Add(new ChatInfo()
ChatHistory.Add(new ChatInfo
{
Name = E.Origin.Name,
Message = "CONNECTED",
@ -431,6 +434,10 @@ namespace IW4MAdmin
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
{
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());
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;
}
if (gameServer.PerformanceBucket != PerformanceBucket)
{
gameServer.PerformanceBucket = PerformanceBucket;
context.Entry(gameServer).Property(property => property.PerformanceBucket).IsModified = true;
}
if (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<ZombieAggregateClientStat> ZombieClientStatAggregates { get; set; }
public DbSet<ZombieClientStatRecord> ZombieClientStatRecords { get; set; }
public DbSet<ZombieEventLog> ZombieEvents { get; set; }
#endregion
@ -102,6 +103,19 @@ namespace Data.Context
client.NetworkId,
client.GameName
});
/* entity.HasMany(prop => prop.ZombieMatchClientStats)
.WithOne(prop => prop.Client)
.HasForeignKey(prop => prop.ClientId);
entity.HasMany(prop => prop.ZombieRoundClientStats)
.WithOne(prop => prop.Client)
.HasForeignKey(prop => prop.ClientId);
entity.HasMany(prop => prop.ZombieAggregateClientStats)
.WithOne(prop => prop.Client)
.HasForeignKey(prop => prop.ClientId);*/
});
modelBuilder.Entity<EFPenalty>(entity =>
@ -171,12 +185,34 @@ namespace Data.Context
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
modelBuilder.Entity(typeof(ZombieMatch)).ToTable($"EF{nameof(ZombieMatch)}");
modelBuilder.Entity(typeof(ZombieMatchClientStat)).ToTable($"EF{nameof(ZombieMatchClientStat)}");
modelBuilder.Entity(typeof(ZombieRoundClientStat)).ToTable($"EF{nameof(ZombieRoundClientStat)}");
modelBuilder.Entity(typeof(ZombieAggregateClientStat)).ToTable($"EF{nameof(ZombieAggregateClientStat)}");
modelBuilder.Entity(typeof(ZombieClientStat)).ToTable($"EF{nameof(ZombieClientStat)}");
modelBuilder.Entity(typeof(ZombieClientStatRecord)).ToTable($"EF{nameof(ZombieClientStatRecord)}");
modelBuilder.Entity<ZombieMatch>().ToTable($"EF{nameof(ZombieMatch)}");
modelBuilder.Entity<ZombieClientStat>(ent =>
{
ent.ToTable($"EF{nameof(ZombieClientStat)}");
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);

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
modelBuilder
.HasAnnotation("ProductVersion", "6.0.1")
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -459,6 +459,9 @@ namespace Data.Migrations.Postgresql
b.Property<bool>("Newest")
.HasColumnType("boolean");
b.Property<string>("PerformanceBucket")
.HasColumnType("text");
b.Property<double?>("PerformanceMetric")
.HasColumnType("double precision");
@ -1125,6 +1128,9 @@ namespace Data.Migrations.Postgresql
b.Property<bool>("IsPasswordProtected")
.HasColumnType("boolean");
b.Property<string>("PerformanceBucket")
.HasColumnType("text");
b.Property<int>("Port")
.HasColumnType("integer");
@ -1222,6 +1228,278 @@ namespace Data.Migrations.Postgresql
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 =>
{
b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot")
@ -1663,6 +1941,107 @@ namespace Data.Migrations.Postgresql
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany("ZombieClientStats")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany()
.HasForeignKey("MatchId");
b.Navigation("Client");
b.Navigation("Match");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId");
b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round")
.WithMany()
.HasForeignKey("RoundId");
b.Navigation("Client");
b.Navigation("Round");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b =>
{
b.HasOne("Data.Models.Client.EFClient", "AssociatedClient")
.WithMany()
.HasForeignKey("AssociatedClientId");
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany()
.HasForeignKey("MatchId");
b.HasOne("Data.Models.Client.EFClient", "SourceClient")
.WithMany()
.HasForeignKey("SourceClientId");
b.Navigation("AssociatedClient");
b.Navigation("Match");
b.Navigation("SourceClient");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId");
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Map");
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClient", b =>
{
b.Navigation("AdministeredPenalties");
@ -1670,6 +2049,8 @@ namespace Data.Migrations.Postgresql
b.Navigation("Meta");
b.Navigation("ReceivedPenalties");
b.Navigation("ZombieClientStats");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>

View File

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

View File

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

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)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b =>
{
@ -1181,7 +1181,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int>("DamageDealt")
b.Property<long>("DamageDealt")
.HasColumnType("INTEGER");
b.Property<int>("DamageReceived")
@ -1193,6 +1193,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("Downs")
.HasColumnType("INTEGER");
b.Property<int>("HeadshotKills")
.HasColumnType("INTEGER");
b.Property<int>("Headshots")
.HasColumnType("INTEGER");
@ -1230,6 +1233,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("MatchId");
b.ToTable("EFZombieClientStat", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
@ -1271,6 +1276,47 @@ namespace Data.Migrations.Sqlite
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 =>
{
b.Property<int>("ZombieMatchId")
@ -1283,9 +1329,6 @@ namespace Data.Migrations.Sqlite
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<int?>("MapId")
.HasColumnType("INTEGER");
@ -1303,8 +1346,6 @@ namespace Data.Migrations.Sqlite
b.HasKey("ZombieMatchId");
b.HasIndex("EFClientClientId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
@ -1337,15 +1378,15 @@ namespace Data.Migrations.Sqlite
b.Property<double>("AverageRoundReached")
.HasColumnType("REAL");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<double>("HeadshotPercentage")
.HasColumnType("REAL");
b.Property<int>("HighestRound")
.HasColumnType("INTEGER");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesCompleted")
.HasColumnType("INTEGER");
@ -1355,7 +1396,7 @@ namespace Data.Migrations.Sqlite
b.Property<int>("TotalRoundsPlayed")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.HasIndex("ServerId");
b.ToTable("EFZombieAggregateClientStat", (string)null);
});
@ -1364,11 +1405,6 @@ namespace Data.Migrations.Sqlite
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieMatchClientStat", (string)null);
});
@ -1379,9 +1415,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("Duration")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("TEXT");
@ -1397,8 +1430,6 @@ namespace Data.Migrations.Sqlite
b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("TEXT");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieRoundClientStat", (string)null);
});
@ -1852,7 +1883,7 @@ namespace Data.Migrations.Sqlite
.IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany("ClientStats")
.WithMany()
.HasForeignKey("MatchId");
b.Navigation("Client");
@ -1875,12 +1906,29 @@ namespace Data.Migrations.Sqlite
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.EFClient", null)
.WithMany("ZombieMatches")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId");
@ -1896,23 +1944,21 @@ namespace Data.Migrations.Sqlite
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieAggregateClientStats")
.HasForeignKey("EFClientClientId");
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.Client.EFClient", null)
.WithMany("ZombieMatchClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
@ -1922,10 +1968,6 @@ namespace Data.Migrations.Sqlite
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)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
@ -1941,15 +1983,7 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties");
b.Navigation("ZombieAggregateClientStats");
b.Navigation("ZombieClientStats");
b.Navigation("ZombieMatchClientStats");
b.Navigation("ZombieMatches");
b.Navigation("ZombieRoundClientStats");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>
@ -1973,11 +2007,6 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Navigation("ClientStats");
});
#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<EFPenalty> ReceivedPenalties { 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<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;
public class DatedRecord
public class DatedRecord : IdentifierRecord
{
public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow;
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.Linq;
using Data.Models.Server;
namespace Data.Models.Zombie;
public class ZombieAggregateClientStat : ZombieClientStat
{
public long? ServerId { get; set; }
[ForeignKey(nameof(ServerId))]
public EFServer Server { get; set; }
#region Average
public double AverageKillsPerDown { get; set; }
@ -41,4 +47,8 @@ public class ZombieAggregateClientStat : ZombieClientStat
nameof(TotalRoundsPlayed),
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]
public long ZombieClientStatId { get; set; }
[NotMapped] public override long Id => ZombieClientStatId;
public int? MatchId { get; set; }
[ForeignKey(nameof(MatchId))]
@ -17,13 +19,14 @@ public abstract class ZombieClientStat : DatedRecord
public int ClientId { get; set; }
[ForeignKey(nameof(ClientId))]
public virtual EFClient? Client { get; set; }
public virtual EFClient Client { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public int DamageDealt { get; set; }
public long DamageDealt { get; set; }
public int DamageReceived { get; set; }
public int Headshots { get; set; }
public int HeadshotKills { get; set; }
public int Melees { get; set; }
public int Downs { get; set; }
public int Revives { get; set; }

View File

@ -15,6 +15,9 @@ public class ZombieClientStatRecord : DatedRecord
{
[Key]
public int ZombieClientStatRecordId { get; set; }
[NotMapped] public override long Id => ZombieClientStatRecordId;
public string Name { get; set; } = string.Empty;
public string Type { 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
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client.Stats.Reference;
@ -13,6 +12,8 @@ public class ZombieMatch : DatedRecord
[Key]
public int ZombieMatchId { get; set; }
[NotMapped] public override long Id => ZombieMatchId;
public int? MapId { get; set; }
[ForeignKey(nameof(MapId))]
public virtual EFMap? Map { get; set; }
@ -23,8 +24,6 @@ public class ZombieMatch : DatedRecord
public int ClientsCompleted { get; set; }
public virtual ICollection<ZombieClientStat>? ClientStats { get; set; }
public DateTimeOffset MatchStartDate { get; set; } = DateTimeOffset.UtcNow;
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
{
[NotMapped] public int? JoinedRound { get; set; }
}

View File

@ -57,7 +57,7 @@ namespace Stats.Client
var iqPerformances = set
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.Where(s => s.EloRating >= 0)
.Where(s => s.Client.Level != EFClient.Permission.Banned);
foreach (var serverId in _serverIds)
@ -71,30 +71,33 @@ namespace Stats.Client
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;
}
var performanceBucket = performanceBucketGroup.Key;
var bucketConfig =
_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
.Where(perf => perf.Server.PerformanceBucket == server.PerformanceBucket)
.Where(perf => perf.Server.PerformanceBucket == performanceBucket)
.Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
.Where(perf => perf.UpdatedAt >= oldestPerf)
.Where(perf => perf.Skill < 999999)
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
.ToListAsync(token);
var distributionParams = performances.GenerateDistributionParameters();
distributions.Add(server.PerformanceBucket, distributionParams);
distributions.Add(performanceBucket, distributionParams);
}
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)
{
@ -117,7 +120,7 @@ namespace Stats.Client
var zScore = await set
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.Where(s => s.EloRating >= 1)
.Where(stat =>
performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket)
.GroupBy(stat => stat.ClientId)
@ -127,7 +130,7 @@ namespace Stats.Client
return zScore ?? 0;
}, 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 });
}
@ -199,6 +202,8 @@ namespace Stats.Client
return 0.0;
}
value = Math.Max(1, value);
var zScore = (Math.Log(value) - sdParams.Mean) / sdParams.Sigma;
return zScore;
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using SharedLibraryCore.Dtos;
@ -25,5 +26,6 @@ namespace Stats.Dtos
public List<EFClientHitStatistic> ByAttachmentCombo { get; set; }
public List<EFClientRankingHistory> Ratings { 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 System;
using System.Collections.Generic;
using System.Text;
using Data.Models;
namespace IW4MAdmin.Plugins.Stats.Web.Dtos
{
@ -22,6 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public List<PerformanceHistory> PerformanceHistory { get; set; }
public double? ZScore { get; set; }
public long? ServerId { get; set; }
public List<EFMeta> Metrics { get; } = new();
}
public class PerformanceHistory

View File

@ -110,25 +110,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0;
}
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
long? serverId = null)
private Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null)
{
return (ranking) => ranking.ServerId == serverId
var oldestDate = DateTime.UtcNow - oldestStat;
return ranking => ranking.ServerId == serverId
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
&& ranking.CreatedDateTime >= Extensions.FifteenDaysAgo()
&& ranking.CreatedDateTime >= oldestDate
&& ranking.ZScore != null
&& ranking.PerformanceMetric != null
&& ranking.Newest
&& ranking.Client.TotalConnectionTime >=
_config.TopPlayersMinPlayTime;
&& ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds;
}
public async Task<int> GetTotalRankedPlayers(long serverId)
{
var bucketConfig = await GetBucketConfig(serverId);
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.Set<EFClientRankingHistory>()
.Where(GetNewRankingFunc(serverId: serverId))
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
.CountAsync();
}
@ -143,12 +144,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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>()
.Where(GetNewRankingFunc(serverId: serverId))
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
.OrderByDescending(ranking => ranking.PerformanceMetric)
.Select(ranking => ranking.ClientId)
.Skip(start)
@ -233,9 +235,77 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.OrderBy(r => r.Ranking)
.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;
}
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)
{
if (_config.EnableAdvancedMetrics)
@ -1179,54 +1249,41 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId)
{
var bucketConfig = await GetBucketConfig(serverId);
await using var context = _contextFactory.CreateContext();
var minPlayTime = _config.TopPlayersMinPlayTime;
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 oldestStateDate = DateTime.UtcNow - bucketConfig.RankingExpiration;
var performances = await context.Set<EFClientStatistics>()
.AsNoTracking()
.Include(stat => stat.Server)
.Where(stat => stat.ClientId == clientId)
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
.Where(stats => stats.UpdatedAt >= oldestStateDate)
.Where(stats => stats.TimePlayed >= minPlayTime)
.Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)
.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);
}
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
if (performances.Any(performance => performance.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds))
{
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, minPlayTime,
oldestStat, performanceBucket);
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, bucketConfig);
}
}
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances,
int minPlayTime, TimeSpan oldestStat, string performanceBucket)
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, PerformanceBucketConfiguration bucketConfig)
{
var aggregateZScore =
performances.Where(performance => performance.Server.PerformanceBucket == performanceBucket)
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
performances.Where(performance => performance.Server.PerformanceBucket == bucketConfig.Name)
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds);
int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat))
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
.GroupBy(stat => stat.ClientId)
.Where(group =>
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)
.CountAsync();
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, performanceBucket);
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, bucketConfig.Name);
if (newPerformanceMetric == null)
{
@ -1249,13 +1306,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ZScore = aggregateZScore,
Ranking = aggregateRanking,
PerformanceMetric = newPerformanceMetric,
PerformanceBucket = performanceBucket,
PerformanceBucket = bucketConfig.Name,
Newest = true,
};
context.Add(aggregateRankingSnapshot);
await PruneOldRankings(context, clientId);
await PruneOldRankings(context, clientId, performanceBucket: bucketConfig.Name);
await context.SaveChangesAsync();
}
@ -1364,6 +1421,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.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
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.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
: 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
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);
// 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}",
new

View File

@ -1,10 +1,20 @@
using Data.Models.Client.Stats;
using System.Linq;
using Data.Models.Client.Stats;
namespace Stats.Helpers
{
public static class WeaponNameExtensions
{
public static string RebuildWeaponName(this EFClientHitStatistic stat) =>
$"{stat.Weapon?.Name}{string.Join("_", stat.WeaponAttachmentCombo?.Attachment1?.Name, stat.WeaponAttachmentCombo?.Attachment2?.Name, stat.WeaponAttachmentCombo?.Attachment3?.Name)}";
public static string RebuildWeaponName(this EFClientHitStatistic stat)
{
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.RoundEnded += (roundEndedEvent, token) => _statManager.Sync(roundEndedEvent.Server, token);
IGameEventSubscriptions.MatchStarted += OnMatchEvent;
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
IGameEventSubscriptions.ClientKilled += OnClientKilled;

View File

@ -1,21 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>IW4MAdmin.Plugins.ZombieStats</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.16">
<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="..\..\Data\Data.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>

View File

@ -15,11 +15,11 @@ namespace SharedLibraryCore.Configuration
if (!ContainsKey(game.Value))
{
return key.Transform(To.TitleCase);
return key;
}
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

@ -23,10 +23,18 @@ public static class EventExtensions
TEventType eventArgType, CancellationToken token)
{
if (token == CancellationToken.None)
{
try
{
// 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 tokenSource =
@ -36,9 +44,10 @@ public static class EventExtensions
{
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 CurrentScore { get; init; }
public int CurrentRound { get; init; }
public bool IsGameOver { get; init; }
}

View File

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

View File

@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Events.Game.GameScript.Zombie;
namespace SharedLibraryCore.Interfaces.Events;
@ -22,6 +23,12 @@ public interface IGameEventSubscriptions
/// </summary>
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>
/// Raised when game log printed that client has entered the match
/// <remarks>J;clientNetworkId;clientSlotNumber;clientName</remarks>
@ -97,6 +104,7 @@ public interface IGameEventSubscriptions
{
MatchStartEvent matchStartEvent => MatchStarted?.InvokeAsync(matchStartEvent, 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,
ClientExitMatchEvent clientExitMatchEvent => ClientExitedMatch?.InvokeAsync(clientExitMatchEvent, 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.Threading;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
@ -113,5 +114,6 @@ namespace SharedLibraryCore.Interfaces
IAlertManager AlertManager { 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;
ServerLogger = logger;
DefaultSettings = serviceProvider.GetRequiredService<DefaultSettings>();
PerformanceBucket = ServerConfig.PerformanceBucket;
InitializeTokens();
InitializeAutoMessages();
}
@ -163,6 +164,7 @@ namespace SharedLibraryCore
public bool IsInitialized { get; set; }
public int Port { get; protected set; }
public int ListenPort => Port;
public string PerformanceBucket { get; init; }
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 SetDvarAsync(string name, object value, CancellationToken token = default);

View File

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

View File

@ -46,7 +46,7 @@ namespace SharedLibraryCore
public static Encoding EncodingType;
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 CommandPrefix { get; set; } = '!';
@ -1224,6 +1224,16 @@ namespace SharedLibraryCore
}
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);
}

View File

@ -1,6 +1,8 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@ -48,6 +50,11 @@ namespace WebfrontCore.Controllers
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);
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);

View File

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

View File

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

View File

@ -67,21 +67,13 @@
</div>
<div class="d-flex flex-column font-size-12 text-right text-md-left">
@foreach (var meta in stat.Metrics)
{
<div>
<span class="text-primary">@stat.Kills.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
</div>
<div>
<span class="text-primary">@stat.Deaths.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br/>
</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>
<span class="text-primary">@meta.Value</span>
<span class="text-muted">@meta.Key</span>
</div>
}
</div>
</div>
<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>@Math.Round(client.Kdr ?? 0, 2)</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>
</tr>
@ -94,7 +94,7 @@ else
<div>@(client.Deaths ?? 0)</div>
<div>@Math.Round(client.Kdr ?? 0, 2)</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>
</td>
</tr>