diff --git a/.github/workflows/build_application.yml b/.github/workflows/build_application.yml index 635150033..94f783fd1 100644 --- a/.github/workflows/build_application.yml +++ b/.github/workflows/build_application.yml @@ -2,7 +2,7 @@ name: Application build on: push: - branches: [ develop, release/pre, master ] + branches: [ develop, release/pre, master, feature/zombie-stats ] paths: - Application/** - WebfrontCore/** @@ -217,6 +217,9 @@ jobs: mkdir -p ${{ env.outputFolder }}/wwwroot/js/user cp ${{ github.workspace }}/WebfrontCore/wwwroot/js/app.min.js ${{ env.outputFolder }}/wwwroot/js/app.min.js cp ${{ github.workspace }}/WebfrontCore/wwwroot/js/user/user.js ${{ env.outputFolder }}/wwwroot/js/user/user.js + # zombie-scrubber.js is loaded standalone (kept out of the main bundle + # for cache isolation — UI changes ship without invalidating app.min.js). + cp ${{ github.workspace }}/WebfrontCore/wwwroot/js/zombie-scrubber.js ${{ env.outputFolder }}/wwwroot/js/zombie-scrubber.js mkdir -p ${{ env.outputFolder }}/wwwroot/font mkdir -p ${{ env.outputFolder }}/wwwroot/font/phosphor rsync -ar ${{ github.workspace }}/WebfrontCore/wwwroot/font/phosphor/ ${{ env.outputFolder }}/wwwroot/font/phosphor/ @@ -235,7 +238,7 @@ jobs: build_and_push_docker_dev: runs-on: ubuntu-latest needs: [ make_version, build ] - if: ${{ github.ref == 'refs/heads/develop' }} + if: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/feature/zombie-stats' }} permissions: contents: read packages: write @@ -270,10 +273,10 @@ jobs: with: images: ghcr.io/${{ github.repository_owner }}/iw4madmin tags: | - # push to develop branch: tags as 'develop' - type=raw,value=develop - # tag with the build number and -dev suffix (e.g., 2025.8.13.1-dev) - type=raw,value=${{ env.buildNumber }}-dev + # branch name as tag (e.g., 'develop' or 'zombie-stats') + type=ref,event=branch + # tag with the build number and branch suffix (e.g., 2025.8.13.1-dev) + type=raw,value=${{ env.buildNumber }}-${{ github.ref_name == 'develop' && 'dev' || github.ref_name }} # tag with the short git sha (e.g., f15fbd3) type=sha,prefix= labels: | diff --git a/.gitignore b/.gitignore index 08216d4eb..b991fe5dc 100644 --- a/.gitignore +++ b/.gitignore @@ -250,6 +250,8 @@ Application/WebfrontCore/* /Data/IW4MAdmin_Migration.db-wal bundle/ WebfrontCore/Tools/* +_PRIVATE/ +translation_temp.csv # LLM folders .claude/ diff --git a/Application/Application.csproj b/Application/Application.csproj index 0ce040cfa..f533f8036 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -4,7 +4,7 @@ Exe net10.0 RaidMax.IW4MAdmin.Application - 2020.0.0.0 + 2024.0.0.0 RaidMax Forever None IW4MAdmin diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 4c58f4af1..148698488 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -142,6 +142,12 @@ public ApplicationManager(ILogger logger, IMiddlewareActionH public IEnumerable Plugins { get; } public IInteractionRegistration InteractionRegistration { get; } + public IList>, long?, string, bool, Task>> CustomStatsMetrics { get; } = + new List>, long?, string, bool, Task>>(); + + public IList, long?, string, Task>> CustomTopStatsTransformers { get; } = + new List, long?, string, Task>>(); + public async Task ExecuteEvent(GameEvent newEvent) { ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent); diff --git a/Application/BuildScripts/PostBuild.sh b/Application/BuildScripts/PostBuild.sh index d6a96ebaa..bfdad22a5 100644 --- a/Application/BuildScripts/PostBuild.sh +++ b/Application/BuildScripts/PostBuild.sh @@ -32,12 +32,15 @@ mv "$PublishDir/DefaultSettings.json" "$PublishDir/Configuration/" mkdir -p "$PublishDir/Lib" rm -f "$PublishDir/Microsoft.CodeAnalysis*.dll" -# Get list of plugin DLLs from BUILD/Plugins (dynamically detected) +# Get list of plugin DLLs by matching .csproj project names in the Plugins/ source directory. +# This avoids treating transitive dependencies (e.g. EF Core, Serilog) that land in BUILD/Plugins +# as plugin assemblies when plugins use ProjectReferences instead of PackageReferences. pluginDllNames=() -if [ -d "$SourceDir/BUILD/Plugins" ]; then - for pluginDll in "$SourceDir/BUILD/Plugins"/*.dll; do - if [ -f "$pluginDll" ]; then - pluginDllNames+=("$(basename "$pluginDll")") +if [ -d "$SourceDir/Plugins" ]; then + for csproj in "$SourceDir/Plugins"/*/*.csproj; do + if [ -f "$csproj" ]; then + projName=$(basename "$csproj" .csproj) + pluginDllNames+=("${projName}.dll") fi done fi diff --git a/Application/Commands/SetLogLevelCommand.cs b/Application/Commands/SetLogLevelCommand.cs index 250b139fe..720371f4c 100644 --- a/Application/Commands/SetLogLevelCommand.cs +++ b/Application/Commands/SetLogLevelCommand.cs @@ -20,7 +20,7 @@ public SetLogLevelCommand(CommandConfiguration config, ITranslationLookup layout Name = "loglevel"; Alias = "ll"; - Description = "set minimum logging level"; + Description = layout["COMMANDS_LOGLEVEL_DESC"]; Permission = EFClient.Permission.Owner; Arguments = new CommandArgument[] { @@ -50,7 +50,8 @@ public override async Task ExecuteAsync(GameEvent gameEvent) { await gameEvent.Origin.TellAsync(new[] { - $"Valid log values: {string.Join(",", Enum.GetValues())}" + _translationLookup["COMMANDS_LOGLEVEL_VALID_VALUES"] + .FormatExt(string.Join(",", Enum.GetValues())) }); return; } @@ -75,6 +76,9 @@ await gameEvent.Origin.TellAsync(new[] } await gameEvent.Origin.TellAsync(new[] - { $"Set minimum log level to {loggingSwitch.MinimumLevel.ToString()}" }); + { + _translationLookup["COMMANDS_LOGLEVEL_SUCCESS"] + .FormatExt(loggingSwitch.MinimumLevel.ToString()) + }); } } diff --git a/Application/CoreEventHandler.cs b/Application/CoreEventHandler.cs index fbac588b3..133a3727f 100644 --- a/Application/CoreEventHandler.cs +++ b/Application/CoreEventHandler.cs @@ -45,15 +45,13 @@ public void QueueEvent(IManager manager, CoreEvent coreEvent) 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 @@ public void StartProcessing(CancellationToken token) _onProcessingEvents.Release(1); } - _onEventReady.Wait(_cancellationToken); + _onEventReady.Wait(token); continue; } diff --git a/Application/DefaultSettings.json b/Application/DefaultSettings.json index 641caba7d..650f9aa39 100644 --- a/Application/DefaultSettings.json +++ b/Application/DefaultSettings.json @@ -3011,6 +3011,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", @@ -3018,38 +3019,465 @@ "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", + "doublebarrel": "Double-Barreled Shotgun", + "30cal": "Browning M1919", + "bar": "BAR", + "fg42": "FG42", + "m1garand": "M1 Garand", "mp40": "MP40", - "type100smg": "Type 100", "ppsh": "PPSh-41", - "svt40": "SVT-40", "gewehr43": "Gewehr 43", - "m1garand": "M1 Garand", + "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", + "m1garand_gl_upgraded": "The Imploder", + "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", + + "zombie_perk_bottle_jugg": "Juggernog", + "zombie_perk_bottle_revive": "Quick Revive", + "zombie_perk_bottle_sleight": "Speed Cola", + "zombie_perk_bottle_doubletap": "Double Tap", + "specialty_armorvest": "Juggernog", + "specialty_quickrevive": "Quick Revive", + "specialty_fastreload": "Speed Cola", + "specialty_rof": "Double Tap", + "specialty_juggernaut_zombies": "Juggernog", + "specialty_quickrevive_zombies": "Quick Revive", + "specialty_fastreload_zombies": "Speed Cola", + "specialty_doubletap_zombies": "Double Tap", + + "full_ammo": "Max Ammo", + "double_points": "Double Points", + "insta_kill": "Insta-Kill", + "nuke": "Nuke", + "carpenter": "Carpenter", + "fire_sale": "Fire Sale", + + "zombie_30cal": "Browning M1919", + "zombie_30cal_upgraded": "B115 accelerator", + "zombie_sw_357": ".357 Magnum", + "zombie_sw_357_upgraded": ".357 Plus 1 K1L-u", + "zombie_bar": "BAR", + "zombie_bar_upgraded": "The Widow Maker", + "zombie_colt": "Colt M1911", + "zombie_colt_upgraded": "C-3000 b1at-ch35", + "zombie_fg42": "FG42", + "zombie_fg42_upgraded": "420 Impeller", + "zombie_gewehr43": "Gewehr 43", + "zombie_gewehr43_upgraded": "G115 Compressor", + "zombie_kar98k": "Kar98k", + "zombie_kar98k_upgraded": "Armageddon", + "zombie_m1carbine": "M1A1 Carbine", + "zombie_m1carbine_upgraded": "Widdershins RC-1", + "zombie_m1garand": "M1 Garand", + "zombie_m1garand_upgraded": "M1000", + "m2_flamethrower_zombie": "M2 Flamethrower", + "m2_flamethrower_zombie_upgraded": "FIW Nitrogen cooled", + "m7_launcher_zombie": "M7 Grenade Launcher", + "m7_launcher_zombie_upgraded": "The Imploder", + "zombie_mg42": "MG42", + "zombie_mg42_upgraded": "Barracuda FU-A11", + "zombie_mp40": "MP40", + "zombie_mp40_upgraded": "The Afterburner", + "panzerschrek_zombie": "Panzerschrek", + "panzerschrek_zombie_upgraded": "Longinus", + "zombie_ppsh": "PPSh-41", + "zombie_ppsh_upgraded": "The Reaper", + "ptrs41_zombie": "PTRS-41", + "ptrs41_zombie_upgraded": "The Penetrator", + "zombie_shotgun": "M1897 Trench Gun", + "zombie_shotgun_upgraded": "Gut Shot", + "zombie_doublebarrel": "Double-Barreled Shotgun", + "zombie_doublebarrel_upgraded": "24 Bore long range", + "zombie_doublebarrel_sawed": "Sawed-Off Double-Barreled Shotgun w/ Grip", + "zombie_doublebarrel_sawed_upgraded": "The Snuff Box", + "zombie_stg44": "STG-44", + "zombie_stg44_upgraded": "Spatz-447 +", + "zombie_thompson": "Thompson", + "zombie_thompson_upgraded": "Gibs-o-matic", + "zombie_type100_smg": "Type 100", + "zombie_type100_smg_upgraded": "1001 Samurais", + "zombie_type99_rifle": "Arisaka", + "zombie_type99_rifle_upgraded": "The Eviscerator" }, - - "T6" : { - + + "T5": { + "specialty_armorvest": "Juggernog", + "specialty_quickrevive": "Quick Revive", + "specialty_fastreload": "Speed Cola", + "specialty_rof": "Double Tap", + "specialty_deadshot": "Deadshot Daiquiri", + "specialty_longersprint": "Stamin-Up", + "specialty_flakjacket": "PhD Flopper", + "specialty_additionalprimaryweapon": "Mule Kick", + + "full_ammo": "Max Ammo", + "double_points": "Double Points", + "insta_kill": "Insta-Kill", + "nuke": "Nuke", + "carpenter": "Carpenter", + "fire_sale": "Fire Sale", + "bonfire_sale": "Bonfire Sale", + "all_revive": "All Revive", + "minigun": "Death Machine", + "free_perk": "Random Perk", + "tesla": "Wunderwaffe", + "random_weapon": "Random Weapon", + "monkey_swarm": "Monkey Swarm", + + "zombie_30cal": "Browning M1919", + "zombie_30cal_upgraded": "B115 accelerator", + "zombie_sw_357": ".357 Magnum", + "zombie_sw_357_upgraded": ".357 Plus 1 K1L-u", + "ak74u_zm": "AK74u", + "ak74u_upgraded_zm": "AK74fu2", + "aug_acog_zm": "AUG", + "aug_acog_mk_upgraded_zm": "AUG-50M3", + "aug_acog_gl_zm": "AUG Grenade Launcher", + "aug_acog_gl_upgraded_zm": "AUG Grenade Launcher Upgraded", + "zombie_bar": "BAR", + "zombie_bar_upgraded": "The Widow Maker", + "china_lake_zm": "China Lake", + "china_lake_upgraded_zm": "China Beach", + "zombie_colt": "Colt M1911", + "zombie_colt_upgraded": "C-3000 b1at-ch35", + "commando_zm": "Commando", + "commando_upgraded_zm": "Predator", + "crossbow_explosive_zm": "Crossbow", + "crossbow_explosive_upgraded_zm": "Awful Lawton", + "cz75_zm": "CZ75", + "cz75_upgraded_zm": "Calamity", + "cz75dw_zm": "CZ75 Dual Wield", + "cz75dw_upgraded_zm": "Calamity & Jane", + "dragunov_zm": "Dragunov", + "dragunov_upgraded_zm": "D115 Disassembler", + "enfield_zm": "Enfield", + "enfield_upgraded_zm": "Enfield Upgraded", + "famas_zm": "Famas", + "famas_upgraded_zm": "G16-GL35", + "zombie_fg42": "FG42", + "zombie_fg42_upgraded": "420 Impeller", + "fnfal_zm": "FN FAL", + "fnfal_upgraded_zm": "EPC WN", + "freezegun_zm": "Winter's Howl", + "freezegun_upgraded_zm": "Winter's Fury", + "g11_lps_zm": "G11", + "g11_lps_upgraded_zm": "G115 Generator", + "galil_zm": "Galil", + "galil_upgraded_zm": "Lamentation", + "zombie_gewehr43": "Gewehr 43", + "zombie_gewehr43_upgraded": "G115 Compressor", + "hk21_zm": "HK21", + "hk21_upgraded_zm": "H115 Oscillator", + "hs10_zm": "HS10", + "hs10_upgraded_zm": "Typhoid & Mary", + "ithaca_zm": "Stakeout", + "ithaca_upgraded_zm": "Raid", + "zombie_kar98k": "Kar98k", + "zombie_kar98k_upgraded": "Armageddon", + "knife_ballistic_zm": "Ballistic Knife", + "knife_ballistic_upgraded_zm": "The Krauss Refibrillator", + "l96a1_zm": "L96A1", + "l96a1_upgraded_zm": "L115 Isolator", + "m14_zm": "M14", + "m14_upgraded_zm": "Mnesia", + "m16_zm": "M16", + "m16_gl_upgraded_zm": "Skullcrusher", + "m1911_zm": "M1911", + "m1911_upgraded_zm": "Mustang & Sally", + "zombie_m1carbine": "M1 Carbine", + "zombie_m1carbine_upgraded": "Widdershins RC-1", + "zombie_m1garand": "M1 Garand", + "zombie_m1garand_upgraded": "M1000", + "m202_flash_zm": "Grim Reaper", + "m202_flash_upgraded_zm": "Grim Reaper Upgraded", + "m2_flamethrower_zombie": "M2 Flamethrower", + "m2_flamethrower_zombie_upgraded": "FIW Nitrogen cooled", + "m72_law_zm": "M72 LAW", + "m72_law_upgraded_zm": "M72 Anarchy", + "mac11_zm": "MAC11", + "mac11_upgraded_zm": "MAC11 Upgraded", + "zombie_mg42": "MG42", + "zombie_mg42_upgraded": "Barracuda FU-A11", + "mp40_zm": "MP40", + "mp40_upgraded_zm": "The Afterburner", + "mp5k_zm": "MP5K", + "mp5k_upgraded_zm": "MP115 Kollider", + "mpl_zm": "MPL", + "mpl_upgraded_zm": "MPL-LF", + "panzerschrek_zombie": "Panzerschrek", + "panzerschrek_zombie_upgraded": "Longinus", + "pm63_zm": "PM63", + "pm63_upgraded_zm": "Tokyo & Rose", + "zombie_ppsh": "PPSh-41", + "zombie_ppsh_upgraded": "The Reaper", + "ptrs41_zombie": "PTRS-41", + "ptrs41_zombie_upgraded": "The Penetrator", + "python_zm": "Python", + "python_upgraded_zm": "Cobra", + "ray_gun_zm": "Ray Gun", + "ray_gun_upgraded_zm": "Porter's X2 Ray Gun", + "rottweil72_zm": "Olympia", + "rottweil72_upgraded_zm": "Hades", + "rpk_zm": "RPK", + "rpk_upgraded_zm": "R115 Resonator", + "zombie_shotgun": "M1897 Trench Gun", + "zombie_shotgun_upgraded": "Gut Shot", + "zombie_doublebarrel": "Double-Barreled Shotgun", + "zombie_doublebarrel_upgraded": "24 Bore long range", + "zombie_doublebarrel_sawed": "Sawed-Off Double-Barreled Shotgun w/ Grip", + "zombie_doublebarrel_sawed_upgraded": "The Snuff Box", + "spas_zm": "SPAS-12", + "spas_upgraded_zm": "SPAZ-24", + "spectre_zm": "Spectre", + "spectre_upgraded_zm": "Phantom", + "zombie_stg44": "STG-44", + "zombie_stg44_upgraded": "Spatz-447 +", + "blow_gun_zm": "Blow Gun", + "blow_gun_upgraded_zm": "Super Blow Gun", + "tesla_gun_zm": "Wunderwaffe DG-2", + "tesla_gun_upgraded_zm": "Wunderwaffe DG-3 JZ", + "zombie_thompson": "Thompson", + "zombie_thompson_upgraded": "Gibs-o-matic", + "thundergun_zm": "Thundergun", + "thundergun_upgraded_zm": "ZeusCannon", + "zombie_type100_smg": "Type 100", + "zombie_type100_smg_upgraded": "1001 Samurais", + "zombie_type99_rifle": "Arisaka", + "zombie_type99_rifle_upgraded": "The Eviscerator", + "humangun_zm": "V-R11", + "humangun_upgraded_zm": "V-R11 Lazarus", + "microwavegun_zm": "Wave Gun", + "microwavegun_upgraded_zm": "Max Wave Gun", + "microwavegundw_zm": "Zap Gun Dual Wield", + "microwavegundw_upgraded_zm": "Porter's X2 Zap Gun Dual Wield", + "shrink_ray_zm": "31-79 JGb215", + "shrink_ray_upgraded_zm": "The Fractalizer", + "sniper_explosive_zm": "Scavenger", + "sniper_explosive_upgraded_zm": "Hyena Infra-dead" + }, + + "T6" : { "mp7": "MP7", "pdw57": "PDW-57", "vector": "Vector K10", @@ -3163,8 +3591,213 @@ "remote_mortar": "Lodestar", "player_gunner": "VTOL Warship", "dogs": "K9 Unit", - "missile_swarm": "Swarm" - + "missile_swarm": "Swarm", + + "specialty_armorvest": "Juggernog", + "specialty_quickrevive": "Quick Revive", + "specialty_fastreload": "Speed Cola", + "specialty_rof": "Double Tap", + "specialty_deadshot": "Deadshot Daiquiri", + "specialty_longersprint": "Stamin-Up", + "specialty_additionalprimaryweapon": "Mule Kick", + "specialty_scavenger": "Tombstone Soda", + "specialty_finalstand": "Who's Who", + "specialty_grenadepulldeath": "Electric Cherry", + "specialty_nomotionsensor": "Vulture Aid", + "specialty_weapupgrade": "Pack-a-Punch", + + "full_ammo": "Max Ammo", + "double_points": "Double Points", + "insta_kill": "Insta-Kill", + "insta_kill_ug": "Insta-Kill", + "nuke": "Nuke", + "carpenter": "Carpenter", + "fire_sale": "Fire Sale", + "bonfire_sale": "Bonfire Sale", + "all_revive": "All Revive", + "free_perk": "Random Perk", + "random_weapon": "Random Weapon", + "bonus_points_player": "Bonus Points", + "bonus_points_team": "Team Bonus Points", + "lose_points_team": "Lose Points", + "lose_perk": "Lose Perk", + "empty_clip": "Empty Clip", + "zombie_blood": "Zombie Blood", + "the_cure": "The Cure", + "teller_withdrawl": "Bank Withdrawal", + "blue_monkey": "Blue Monkey", + "meat_stink": "Meat", + + "870mcs_dualclip_zm": "R870 MCS Fast Mag", + "870mcs_dualclip_upgraded_zm": "Rettified-870 Mechanical Cranium Sequencer", + "870mcs_zm": "Remington 870 MCS", + "870mcs_upgraded_zm": "Refitted-870 Mechanical Cranium Sequencer", + "acidgat_zm": "Acid Gat", + "acidgat_upgraded_zm": "Vitriolic Withering", + "ak47_zm": "AK-47", + "ak47_upgraded_zm": "Reznov's Revenge", + "an94_zm": "AN-94", + "an94_upgraded_zm": "Actuated Neutralizer 94000", + "ballista_zm": "Ballista", + "ballista_upgraded_zm": "Infused Arbalest", + "barretm82_zm": "Barret M82A1", + "barretm82_upgraded_zm": "Macro Annihilator", + "beretta93r_zm": "B23R", + "beretta93r_upgraded_zm": "B34R", + "blundergat_zm": "Blundergat", + "blundergat_upgraded_zm": "The Sweeper", + "c96_zm": "Mauser C96", + "c96_upgraded_zm": "BoomHilda", + "dsr50_zm": "DSR-50", + "dsr50_upgraded_zm": "Dead Specimen Reactor 5000", + "evoskorpion_zm": "Skorpion", + "evoskorpion_upgraded_zm": "Evolved Death Stalker", + "fivesevendw_zm": "Five-seven Dual Wield", + "fivesevendw_upgraded_zm": "Ultra & Violet", + "fiveseven_zm": "Five-seven", + "fiveseven_upgraded_zm": "Ultra", + "hamr_zm": "HAMR", + "hamr_upgraded_zm": "SLDG HAMR", + "hk416_zm": "M27", + "hk416_upgraded_zm": "Mystifier", + "judge_zm": "Raging Judge", + "judge_upgraded_zm": "Voice of Justice", + "kard_zm": "KAP", + "kard_upgraded_zm": "Karmic Atom Perforator-4000", + "ksg_zm": "KSG", + "ksg_upgraded_zm": "Mist Maker", + "lsat_zm": "LSAT", + "lsat_upgraded_zm": "FSIRT", + "m32_zm": "M32 Grenade Launcher", + "m32_upgraded_zm": "Dystopic Demolisher", + "mg08_zm": "MG08/15", + "mg08_upgraded_zm": "Magna Collider", + "minigun_zm": "Minigun", + "minigun_upgraded_zm": "Meat Grinder", + "mp44_zm": "STG-44", + "mp44_upgraded_zm": "Spatz-447 +", + "slowgun_zm": "Paralyzer", + "slowgun_upgraded_zm": "Petrifier", + "pdw57_zm": "PDW-57", + "pdw57_upgraded_zm": "Predictive Death Wish 57000", + "qcw05_zm": "QCW-05", + "qcw05_upgraded_zm": "Chicom Cataclysmic Quadruple Burst", + "raygun_mark2_zm": "Ray Gun Mark II", + "raygun_mark2_upgraded_zm": "Porter's Mark II Ray Gun", + "rnma_zm": "Remington New Model Army", + "rnma_upgraded_zm": "Sassafras", + "rpd_zm": "RPD", + "rpd_upgraded_zm": "Relativistic Punishment Device", + "saiga12_zm": "Saiga-12", + "saiga12_upgraded_zm": "Synthetic Dozen", + "saritch_zm": "TOZ Saritch", + "saritch_upgraded_zm": "SM1L3R", + "scar_zm": "SCAR-H", + "scar_upgraded_zm": "Agarthan Reaper", + "slipgun_zm": "Sliquifier", + "slipgun_upgraded_zm": "Sl1qu1f13r", + "srm1216_zm": "M1216", + "srm1216_upgraded_zm": "Mesmerizer", + "staff_air_zm": "Staff of Wind", + "staff_air_upgraded_zm": "Boreas' Fury", + "staff_fire_zm": "Staff of Fire", + "staff_fire_upgraded_zm": "Kagutsuchi's Blood", + "staff_lightning_zm": "Staff of Lightning", + "staff_lightning_upgraded_zm": "Kimat's Bite", + "staff_water_zm": "Staff of Ice", + "staff_water_upgraded_zm": "Ull's Arrow", + "svu_zm": "SVU-AS", + "svu_upgraded_zm": "Shadowy Veil Utilizer", + "tar21_zm": "X95L", + "tar21_upgraded_zm": "Malevolent Taxonomic Anodized Redeemer", + "thompson_zm": "M1927", + "thompson_upgraded_zm": "Speakeasy", + "tomahawk_zm": "Hell's Retriever", + "tomahawk_upgraded_zm": "Hell's Redeemer", + "type95_zm": "Type 25", + "type95_upgraded_zm": "Strain 25", + "usrpg_zm": "RPG", + "usrpg_upgraded_zm": "Rocket Propelled Grievance", + "uzi_zm": "Uzi", + "uzi_upgraded_zm": "Uncle Gal", + "xm8_zm": "M8A1", + "xm8_upgraded_zm": "Micro Aerator", + "ak74u_zm": "AK74u", + "ak74u_upgraded_zm": "AK74fu2", + "aug_acog_gl_zm": "AUG w/ Grenade Launcher", + "aug_acog_gl_upgraded_zm": "AUG Grenade Launcher Upgraded", + "aug_acog_zm": "AUG", + "aug_acog_mk_upgraded_zm": "AUG-50M3", + "china_lake_zm": "China Lake", + "china_lake_upgraded_zm": "China Beach", + "commando_zm": "Commando", + "commando_upgraded_zm": "Predator", + "crossbow_explosive_zm": "Crossbow", + "crossbow_explosive_upgraded_zm": "Awful Lawton", + "cz75dw_zm": "CZ75 Dual Wield", + "cz75dw_upgraded_zm": "Calamity & Jane", + "cz75_zm": "CZ75", + "cz75_upgraded_zm": "Calamity", + "dragunov_zm": "Dragunov", + "dragunov_upgraded_zm": "D115 Disassembler", + "enfield_zm": "Enfield", + "enfield_upgraded_zm": "Enfield Upgraded", + "famas_zm": "FAMAS", + "famas_upgraded_zm": "G16-GL35", + "fnfal_zm": "FAL", + "fnfal_upgraded_zm": "WN", + "freezegun_zm": "Winter's Howl", + "freezegun_upgraded_zm": "Winter's Fury", + "g11_lps_zm": "G11", + "g11_lps_upgraded_zm": "G115 Generator", + "galil_zm": "Galil", + "galil_upgraded_zm": "Lamentation", + "hk21_zm": "HK21", + "hk21_upgraded_zm": "H115 Oscillator", + "hs10_zm": "HS10", + "hs10_upgraded_zm": "Typhoid & Mary", + "ithaca_zm": "Stakeout", + "ithaca_upgraded_zm": "Raid", + "knife_ballistic_zm": "Ballistic Knife", + "knife_ballistic_upgraded_zm": "The Krauss Refibrillator", + "l96a1_zm": "L96A1", + "l96a1_upgraded_zm": "L115 Isolator", + "m14_zm": "M14", + "m14_upgraded_zm": "Mnesia", + "m16_zm": "Colt M16A1", + "m16_gl_upgraded_zm": "Skullcrusher", + "m1911_zm": "M1911", + "m1911_upgraded_zm": "Mustang & Sally", + "m202_flash_zm": "M202 Flash", + "m202_flash_upgraded_zm": "Grim Reaper Upgraded", + "m72_law_zm": "M72 LAW", + "m72_law_upgraded_zm": "M72 Anarchy", + "mac11_zm": "MAC-11", + "mac11_upgraded_zm": "MAC11 Upgraded", + "mp40_zm": "MP40", + "mp40_upgraded_zm": "The Afterburner", + "mp5k_zm": "MP5", + "mp5k_upgraded_zm": "MP115 Kollider", + "mpl_zm": "MPL", + "mpl_upgraded_zm": "MPL-LF", + "pm63_zm": "PM63", + "pm63_upgraded_zm": "Tokyo & Rose", + "python_zm": "Python", + "python_upgraded_zm": "Cobra", + "ray_gun_zm": "Ray Gun", + "ray_gun_upgraded_zm": "Porter's X2 Ray Gun", + "rottweil72_zm": "Olympia", + "rottweil72_upgraded_zm": "Hades", + "rpk_zm": "RPK", + "rpk_upgraded_zm": "R115 Resonator", + "spas_zm": "SPAS-12", + "spas_upgraded_zm": "SPAZ-24", + "spectre_zm": "Spectre", + "spectre_upgraded_zm": "Phantom", + "tesla_gun_zm": "Wunderwaffe DG-2", + "tesla_gun_upgraded_zm": "Wunderwaffe DG-3 JZ", + "thundergun_zm": "Thunder Gun", + "thundergun_upgraded_zm": "ZeusCannon" } } } diff --git a/Application/Extensions/StartupExtensions.cs b/Application/Extensions/StartupExtensions.cs index be6acabe1..cfe723f17 100644 --- a/Application/Extensions/StartupExtensions.cs +++ b/Application/Extensions/StartupExtensions.cs @@ -48,8 +48,8 @@ public static IServiceCollection AddBaseLogger(this IServiceCollection services, loggerConfig = loggerConfig.WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}") - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Debug(); + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Debug(); } _defaultLogger = loggerConfig.CreateLogger(); @@ -83,10 +83,10 @@ public static IServiceCollection AddDatabaseContextOptions(this IServiceCollecti : currentPath; var connectionStringBuilder = new SqliteConnectionStringBuilder - {DataSource = Path.Join(currentPath, "Database", "Database.db")}; + { DataSource = Path.Join(currentPath, "Database", "Database.db") }; var connectionString = connectionStringBuilder.ToString(); - services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder() + services.AddSingleton(sp => (DbContextOptions)new DbContextOptionsBuilder() .UseSqlite(connectionString) .UseLoggerFactory(sp.GetRequiredService()) .EnableSensitiveDataLogging().Options); @@ -100,7 +100,7 @@ public static IServiceCollection AddDatabaseContextOptions(this IServiceCollecti StringComparison.InvariantCultureIgnoreCase); var connectionString = appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""); - services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder() + services.AddSingleton(sp => (DbContextOptions)new DbContextOptionsBuilder() .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mysqlOptions => mysqlOptions.EnableRetryOnFailure()) .UseLoggerFactory(sp.GetRequiredService()).Options); @@ -109,7 +109,7 @@ public static IServiceCollection AddDatabaseContextOptions(this IServiceCollecti appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout", StringComparison.InvariantCultureIgnoreCase); services.AddSingleton(sp => - (DbContextOptions) new DbContextOptionsBuilder() + (DbContextOptions)new DbContextOptionsBuilder() .UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""), postgresqlOptions => { diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 6ee9be854..4f30f6498 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -109,10 +109,12 @@ public override async Task OnClientConnected(EFClient clientFromLog) 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); @@ -120,12 +122,16 @@ public override async Task OnClientConnected(EFClient clientFromLog) client.CopyAdditionalProperties(clientFromLog); - // this is only a temporary version until the IPAddress is transmitted - client.CurrentAlias = new EFAlias() + if (foundClient) { - Name = clientFromLog.Name, - IPAddress = clientFromLog.IPAddress - }; + 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; @@ -426,10 +432,7 @@ protected override async Task ProcessEvent(GameEvent E) { if (E.Origin.State != ClientState.Connected) { - E.Origin.State = ClientState.Connected; - E.Origin.Connections += 1; - - ChatHistory.Add(new ChatInfo() + ChatHistory.Add(new ChatInfo { ClientId = E.Origin.ClientId, Name = E.Origin.Name, @@ -444,6 +447,10 @@ protected override async Task ProcessEvent(GameEvent E) { 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 { @@ -463,8 +470,6 @@ protected override async Task ProcessEvent(GameEvent E) 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); } } @@ -923,6 +928,23 @@ public async Task EnsureServerAdded() context.Entry(gameServer).Property(property => property.HostName).IsModified = true; } + var normalizedPerformanceCode = PerformanceCode?.ToLowerInvariant(); + if (gameServer.PerformanceBucket?.Code != normalizedPerformanceCode && !string.IsNullOrEmpty(normalizedPerformanceCode)) + { + var bucket = await context.Set() + .FirstOrDefaultAsync(b => b.Code == normalizedPerformanceCode); + + if (bucket == null) + { + bucket = new Data.Models.Client.Stats.EFPerformanceBucket { Code = normalizedPerformanceCode }; + context.Add(bucket); + await context.SaveChangesAsync(); + } + + gameServer.PerformanceBucketId = bucket.PerformanceBucketId; + context.Entry(gameServer).Property(p => p.PerformanceBucketId).IsModified = true; + } + if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword)) { gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword); @@ -1488,7 +1510,7 @@ await this.SetDvarAsync("sv_sayname", CustomSayName, MaxClients = maxplayers; FSGame = game.Value; Gametype = gametype; - IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress; + IP = ServerConfig.IPAddress; GamePassword = gamePassword.Value; PrivateClientSlots = privateClients.Value; diff --git a/Application/Main.cs b/Application/Main.cs index 555b28c93..9e653fc25 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -517,7 +517,7 @@ private static void ConfigureServices(IServiceCollection serviceCollection) var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = true }) { BaseAddress = masterUri, - Timeout = TimeSpan.FromSeconds(15) + Timeout = Utilities.IsDevelopment ? TimeSpan.FromMilliseconds(500) : TimeSpan.FromSeconds(15) }; var masterRestClient = RestService.For(httpClient); var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig); diff --git a/Application/Misc/ServerDataCollector.cs b/Application/Misc/ServerDataCollector.cs index 0bbc2d3ef..3adc3339a 100644 --- a/Application/Misc/ServerDataCollector.cs +++ b/Application/Misc/ServerDataCollector.cs @@ -30,6 +30,12 @@ public class ServerDataCollector : IServerDataCollector private bool _inProgress; private TimeSpan _period; + // Serializes GetOrCreateMap across all servers in a single collection tick so that + // two servers discovering the same new map cannot both insert an EFMaps row for it. + // Contention is negligible — collection runs on a long interval, serving ~1 call + // per server per tick, and EFMaps reads/writes are cheap. + private readonly SemaphoreSlim _getOrCreateMapLock = new(1, 1); + public ServerDataCollector(ILogger logger, ApplicationConfiguration appConfig, IManager manager, IDatabaseContextFactory contextFactory) { @@ -97,25 +103,32 @@ private async Task> BuildCollectionData(Cancellati private async Task GetOrCreateMap(string mapName, Reference.Game game, CancellationToken token) { - await using var context = _contextFactory.CreateContext(); - var existingMap = - await context.Maps.FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token); - - if (existingMap != null) + await _getOrCreateMapLock.WaitAsync(token); + try { - return existingMap.MapId; - } + await using var context = _contextFactory.CreateContext(); + var existingMap = await context.Maps + .FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token); - var newMap = new EFMap - { - Name = mapName, - Game = game - }; + if (existingMap != null) + { + return existingMap.MapId; + } - context.Maps.Add(newMap); - await context.SaveChangesAsync(token); + var newMap = new EFMap + { + Name = mapName, + Game = game + }; - return newMap.MapId; + context.Maps.Add(newMap); + await context.SaveChangesAsync(token); + return newMap.MapId; + } + finally + { + _getOrCreateMapLock.Release(); + } } private async Task SaveData(IEnumerable snapshots, CancellationToken token) diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs index aad6a3af3..3e2b3befd 100644 --- a/Application/Misc/ServerDataViewer.cs +++ b/Application/Misc/ServerDataViewer.cs @@ -8,6 +8,7 @@ using Data.Models.Client; using Data.Models.Client.Stats; using Data.Models.Server; +using IW4MAdmin.Plugins.Stats.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; @@ -25,31 +26,35 @@ public class ServerDataViewer : IServerDataViewer private readonly IDataValueCache _serverStatsCache; private readonly IDataValueCache> _clientHistoryCache; private readonly IDataValueCache _rankedClientsCache; + private readonly StatManager _statManager; private readonly TimeSpan? _cacheTimeSpan = - Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10); + Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?)TimeSpan.FromMinutes(10); public ServerDataViewer(ILogger logger, IDataValueCache snapshotCache, IDataValueCache serverStatsCache, - IDataValueCache> clientHistoryCache, IDataValueCache rankedClientsCache) + IDataValueCache> clientHistoryCache, + IDataValueCache rankedClientsCache, StatManager statManager) { _logger = logger; _snapshotCache = snapshotCache; _serverStatsCache = serverStatsCache; _clientHistoryCache = clientHistoryCache; _rankedClientsCache = rankedClientsCache; + _statManager = statManager; } - public async Task<(int?, DateTime?)> + public async Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null, - CancellationToken token = default) + CancellationToken token = default) { - _snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) => + _snapshotCache.SetCacheItem(async (snapshots, idsList, cancellationToken) => { Reference.Game? game = null; long? id = null; - if (ids.Any()) + var ids = idsList.ToList(); + if (ids.Count is not 0) { game = (Reference.Game?)ids.First(); id = (long?)ids.Last(); @@ -99,12 +104,11 @@ public ServerDataViewer(ILogger logger, IDataValueCache logger, IDataValueCache ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default) + public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, + CancellationToken token = default) { _serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) => { Reference.Game? game = null; - + if (ids.Any()) { game = (Reference.Game?)ids.First(); } - + var count = await set.CountAsync(item => game == null || item.GameName == game, cancellationToken); var startOfPeriod = DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24); - var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod, + var recentCount = await set.CountAsync( + client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod, cancellationToken); return (count, recentCount); @@ -170,7 +176,10 @@ public async Task> ClientHistoryAsync(TimeSpan? o { ServerId = byServer.Key, ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot - { Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount, ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName}).ToList() + { + Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount, + ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName + }).ToList() }).ToList(); }, nameof(_clientHistoryCache), TimeSpan.MaxValue); @@ -181,34 +190,38 @@ public async Task> ClientHistoryAsync(TimeSpan? o catch (Exception ex) { _logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientHistoryAsync)); - return Enumerable.Empty(); + return []; } } - public async Task RankedClientsCountAsync(long? serverId = null, CancellationToken token = default) + public async Task RankedClientsCountAsync(long? serverId = null, string performanceBucketCode = null, + CancellationToken token = default) { - _rankedClientsCache.SetCacheItem((set, ids, cancellationToken) => + _rankedClientsCache.SetCacheItem(async (set, idsList, cancellationToken) => { long? id = null; - - if (ids.Any()) + string bucket = null; + + var ids = idsList.ToList(); + if (ids.Count is not 0) { id = (long?)ids.First(); } - var fifteenDaysAgo = Plugins.Stats.Extensions.FifteenDaysAgo(); - return set - .Where(rating => rating.Newest) - .Where(rating => rating.ServerId == id) - .Where(rating => rating.CreatedDateTime >= fifteenDaysAgo) - .Where(rating => rating.Client.Level != EFClient.Permission.Banned) - .Where(rating => rating.Ranking != null) - .CountAsync(cancellationToken); - }, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan); - + if (ids.Count is 2) + { + bucket = (string)ids.Last(); + } + + // GetTotalRankedPlayers resolves its own bucket config — no need to pre-fetch. + // The prior .ContinueWith(...).Result pattern blocked sync-over-async and + // orphaned the DbContext inside GetBucketConfig, leaking Npgsql connections. + return await _statManager.GetTotalRankedPlayers(id, bucket); + }, nameof(_rankedClientsCache), [serverId, performanceBucketCode], _cacheTimeSpan); + try { - return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token); + return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), [serverId, performanceBucketCode], token); } catch (Exception ex) { diff --git a/Application/RConParsers/BaseRConParser.cs b/Application/RConParsers/BaseRConParser.cs index ba238e6e8..f21d898d5 100644 --- a/Application/RConParsers/BaseRConParser.cs +++ b/Application/RConParsers/BaseRConParser.cs @@ -218,13 +218,13 @@ private T GetValueFromStatus(IEnumerable response, ParserRegex.GroupT return (T)Convert.ChangeType(value, typeof(T)); } - public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default) + public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default, Action onPacketSent = null) { var dvarString = (dvarValue is string str) ? $"{dvarName} \"{str}\"" : $"{dvarName} {dvarValue}"; - return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0; + return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token, onPacketSent)).Length > 0; } public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, diff --git a/Application/Services/ServerLatencyMonitoringService.cs b/Application/Services/ServerLatencyMonitoringService.cs index abd91d619..bb954d3b4 100644 --- a/Application/Services/ServerLatencyMonitoringService.cs +++ b/Application/Services/ServerLatencyMonitoringService.cs @@ -22,8 +22,10 @@ public class ServerLatencyMonitoringService(Server server, ApplicationConfigurat private const string DvarProbe = "sv_iw4madmin_probe"; private const string DvarLatencyProbe = "sv_iw4madmin_latencyprobe"; private static readonly TimeSpan StaleProbeThreshold = TimeSpan.FromSeconds(30); + private const double ProbeJitterFraction = 0.2; private readonly ConcurrentDictionary _pendingProbes = new(); + private readonly Random _jitterRng = new(); private Timer _probeTimer; private bool _gscDetected; @@ -33,33 +35,33 @@ public class ServerLatencyMonitoringService(Server server, ApplicationConfigurat public void Start(CancellationToken cancellationToken) { IGameServerEventSubscriptions.ServerStatusReceived += OnServerStatusReceived; - IGameEventSubscriptions.ScriptEventTriggered += OnScriptEventTriggered; - if (appConfig.LatencyProbeIntervalMs > 0 && server.IsLegacyGameIntegrationEnabled) + if (appConfig.LatencyProbeIntervalMs > 0) { + // GSC presence is determined by DetectGscCompanionAsync probing + // sv_iw4madmin_latencyprobe directly — no pre-gate required, and + // any pre-gate based on legacy flags (e.g. sv_customcallbacks) + // produces false negatives on games that don't use them. + IGameEventSubscriptions.ScriptEventTriggered += OnScriptEventTriggered; IGameEventSubscriptions.MatchStarted += OnMatchStarted; // Initial detection attempt after a short delay to allow GSC to initialize _ = new Timer(OnInitialDetection, null, appConfig.LatencyProbeIntervalMs, Timeout.Infinite); - - cancellationToken.Register(() => - { - _probeTimer?.Dispose(); - IGameServerEventSubscriptions.ServerStatusReceived -= OnServerStatusReceived; - IGameEventSubscriptions.ScriptEventTriggered -= OnScriptEventTriggered; - IGameEventSubscriptions.MatchStarted -= OnMatchStarted; - _pendingProbes.Clear(); - }); } - else + + cancellationToken.Register(() => { - cancellationToken.Register(() => + _probeTimer?.Dispose(); + IGameServerEventSubscriptions.ServerStatusReceived -= OnServerStatusReceived; + + if (appConfig.LatencyProbeIntervalMs > 0) { - IGameServerEventSubscriptions.ServerStatusReceived -= OnServerStatusReceived; IGameEventSubscriptions.ScriptEventTriggered -= OnScriptEventTriggered; - _pendingProbes.Clear(); - }); - } + IGameEventSubscriptions.MatchStarted -= OnMatchStarted; + } + + _pendingProbes.Clear(); + }); } private Task OnServerStatusReceived(ServerStatusReceiveEvent statusEvent, CancellationToken token) @@ -86,12 +88,31 @@ private Task OnScriptEventTriggered(GameScriptEvent scriptEvent, CancellationTok if (!string.IsNullOrEmpty(probe.ProbeId) && _pendingProbes.TryRemove(probe.ProbeId, out var sendTime)) { - var latencyMs = (DateTime.UtcNow - sendTime).TotalMilliseconds; - LatencyMetrics.RecordLogProbeLatency(latencyMs); + var totalMs = (DateTime.UtcNow - sendTime).TotalMilliseconds; + + // GameLogIngestMs is the path a natural log line takes — game-write → + // GLS poll → IW4MAdmin parse — and explicitly does NOT include the RCon + // out-leg the probe used to start the clock. Subtract one-way RCon delivery + // (rtt/2) from the measured total. If RCon RTT isn't established yet, skip + // this sample rather than record an inflated number; EMA recovers on the + // next probe once RTT samples accumulate. + var rtt = LatencyMetrics.RconRoundTripMs; + if (rtt is null) + { + using (LogContext.PushProperty("Server", server.Id)) + { + logger.LogDebug("Log probe {ProbeId} dropped: RCon RTT not yet stable", probe.ProbeId); + } + return Task.CompletedTask; + } + + var pipelineMs = Math.Max(0, totalMs - rtt.Value / 2.0); + LatencyMetrics.RecordLogProbeLatency(pipelineMs); using (LogContext.PushProperty("Server", server.Id)) { - logger.LogDebug("Log probe {ProbeId} latency: {LatencyMs:F1}ms", probe.ProbeId, latencyMs); + logger.LogDebug("Log probe {ProbeId} pipeline: {PipelineMs:F1}ms (total {TotalMs:F1}ms - rtt/2 {HalfRtt:F1}ms)", + probe.ProbeId, pipelineMs, totalMs, rtt.Value / 2.0); } } @@ -134,13 +155,14 @@ private async Task TryDetectAndStartProbing() if (_gscDetected && _probeTimer is null) { - _probeTimer = new Timer(OnProbeTimerElapsed, null, appConfig.LatencyProbeIntervalMs, - appConfig.LatencyProbeIntervalMs); + // One-shot timer; OnProbeTimerElapsed re-arms with a jittered delay each + // cycle so probes don't phase-lock to the GameLogReader poll cycle. + _probeTimer = new Timer(OnProbeTimerElapsed, null, NextJitteredDelayMs(), Timeout.Infinite); using (LogContext.PushProperty("Server", server.Id)) { - logger.LogInformation("Latency probe started (interval: {Interval}ms)", - appConfig.LatencyProbeIntervalMs); + logger.LogInformation("Latency probe started (interval: {Interval}ms ±{JitterPct:P0})", + appConfig.LatencyProbeIntervalMs, ProbeJitterFraction); } } } @@ -159,16 +181,33 @@ private async void OnProbeTimerElapsed(object state) logger.LogWarning(ex, "Error in latency probe timer"); } } + finally + { + _probeTimer?.Change(NextJitteredDelayMs(), Timeout.Infinite); + } + } + + private int NextJitteredDelayMs() + { + var basePeriod = appConfig.LatencyProbeIntervalMs; + var jitterRange = (int)(basePeriod * ProbeJitterFraction); + // uniform offset in [-jitterRange, +jitterRange] + var offset = _jitterRng.Next(-jitterRange, jitterRange + 1); + return Math.Max(1, basePeriod + offset); } private async Task SendProbeAsync() { var probeId = Guid.NewGuid().ToString("N")[..8]; - _pendingProbes[probeId] = DateTime.UtcNow; try { - await server.SetDvarAsync(DvarProbe, probeId, server.Manager.CancellationToken); + // T1 anchored to the actual UDP send moment via onPacketSent callback — + // strips C#-side queue/flood-protect/retry pollution from the measurement. + // TryAdd ensures retries (which re-fire the callback) don't overwrite the + // first send time; GSC echoes on whichever attempt arrives first. + await server.SetDvarAsync(DvarProbe, probeId, server.Manager.CancellationToken, + onPacketSent: sentAt => _pendingProbes.TryAdd(probeId, sentAt)); using (LogContext.PushProperty("Server", server.Id)) { diff --git a/CLAUDE.md b/CLAUDE.md index 67724525c..e009b2443 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,32 @@ EF Core 9.0 with three supported providers (each has its own migration context i Migrations live in `Data/Migrations/{Sqlite,Postgresql,MySql}/`. When adding a migration, you must add it for each provider using the corresponding context class. +### Generating Migrations + +The `MigrationContext` classes require `ASPNETCORE_ENVIRONMENT=Migration` to enable their parameterless constructors. Use the `Data` project as both the project and startup project (not `Application`, which has a PreBuild script that fails under `dotnet ef`). + +```bash +# SQLite +ASPNETCORE_ENVIRONMENT=Migration dotnet ef migrations add \ + -p Data/Data.csproj -s Data/Data.csproj \ + -c Data.MigrationContext.SqliteDatabaseContext \ + -o Migrations/Sqlite --configuration Release + +# PostgreSQL +ASPNETCORE_ENVIRONMENT=Migration dotnet ef migrations add \ + -p Data/Data.csproj -s Data/Data.csproj \ + -c Data.MigrationContext.PostgresqlDatabaseContext \ + -o Migrations/Postgresql --configuration Release + +# MySQL (requires a live MySQL server for ServerVersion.AutoDetect — +# if unavailable, temporarily replace AutoDetect in MySqlDatabaseContext.cs +# with: new MySqlServerVersion(new Version(8, 0, 35)), then revert after) +ASPNETCORE_ENVIRONMENT=Migration dotnet ef migrations add \ + -p Data/Data.csproj -s Data/Data.csproj \ + -c Data.MigrationContext.MySqlDatabaseContext \ + -o Migrations/MySql --configuration Release +``` + ## Architecture ### Project Dependency Graph diff --git a/Data/Context/DatabaseContext.cs b/Data/Context/DatabaseContext.cs index 0d9b98d0f..6de8862f8 100644 --- a/Data/Context/DatabaseContext.cs +++ b/Data/Context/DatabaseContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; using System.Threading; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using Data.Models.Client.Stats.Reference; using Data.Models.Misc; using Data.Models.Server; +using Data.Models.Zombie; namespace Data.Context { @@ -24,6 +26,7 @@ public abstract class DatabaseContext : DbContext #region STATS + public DbSet PerformanceBuckets { get; set; } public DbSet Vector3s { get; set; } public DbSet SnapshotVector3s { get; set; } public DbSet ACSnapshots { get; set; } @@ -37,6 +40,9 @@ public abstract class DatabaseContext : DbContext public DbSet HitStatistics { get; set; } public DbSet Weapons { get; set; } public DbSet WeaponAttachments { get; set; } + + public DbSet ClientStatTags { get; set; } + public DbSet ClientStatTagValues { get; set; } public DbSet Maps { get; set; } #endregion @@ -48,6 +54,18 @@ public abstract class DatabaseContext : DbContext public DbSet ServerSnapshots { get;set; } public DbSet ConnectionHistory { get; set; } + #endregion + + #region Zombie + + public DbSet ZombieMatches { get; set; } + public DbSet ZombieMatchClientStats { get; set; } + public DbSet ZombieRoundClientStats { get; set; } + public DbSet ZombieClientStatAggregates { get; set; } + public DbSet ZombieClientStatRecords { get; set; } + public DbSet ZombieEvents { get; set; } + public DbSet ZombieRoundDurationEmas { get; set; } + #endregion private void SetAuditColumns() @@ -63,10 +81,6 @@ public DatabaseContext() } } - public DatabaseContext(DbContextOptions options) : base(options) - { - } - protected DatabaseContext(DbContextOptions options) : base(options) { } @@ -84,6 +98,33 @@ public override int SaveChanges() return base.SaveChanges(); } + /// + /// SQLite has no native type and the EF Core + /// SQLite provider can't translate ORDER BY on DateTimeOffset columns + /// to SQL (throws NotSupportedException at query compile time). The + /// canonical fix is a value converter that stores DateTimeOffset as a long + /// (binary tick representation) — comparable, ORDER BY-able, and round-trips + /// the offset losslessly via . + /// + /// Postgres + MySQL handle DateTimeOffset natively, so the converter is only + /// applied when running on SQLite (detected via ). + /// Apply via ConfigureConventions so it covers every DateTimeOffset + /// property in every entity globally — no risk of forgetting one as new + /// models are added. + /// + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + { + configurationBuilder.Properties() + .HaveConversion(); + configurationBuilder.Properties() + .HaveConversion(); + } + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { // make network id unique @@ -175,7 +216,69 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("EFPenaltyIdentifiers"); modelBuilder.Entity().ToTable(nameof(EFServerSnapshot)); modelBuilder.Entity().ToTable(nameof(EFClientConnectionHistory)); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieMatches)}"); + // Index supports the GSC stitching lookup: TrackClient checks + // for an open match on this server with the same GameMatchId. + ent.HasIndex(m => new { m.ServerId, m.GameMatchId, m.MatchEndDate }); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieClientStat)}s"); + ent.HasOne(prop => prop.Client) + .WithMany(prop => prop.ZombieClientStats) + .HasForeignKey(prop => prop.ClientId); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieMatchClientStats)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieRoundClientStats)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieClientStatAggregates)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieEvents)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieClientStatRecords)}"); + }); + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ZombieRoundDurationEmas)}"); + ent.HasKey(e => new { e.MapId, e.RoundNumber, e.PlayerCount }); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(PerformanceBuckets)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ClientStatTags)}"); + }); + + modelBuilder.Entity(ent => + { + ent.ToTable($"EF{nameof(ClientStatTagValues)}"); + }); + Models.Configuration.StatsModelConfiguration.Configure(modelBuilder); base.OnModelCreating(modelBuilder); diff --git a/Data/MigrationContext/MySqlDatabaseContext.cs b/Data/MigrationContext/MySqlDatabaseContext.cs index cddffacd1..60e7a7d09 100644 --- a/Data/MigrationContext/MySqlDatabaseContext.cs +++ b/Data/MigrationContext/MySqlDatabaseContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using Data.Context; using Data.Extensions; using Microsoft.EntityFrameworkCore; @@ -24,6 +24,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (MigrationExtensions.IsMigration) { + // Note: ServerVersion.AutoDetect requires a live MySQL server connection. + // To generate migrations without a running server, temporarily replace with: + // new MySqlServerVersion(new Version(8, 0, 35)) then revert after generation. var connectionString = "Server=127.0.0.1;Database=IW4MAdmin_Migration;Uid=root;Pwd=password;"; optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) .EnableDetailedErrors() diff --git a/Data/Migrations/MySql/20260411135005_AddZombieStats.Designer.cs b/Data/Migrations/MySql/20260411135005_AddZombieStats.Designer.cs new file mode 100644 index 000000000..8fc399002 --- /dev/null +++ b/Data/Migrations/MySql/20260411135005_AddZombieStats.Designer.cs @@ -0,0 +1,2281 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260411135005_AddZombieStats")] + partial class AddZombieStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260411135005_AddZombieStats.cs b/Data/Migrations/MySql/20260411135005_AddZombieStats.cs new file mode 100644 index 000000000..429f458a9 --- /dev/null +++ b/Data/Migrations/MySql/20260411135005_AddZombieStats.cs @@ -0,0 +1,485 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFServers", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "EFClientStatTags", + columns: table => new + { + ZombieStatTagId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TagName = table.Column(type: "varchar(128)", maxLength: 128, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTags", x => x.ZombieStatTagId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieMatches", + columns: table => new + { + ZombieMatchId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + MapId = table.Column(type: "int", nullable: true), + ServerId = table.Column(type: "bigint", nullable: true), + ClientsCompleted = table.Column(type: "int", nullable: false), + PlayerCount = table.Column(type: "int", nullable: true), + HighestRound = table.Column(type: "int", nullable: false), + MatchStartDate = table.Column(type: "datetime(6)", nullable: false), + MatchEndDate = table.Column(type: "datetime(6)", nullable: true), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatches", x => x.ZombieMatchId); + table.ForeignKey( + name: "FK_EFZombieMatches_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId"); + table.ForeignKey( + name: "FK_EFZombieMatches_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "PerformanceBuckets", + columns: table => new + { + PerformanceBucketId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Code = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_PerformanceBuckets", x => x.PerformanceBucketId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFClientStatTagValues", + columns: table => new + { + ZombieClientStatTagValueId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + StatValue = table.Column(type: "int", nullable: true), + StatTagId = table.Column(type: "int", nullable: false), + ClientId = table.Column(type: "int", nullable: false), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTagValues", x => x.ZombieClientStatTagValueId); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClientStatTags_StatTagId", + column: x => x.StatTagId, + principalTable: "EFClientStatTags", + principalColumn: "ZombieStatTagId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + MatchId = table.Column(type: "int", nullable: true), + ClientId = table.Column(type: "int", nullable: false), + Kills = table.Column(type: "int", nullable: false), + Deaths = table.Column(type: "int", nullable: false), + DamageDealt = table.Column(type: "bigint", nullable: false), + DamageReceived = table.Column(type: "int", nullable: false), + Headshots = table.Column(type: "int", nullable: false), + HeadshotKills = table.Column(type: "int", nullable: false), + Melees = table.Column(type: "int", nullable: false), + Downs = table.Column(type: "int", nullable: false), + Revives = table.Column(type: "int", nullable: false), + PointsEarned = table.Column(type: "bigint", nullable: false), + PointsSpent = table.Column(type: "bigint", nullable: false), + PerksConsumed = table.Column(type: "int", nullable: false), + PowerupsGrabbed = table.Column(type: "int", nullable: false), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieEvents", + columns: table => new + { + ZombieEventLogId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + EventType = table.Column(type: "int", nullable: false), + SourceClientId = table.Column(type: "int", nullable: true), + AssociatedClientId = table.Column(type: "int", nullable: true), + NumericalValue = table.Column(type: "double", nullable: true), + TextualValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + MatchId = table.Column(type: "int", nullable: true), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieEvents", x => x.ZombieEventLogId); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_AssociatedClientId", + column: x => x.AssociatedClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_SourceClientId", + column: x => x.SourceClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatAggregates", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false), + ServerId = table.Column(type: "bigint", nullable: true), + AverageKillsPerDown = table.Column(type: "double", nullable: false), + AverageDowns = table.Column(type: "double", nullable: false), + AverageRevives = table.Column(type: "double", nullable: false), + HeadshotPercentage = table.Column(type: "double", nullable: false), + AlivePercentage = table.Column(type: "double", nullable: false), + AverageMelees = table.Column(type: "double", nullable: false), + AverageRoundReached = table.Column(type: "double", nullable: false), + AveragePoints = table.Column(type: "double", nullable: false), + HighestRound = table.Column(type: "int", nullable: false), + TotalRoundsPlayed = table.Column(type: "int", nullable: false), + TotalMatchesPlayed = table.Column(type: "int", nullable: false), + TotalMatchesCompleted = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatAggregates", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFZombieClientStats_ZombieClien~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieMatchClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatchClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieMatchClientStats_EFZombieClientStats_ZombieClientSta~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieRoundClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false), + StartTime = table.Column(type: "datetime(6)", nullable: false), + EndTime = table.Column(type: "datetime(6)", nullable: true), + Duration = table.Column(type: "time(6)", nullable: true), + TimeAlive = table.Column(type: "time(6)", nullable: true), + RoundNumber = table.Column(type: "int", nullable: false), + Points = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieRoundClientStats_EFZombieClientStats_ZombieClientSta~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatRecords", + columns: table => new + { + ZombieClientStatRecordId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Type = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ClientId = table.Column(type: "int", nullable: true), + RoundId = table.Column(type: "bigint", nullable: true), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatRecords", x => x.ZombieClientStatRecordId); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFZombieRoundClientStats_RoundId", + column: x => x.RoundId, + principalTable: "EFZombieRoundClientStats", + principalColumn: "ZombieClientStatId"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_ClientId", + table: "EFClientStatTagValues", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_StatTagId", + table: "EFClientStatTagValues", + column: "StatTagId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatAggregates_ServerId", + table: "EFZombieClientStatAggregates", + column: "ServerId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_ClientId", + table: "EFZombieClientStatRecords", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_RoundId", + table: "EFZombieClientStatRecords", + column: "RoundId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_ClientId", + table: "EFZombieClientStats", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_MatchId", + table: "EFZombieClientStats", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_AssociatedClientId", + table: "EFZombieEvents", + column: "AssociatedClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_MatchId", + table: "EFZombieEvents", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_SourceClientId", + table: "EFZombieEvents", + column: "SourceClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_MapId", + table: "EFZombieMatches", + column: "MapId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropTable( + name: "EFClientStatTagValues"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatAggregates"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatRecords"); + + migrationBuilder.DropTable( + name: "EFZombieEvents"); + + migrationBuilder.DropTable( + name: "EFZombieMatchClientStats"); + + migrationBuilder.DropTable( + name: "PerformanceBuckets"); + + migrationBuilder.DropTable( + name: "EFClientStatTags"); + + migrationBuilder.DropTable( + name: "EFZombieRoundClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieMatches"); + + migrationBuilder.DropIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics"); + } + } +} diff --git a/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.Designer.cs b/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.Designer.cs new file mode 100644 index 000000000..c32422474 --- /dev/null +++ b/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.Designer.cs @@ -0,0 +1,2299 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260414171803_AddZombieEconomyStats")] + partial class AddZombieEconomyStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.cs b/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.cs new file mode 100644 index 000000000..c58b73be7 --- /dev/null +++ b/Data/Migrations/MySql/20260414171803_AddZombieEconomyStats.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieEconomyStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BoxUses", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DoorsOpened", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TrapsActivated", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BoxUses", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "DoorsOpened", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "TrapsActivated", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats"); + } + } +} diff --git a/Data/Migrations/MySql/20260418155419_DedupeEFMaps.Designer.cs b/Data/Migrations/MySql/20260418155419_DedupeEFMaps.Designer.cs new file mode 100644 index 000000000..0606d796f --- /dev/null +++ b/Data/Migrations/MySql/20260418155419_DedupeEFMaps.Designer.cs @@ -0,0 +1,2299 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260418155419_DedupeEFMaps")] + partial class DedupeEFMaps + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260418155419_DedupeEFMaps.cs b/Data/Migrations/MySql/20260418155419_DedupeEFMaps.cs new file mode 100644 index 000000000..ca43903fb --- /dev/null +++ b/Data/Migrations/MySql/20260418155419_DedupeEFMaps.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class DedupeEFMaps : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Merge pre-existing duplicate EFMaps rows that share (Name, Game). These were + // produced by the pre-fix race in ServerDataCollector.GetOrCreateMap — two + // concurrent server scans both inserting the same (Name, Game) row. + // Re-point FK references onto the canonical (lowest) MapId per group, then + // delete the orphans. No schema change; data cleanup only. + + migrationBuilder.Sql(@" + UPDATE `EFServerSnapshot` s + INNER JOIN `EFMaps` m ON s.`MapId` = m.`MapId` + INNER JOIN ( + SELECT `Name`, `Game`, MIN(`MapId`) AS canonical_id + FROM `EFMaps` + GROUP BY `Name`, `Game` + ) c ON m.`Name` = c.`Name` AND m.`Game` = c.`Game` + SET s.`MapId` = c.canonical_id + WHERE m.`MapId` <> c.canonical_id;"); + + migrationBuilder.Sql(@" + UPDATE `EFZombieMatches` z + INNER JOIN `EFMaps` m ON z.`MapId` = m.`MapId` + INNER JOIN ( + SELECT `Name`, `Game`, MIN(`MapId`) AS canonical_id + FROM `EFMaps` + GROUP BY `Name`, `Game` + ) c ON m.`Name` = c.`Name` AND m.`Game` = c.`Game` + SET z.`MapId` = c.canonical_id + WHERE m.`MapId` <> c.canonical_id;"); + + // MySQL forbids self-referencing DELETE with an unbuffered subquery — wrap it. + migrationBuilder.Sql(@" + DELETE FROM `EFMaps` + WHERE `MapId` NOT IN ( + SELECT `MapId` FROM ( + SELECT MIN(`MapId`) AS `MapId` + FROM `EFMaps` + GROUP BY `Name`, `Game` + ) keepers + );"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Dedupe is not reversible — the orphan rows are gone. + } + } +} diff --git a/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs b/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs new file mode 100644 index 000000000..445901066 --- /dev/null +++ b/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs @@ -0,0 +1,2309 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset")] + partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs b/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs new file mode 100644 index 000000000..24acc481d --- /dev/null +++ b/Data/Migrations/MySql/20260425193434_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches"); + + migrationBuilder.AddColumn( + name: "GameMatchId", + table: "EFZombieMatches", + type: "varchar(64)", + maxLength: 64, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches", + columns: new[] { "ServerId", "GameMatchId", "MatchEndDate" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "GameMatchId", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats"); + + migrationBuilder.DropColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + } + } +} diff --git a/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.Designer.cs b/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.Designer.cs new file mode 100644 index 000000000..042b29e89 --- /dev/null +++ b/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.Designer.cs @@ -0,0 +1,2315 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260428195646_AddZombieMatchEasterEgg")] + partial class AddZombieMatchEasterEgg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.cs b/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.cs new file mode 100644 index 000000000..10887186f --- /dev/null +++ b/Data/Migrations/MySql/20260428195646_AddZombieMatchEasterEgg.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieMatchEasterEgg : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "EasterEggRound", + table: "EFZombieMatches", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "EasterEggRound", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs b/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs new file mode 100644 index 000000000..e27e1abb4 --- /dev/null +++ b/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs @@ -0,0 +1,2354 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260508225945_AddZombieRoundEmaSpeedAndSoloFactor")] + partial class AddZombieRoundEmaSpeedAndSoloFactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.cs b/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.cs new file mode 100644 index 000000000..86fecd0ab --- /dev/null +++ b/Data/Migrations/MySql/20260508225945_AddZombieRoundEmaSpeedAndSoloFactor.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieRoundEmaSpeedAndSoloFactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates", + type: "double", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.AddColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates", + type: "double", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.CreateTable( + name: "EFZombieRoundDurationEmas", + columns: table => new + { + MapId = table.Column(type: "int", nullable: false), + RoundNumber = table.Column(type: "int", nullable: false), + PlayerCount = table.Column(type: "int", nullable: false), + EmaSeconds = table.Column(type: "double", nullable: false), + SampleCount = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundDurationEmas", x => new { x.MapId, x.RoundNumber, x.PlayerCount }); + table.ForeignKey( + name: "FK_EFZombieRoundDurationEmas_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFZombieRoundDurationEmas"); + + migrationBuilder.DropColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates"); + + migrationBuilder.DropColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates"); + } + } +} diff --git a/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.Designer.cs b/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.Designer.cs new file mode 100644 index 000000000..56930f5c5 --- /dev/null +++ b/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.Designer.cs @@ -0,0 +1,2354 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260508231318_RenamePerformanceBucketsTable")] + partial class RenamePerformanceBucketsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.cs b/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.cs new file mode 100644 index 000000000..65c53f489 --- /dev/null +++ b/Data/Migrations/MySql/20260508231318_RenamePerformanceBucketsTable.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class RenamePerformanceBucketsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "PerformanceBuckets", + newName: "EFPerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucket~", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBucke~", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucket~", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBucke~", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "EFPerformanceBuckets", + newName: "PerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + } +} diff --git a/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.Designer.cs b/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.Designer.cs new file mode 100644 index 000000000..285dd566c --- /dev/null +++ b/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.Designer.cs @@ -0,0 +1,2357 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260509104057_AddZombieMatchCompletedFlag")] + partial class AddZombieMatchCompletedFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.cs b/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.cs new file mode 100644 index 000000000..1560cff88 --- /dev/null +++ b/Data/Migrations/MySql/20260509104057_AddZombieMatchCompletedFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieMatchCompletedFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Completed", + table: "EFZombieMatches", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Completed", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.Designer.cs b/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.Designer.cs new file mode 100644 index 000000000..d4cbd3751 --- /dev/null +++ b/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.Designer.cs @@ -0,0 +1,2360 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260509155942_AddZombieRoundPlayerCountAtRoundStart")] + partial class AddZombieRoundPlayerCountAtRoundStart + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("int"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.cs b/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.cs new file mode 100644 index 000000000..cf10be9db --- /dev/null +++ b/Data/Migrations/MySql/20260509155942_AddZombieRoundPlayerCountAtRoundStart.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieRoundPlayerCountAtRoundStart : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.Designer.cs b/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.Designer.cs new file mode 100644 index 000000000..61ddd5ab7 --- /dev/null +++ b/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.Designer.cs @@ -0,0 +1,2363 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260510172703_AddZombieRoundSpecialType")] + partial class AddZombieRoundSpecialType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceBucketId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("int"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("SpecialType") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.cs b/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.cs new file mode 100644 index 000000000..0c67b73c0 --- /dev/null +++ b/Data/Migrations/MySql/20260510172703_AddZombieRoundSpecialType.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddZombieRoundSpecialType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs b/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs index 23aa22b97..ca9794536 100644 --- a/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs @@ -407,6 +407,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MeansOfDeathId") .HasColumnType("int"); + b.Property("PerformanceBucketId") + .HasColumnType("int"); + b.Property("ReceivedHitCount") .HasColumnType("int"); @@ -437,6 +440,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("MeansOfDeathId"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("ServerId"); b.HasIndex("WeaponAttachmentComboId"); @@ -465,6 +470,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Newest") .HasColumnType("tinyint(1)"); + b.Property("PerformanceBucketId") + .HasColumnType("int"); + b.Property("PerformanceMetric") .HasColumnType("double"); @@ -486,6 +494,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatedDateTime"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("Ranking"); b.HasIndex("ServerId"); @@ -518,6 +528,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFClientRatingHistory", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("StatTagId") + .HasColumnType("int"); + + b.Property("StatValue") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.Property("ClientId") @@ -616,6 +681,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFHitLocationCounts", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => { b.Property("RatingId") @@ -1181,11 +1267,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsPasswordProtected") .HasColumnType("tinyint(1)"); + b.Property("PerformanceBucketId") + .HasColumnType("int"); + b.Property("Port") .HasColumnType("int"); b.HasKey("ServerId"); + b.HasIndex("PerformanceBucketId"); + b.ToTable("EFServers", (string)null); }); @@ -1278,6 +1369,355 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Vector3", (string)null); }); + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("int"); + + b.Property("BuildablesCompleted") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("DoorsOpened") + .HasColumnType("int"); + + b.Property("Downs") + .HasColumnType("int"); + + b.Property("HeadshotKills") + .HasColumnType("int"); + + b.Property("Headshots") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("Melees") + .HasColumnType("int"); + + b.Property("PerksConsumed") + .HasColumnType("int"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("int"); + + b.Property("Revives") + .HasColumnType("int"); + + b.Property("TrapsActivated") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("WeaponsPurchased") + .HasColumnType("int"); + + b.Property("WeaponsUpgraded") + .HasColumnType("int"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("MatchId") + .HasColumnType("int"); + + b.Property("NumericalValue") + .HasColumnType("double"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("TextualValue") + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("int"); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("EasterEggRound") + .HasColumnType("int"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("MatchEndDate") + .HasColumnType("datetime(6)"); + + b.Property("MatchStartDate") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("PlayerCount") + .HasColumnType("int"); + + b.Property("EmaSeconds") + .HasColumnType("double"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double"); + + b.Property("AverageDowns") + .HasColumnType("double"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double"); + + b.Property("AverageMelees") + .HasColumnType("double"); + + b.Property("AveragePoints") + .HasColumnType("double"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double"); + + b.Property("AverageRevives") + .HasColumnType("double"); + + b.Property("AverageRoundReached") + .HasColumnType("double"); + + b.Property("AverageSoloFactor") + .HasColumnType("double"); + + b.Property("HeadshotPercentage") + .HasColumnType("double"); + + b.Property("HighestRound") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("int"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("int"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("int"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("int"); + + b.Property("SoloFromRound") + .HasColumnType("int"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("int"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("RoundNumber") + .HasColumnType("int"); + + b.Property("SpecialType") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TimeAlive") + .HasColumnType("time(6)"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => { b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") @@ -1464,6 +1904,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("MeansOfDeathId"); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); @@ -1482,6 +1926,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("MeansOfDeath"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); b.Navigation("Weapon"); @@ -1497,12 +1943,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); b.Navigation("Client"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); }); @@ -1517,6 +1969,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Client"); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.HasOne("Data.Models.Client.EFClient", "Client") @@ -1700,6 +2171,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SourceClient"); }); + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => { b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") @@ -1730,6 +2210,118 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + 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"); @@ -1737,6 +2329,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Meta"); b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); }); modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => diff --git a/Data/Migrations/Postgresql/20260411134934_AddZombieStats.Designer.cs b/Data/Migrations/Postgresql/20260411134934_AddZombieStats.Designer.cs new file mode 100644 index 000000000..fb93206c7 --- /dev/null +++ b/Data/Migrations/Postgresql/20260411134934_AddZombieStats.Designer.cs @@ -0,0 +1,2281 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260411134934_AddZombieStats")] + partial class AddZombieStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260411134934_AddZombieStats.cs b/Data/Migrations/Postgresql/20260411134934_AddZombieStats.cs new file mode 100644 index 000000000..2067532fd --- /dev/null +++ b/Data/Migrations/Postgresql/20260411134934_AddZombieStats.cs @@ -0,0 +1,468 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFServers", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "EFClientStatTags", + columns: table => new + { + ZombieStatTagId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TagName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTags", x => x.ZombieStatTagId); + }); + + migrationBuilder.CreateTable( + name: "EFZombieMatches", + columns: table => new + { + ZombieMatchId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MapId = table.Column(type: "integer", nullable: true), + ServerId = table.Column(type: "bigint", nullable: true), + ClientsCompleted = table.Column(type: "integer", nullable: false), + PlayerCount = table.Column(type: "integer", nullable: true), + HighestRound = table.Column(type: "integer", nullable: false), + MatchStartDate = table.Column(type: "timestamp with time zone", nullable: false), + MatchEndDate = table.Column(type: "timestamp with time zone", nullable: true), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatches", x => x.ZombieMatchId); + table.ForeignKey( + name: "FK_EFZombieMatches_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId"); + table.ForeignKey( + name: "FK_EFZombieMatches_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + }); + + migrationBuilder.CreateTable( + name: "PerformanceBuckets", + columns: table => new + { + PerformanceBucketId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PerformanceBuckets", x => x.PerformanceBucketId); + }); + + migrationBuilder.CreateTable( + name: "EFClientStatTagValues", + columns: table => new + { + ZombieClientStatTagValueId = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StatValue = table.Column(type: "integer", nullable: true), + StatTagId = table.Column(type: "integer", nullable: false), + ClientId = table.Column(type: "integer", nullable: false), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTagValues", x => x.ZombieClientStatTagValueId); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClientStatTags_StatTagId", + column: x => x.StatTagId, + principalTable: "EFClientStatTags", + principalColumn: "ZombieStatTagId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MatchId = table.Column(type: "integer", nullable: true), + ClientId = table.Column(type: "integer", nullable: false), + Kills = table.Column(type: "integer", nullable: false), + Deaths = table.Column(type: "integer", nullable: false), + DamageDealt = table.Column(type: "bigint", nullable: false), + DamageReceived = table.Column(type: "integer", nullable: false), + Headshots = table.Column(type: "integer", nullable: false), + HeadshotKills = table.Column(type: "integer", nullable: false), + Melees = table.Column(type: "integer", nullable: false), + Downs = table.Column(type: "integer", nullable: false), + Revives = table.Column(type: "integer", nullable: false), + PointsEarned = table.Column(type: "bigint", nullable: false), + PointsSpent = table.Column(type: "bigint", nullable: false), + PerksConsumed = table.Column(type: "integer", nullable: false), + PowerupsGrabbed = table.Column(type: "integer", nullable: false), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }); + + migrationBuilder.CreateTable( + name: "EFZombieEvents", + columns: table => new + { + ZombieEventLogId = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EventType = table.Column(type: "integer", nullable: false), + SourceClientId = table.Column(type: "integer", nullable: true), + AssociatedClientId = table.Column(type: "integer", nullable: true), + NumericalValue = table.Column(type: "double precision", nullable: true), + TextualValue = table.Column(type: "text", nullable: true), + MatchId = table.Column(type: "integer", nullable: true), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(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_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatAggregates", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false), + ServerId = table.Column(type: "bigint", nullable: true), + AverageKillsPerDown = table.Column(type: "double precision", nullable: false), + AverageDowns = table.Column(type: "double precision", nullable: false), + AverageRevives = table.Column(type: "double precision", nullable: false), + HeadshotPercentage = table.Column(type: "double precision", nullable: false), + AlivePercentage = table.Column(type: "double precision", nullable: false), + AverageMelees = table.Column(type: "double precision", nullable: false), + AverageRoundReached = table.Column(type: "double precision", nullable: false), + AveragePoints = table.Column(type: "double precision", nullable: false), + HighestRound = table.Column(type: "integer", nullable: false), + TotalRoundsPlayed = table.Column(type: "integer", nullable: false), + TotalMatchesPlayed = table.Column(type: "integer", nullable: false), + TotalMatchesCompleted = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatAggregates", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFZombieClientStats_ZombieClie~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieMatchClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatchClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieMatchClientStats_EFZombieClientStats_ZombieClientSt~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieRoundClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "bigint", nullable: false), + StartTime = table.Column(type: "timestamp with time zone", nullable: false), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + Duration = table.Column(type: "interval", nullable: true), + TimeAlive = table.Column(type: "interval", nullable: true), + RoundNumber = table.Column(type: "integer", nullable: false), + Points = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieRoundClientStats_EFZombieClientStats_ZombieClientSt~", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatRecords", + columns: table => new + { + ZombieClientStatRecordId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Type = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false), + ClientId = table.Column(type: "integer", nullable: true), + RoundId = table.Column(type: "bigint", nullable: true), + CreatedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatRecords", x => x.ZombieClientStatRecordId); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFZombieRoundClientStats_RoundId", + column: x => x.RoundId, + principalTable: "EFZombieRoundClientStats", + principalColumn: "ZombieClientStatId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_ClientId", + table: "EFClientStatTagValues", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_StatTagId", + table: "EFClientStatTagValues", + column: "StatTagId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatAggregates_ServerId", + table: "EFZombieClientStatAggregates", + column: "ServerId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_ClientId", + table: "EFZombieClientStatRecords", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_RoundId", + table: "EFZombieClientStatRecords", + column: "RoundId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_ClientId", + table: "EFZombieClientStats", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_MatchId", + table: "EFZombieClientStats", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_AssociatedClientId", + table: "EFZombieEvents", + column: "AssociatedClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_MatchId", + table: "EFZombieEvents", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_SourceClientId", + table: "EFZombieEvents", + column: "SourceClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_MapId", + table: "EFZombieMatches", + column: "MapId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucket~", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucket~", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropTable( + name: "EFClientStatTagValues"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatAggregates"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatRecords"); + + migrationBuilder.DropTable( + name: "EFZombieEvents"); + + migrationBuilder.DropTable( + name: "EFZombieMatchClientStats"); + + migrationBuilder.DropTable( + name: "PerformanceBuckets"); + + migrationBuilder.DropTable( + name: "EFClientStatTags"); + + migrationBuilder.DropTable( + name: "EFZombieRoundClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieMatches"); + + migrationBuilder.DropIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.Designer.cs b/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.Designer.cs new file mode 100644 index 000000000..f62714634 --- /dev/null +++ b/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.Designer.cs @@ -0,0 +1,2299 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260414171743_AddZombieEconomyStats")] + partial class AddZombieEconomyStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.cs b/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.cs new file mode 100644 index 000000000..d0147a0c7 --- /dev/null +++ b/Data/Migrations/Postgresql/20260414171743_AddZombieEconomyStats.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieEconomyStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BoxUses", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DoorsOpened", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TrapsActivated", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BoxUses", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "DoorsOpened", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "TrapsActivated", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.Designer.cs b/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.Designer.cs new file mode 100644 index 000000000..08eff7c25 --- /dev/null +++ b/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.Designer.cs @@ -0,0 +1,2299 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260418155402_DedupeEFMaps")] + partial class DedupeEFMaps + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.cs b/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.cs new file mode 100644 index 000000000..bab07f869 --- /dev/null +++ b/Data/Migrations/Postgresql/20260418155402_DedupeEFMaps.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class DedupeEFMaps : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Merge pre-existing duplicate EFMaps rows that share (Name, Game). These were + // produced by the pre-fix race in ServerDataCollector.GetOrCreateMap — two + // concurrent server scans both inserting the same (Name, Game) row. + // Re-point FK references onto the canonical (lowest) MapId per group, then + // delete the orphans. No schema change; data cleanup only. + + migrationBuilder.Sql(@" + WITH canonical AS ( + SELECT ""Name"", ""Game"", MIN(""MapId"") AS canonical_id + FROM ""EFMaps"" + GROUP BY ""Name"", ""Game"" + ) + UPDATE ""EFServerSnapshot"" s + SET ""MapId"" = c.canonical_id + FROM ""EFMaps"" m + INNER JOIN canonical c ON m.""Name"" = c.""Name"" AND m.""Game"" = c.""Game"" + WHERE s.""MapId"" = m.""MapId"" AND m.""MapId"" <> c.canonical_id;"); + + migrationBuilder.Sql(@" + WITH canonical AS ( + SELECT ""Name"", ""Game"", MIN(""MapId"") AS canonical_id + FROM ""EFMaps"" + GROUP BY ""Name"", ""Game"" + ) + UPDATE ""EFZombieMatches"" z + SET ""MapId"" = c.canonical_id + FROM ""EFMaps"" m + INNER JOIN canonical c ON m.""Name"" = c.""Name"" AND m.""Game"" = c.""Game"" + WHERE z.""MapId"" = m.""MapId"" AND m.""MapId"" <> c.canonical_id;"); + + migrationBuilder.Sql(@" + DELETE FROM ""EFMaps"" + WHERE ""MapId"" NOT IN ( + SELECT MIN(""MapId"") FROM ""EFMaps"" GROUP BY ""Name"", ""Game"" + );"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Dedupe is not reversible — the orphan rows are gone. + } + } +} diff --git a/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs b/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs new file mode 100644 index 000000000..16b2a804f --- /dev/null +++ b/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs @@ -0,0 +1,2309 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset")] + partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs b/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs new file mode 100644 index 000000000..fc49eebcd --- /dev/null +++ b/Data/Migrations/Postgresql/20260425193428_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches"); + + migrationBuilder.AddColumn( + name: "GameMatchId", + table: "EFZombieMatches", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches", + columns: new[] { "ServerId", "GameMatchId", "MatchEndDate" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "GameMatchId", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats"); + + migrationBuilder.DropColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.Designer.cs b/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.Designer.cs new file mode 100644 index 000000000..2325d692e --- /dev/null +++ b/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.Designer.cs @@ -0,0 +1,2315 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260428195634_AddZombieMatchEasterEgg")] + partial class AddZombieMatchEasterEgg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.cs b/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.cs new file mode 100644 index 000000000..3cc597f5d --- /dev/null +++ b/Data/Migrations/Postgresql/20260428195634_AddZombieMatchEasterEgg.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieMatchEasterEgg : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "EasterEggRound", + table: "EFZombieMatches", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "EasterEggRound", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs b/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs new file mode 100644 index 000000000..db5f8cdd6 --- /dev/null +++ b/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs @@ -0,0 +1,2354 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260508225932_AddZombieRoundEmaSpeedAndSoloFactor")] + partial class AddZombieRoundEmaSpeedAndSoloFactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.cs b/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.cs new file mode 100644 index 000000000..f736ff9ae --- /dev/null +++ b/Data/Migrations/Postgresql/20260508225932_AddZombieRoundEmaSpeedAndSoloFactor.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieRoundEmaSpeedAndSoloFactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates", + type: "double precision", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.AddColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates", + type: "double precision", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.CreateTable( + name: "EFZombieRoundDurationEmas", + columns: table => new + { + MapId = table.Column(type: "integer", nullable: false), + RoundNumber = table.Column(type: "integer", nullable: false), + PlayerCount = table.Column(type: "integer", nullable: false), + EmaSeconds = table.Column(type: "double precision", nullable: false), + SampleCount = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundDurationEmas", x => new { x.MapId, x.RoundNumber, x.PlayerCount }); + table.ForeignKey( + name: "FK_EFZombieRoundDurationEmas_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFZombieRoundDurationEmas"); + + migrationBuilder.DropColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates"); + + migrationBuilder.DropColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.Designer.cs b/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.Designer.cs new file mode 100644 index 000000000..798dec376 --- /dev/null +++ b/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.Designer.cs @@ -0,0 +1,2354 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260508231307_RenamePerformanceBucketsTable")] + partial class RenamePerformanceBucketsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.cs b/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.cs new file mode 100644 index 000000000..1d174eaae --- /dev/null +++ b/Data/Migrations/Postgresql/20260508231307_RenamePerformanceBucketsTable.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class RenamePerformanceBucketsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucket~", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "PerformanceBuckets", + newName: "EFPerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucke~", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBuck~", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucke~", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBuck~", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "EFPerformanceBuckets", + newName: "PerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucket~", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.Designer.cs b/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.Designer.cs new file mode 100644 index 000000000..76aba862f --- /dev/null +++ b/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.Designer.cs @@ -0,0 +1,2357 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260509104030_AddZombieMatchCompletedFlag")] + partial class AddZombieMatchCompletedFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.cs b/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.cs new file mode 100644 index 000000000..ea1606c89 --- /dev/null +++ b/Data/Migrations/Postgresql/20260509104030_AddZombieMatchCompletedFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieMatchCompletedFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Completed", + table: "EFZombieMatches", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Completed", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.Designer.cs b/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.Designer.cs new file mode 100644 index 000000000..d530e7167 --- /dev/null +++ b/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.Designer.cs @@ -0,0 +1,2360 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260509155917_AddZombieRoundPlayerCountAtRoundStart")] + partial class AddZombieRoundPlayerCountAtRoundStart + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("integer"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.cs b/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.cs new file mode 100644 index 000000000..766f248bb --- /dev/null +++ b/Data/Migrations/Postgresql/20260509155917_AddZombieRoundPlayerCountAtRoundStart.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieRoundPlayerCountAtRoundStart : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.Designer.cs b/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.Designer.cs new file mode 100644 index 000000000..5987abdf4 --- /dev/null +++ b/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.Designer.cs @@ -0,0 +1,2363 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260510172645_AddZombieRoundSpecialType")] + partial class AddZombieRoundSpecialType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("text"); + + b.Property("TwoFactorSecret") + .HasColumnType("text"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("integer"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("SpecialType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.cs b/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.cs new file mode 100644 index 000000000..fdb5140b4 --- /dev/null +++ b/Data/Migrations/Postgresql/20260510172645_AddZombieRoundSpecialType.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddZombieRoundSpecialType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs b/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs index 6d0fa1c76..952f04b2c 100644 --- a/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs @@ -407,6 +407,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MeansOfDeathId") .HasColumnType("integer"); + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + b.Property("ReceivedHitCount") .HasColumnType("integer"); @@ -437,6 +440,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("MeansOfDeathId"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("ServerId"); b.HasIndex("WeaponAttachmentComboId"); @@ -465,6 +470,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Newest") .HasColumnType("boolean"); + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + b.Property("PerformanceMetric") .HasColumnType("double precision"); @@ -486,6 +494,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatedDateTime"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("Ranking"); b.HasIndex("ServerId"); @@ -518,6 +528,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFClientRatingHistory", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieStatTagId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatTagValueId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StatTagId") + .HasColumnType("integer"); + + b.Property("StatValue") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.Property("ClientId") @@ -616,6 +681,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFHitLocationCounts", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PerformanceBucketId")); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => { b.Property("RatingId") @@ -1181,11 +1267,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsPasswordProtected") .HasColumnType("boolean"); + b.Property("PerformanceBucketId") + .HasColumnType("integer"); + b.Property("Port") .HasColumnType("integer"); b.HasKey("ServerId"); + b.HasIndex("PerformanceBucketId"); + b.ToTable("EFServers", (string)null); }); @@ -1278,6 +1369,355 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Vector3", (string)null); }); + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatId")); + + b.Property("BoxUses") + .HasColumnType("integer"); + + b.Property("BuildablesCompleted") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DamageDealt") + .HasColumnType("bigint"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("DoorsOpened") + .HasColumnType("integer"); + + b.Property("Downs") + .HasColumnType("integer"); + + b.Property("HeadshotKills") + .HasColumnType("integer"); + + b.Property("Headshots") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("Melees") + .HasColumnType("integer"); + + b.Property("PerksConsumed") + .HasColumnType("integer"); + + b.Property("PointsEarned") + .HasColumnType("bigint"); + + b.Property("PointsSpent") + .HasColumnType("bigint"); + + b.Property("PowerupsGrabbed") + .HasColumnType("integer"); + + b.Property("Revives") + .HasColumnType("integer"); + + b.Property("TrapsActivated") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WeaponsPurchased") + .HasColumnType("integer"); + + b.Property("WeaponsUpgraded") + .HasColumnType("integer"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieClientStatRecordId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoundId") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieEventLogId")); + + b.Property("AssociatedClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("MatchId") + .HasColumnType("integer"); + + b.Property("NumericalValue") + .HasColumnType("double precision"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("TextualValue") + .HasColumnType("text"); + + b.Property("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("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ZombieMatchId")); + + b.Property("ClientsCompleted") + .HasColumnType("integer"); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EasterEggRound") + .HasColumnType("integer"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MatchEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MatchStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("PlayerCount") + .HasColumnType("integer"); + + b.Property("EmaSeconds") + .HasColumnType("double precision"); + + b.Property("SampleCount") + .HasColumnType("bigint"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("double precision"); + + b.Property("AverageDowns") + .HasColumnType("double precision"); + + b.Property("AverageKillsPerDown") + .HasColumnType("double precision"); + + b.Property("AverageMelees") + .HasColumnType("double precision"); + + b.Property("AveragePoints") + .HasColumnType("double precision"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("double precision"); + + b.Property("AverageRevives") + .HasColumnType("double precision"); + + b.Property("AverageRoundReached") + .HasColumnType("double precision"); + + b.Property("AverageSoloFactor") + .HasColumnType("double precision"); + + b.Property("HeadshotPercentage") + .HasColumnType("double precision"); + + b.Property("HighestRound") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("integer"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("integer"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("integer"); + + b.Property("SoloFromRound") + .HasColumnType("integer"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("integer"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("RoundNumber") + .HasColumnType("integer"); + + b.Property("SpecialType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeAlive") + .HasColumnType("interval"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => { b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") @@ -1464,6 +1904,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("MeansOfDeathId"); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); @@ -1482,6 +1926,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("MeansOfDeath"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); b.Navigation("Weapon"); @@ -1497,12 +1943,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); b.Navigation("Client"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); }); @@ -1517,6 +1969,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Client"); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.HasOne("Data.Models.Client.EFClient", "Client") @@ -1700,6 +2171,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SourceClient"); }); + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => { b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") @@ -1730,6 +2210,118 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + 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"); @@ -1737,6 +2329,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Meta"); b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); }); modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => diff --git a/Data/Migrations/Sqlite/20260411134922_AddZombieStats.Designer.cs b/Data/Migrations/Sqlite/20260411134922_AddZombieStats.Designer.cs new file mode 100644 index 000000000..38f4f07c3 --- /dev/null +++ b/Data/Migrations/Sqlite/20260411134922_AddZombieStats.Designer.cs @@ -0,0 +1,2206 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260411134922_AddZombieStats")] + partial class AddZombieStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("TEXT"); + + b.Property("MatchStartDate") + .HasColumnType("TEXT"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260411134922_AddZombieStats.cs b/Data/Migrations/Sqlite/20260411134922_AddZombieStats.cs new file mode 100644 index 000000000..33870468f --- /dev/null +++ b/Data/Migrations/Sqlite/20260411134922_AddZombieStats.cs @@ -0,0 +1,470 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFServers", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "EFClientStatTags", + columns: table => new + { + ZombieStatTagId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TagName = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTags", x => x.ZombieStatTagId); + }); + + migrationBuilder.CreateTable( + name: "EFZombieMatches", + columns: table => new + { + ZombieMatchId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MapId = table.Column(type: "INTEGER", nullable: true), + ServerId = table.Column(type: "INTEGER", nullable: true), + ClientsCompleted = table.Column(type: "INTEGER", nullable: false), + PlayerCount = table.Column(type: "INTEGER", nullable: true), + HighestRound = table.Column(type: "INTEGER", nullable: false), + MatchStartDate = table.Column(type: "TEXT", nullable: false), + MatchEndDate = table.Column(type: "TEXT", nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatches", x => x.ZombieMatchId); + table.ForeignKey( + name: "FK_EFZombieMatches_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId"); + table.ForeignKey( + name: "FK_EFZombieMatches_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + }); + + migrationBuilder.CreateTable( + name: "PerformanceBuckets", + columns: table => new + { + PerformanceBucketId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Code = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PerformanceBuckets", x => x.PerformanceBucketId); + }); + + migrationBuilder.CreateTable( + name: "EFClientStatTagValues", + columns: table => new + { + ZombieClientStatTagValueId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StatValue = table.Column(type: "INTEGER", nullable: true), + StatTagId = table.Column(type: "INTEGER", nullable: false), + ClientId = table.Column(type: "INTEGER", nullable: false), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFClientStatTagValues", x => x.ZombieClientStatTagValueId); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClientStatTags_StatTagId", + column: x => x.StatTagId, + principalTable: "EFClientStatTags", + principalColumn: "ZombieStatTagId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFClientStatTagValues_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MatchId = table.Column(type: "INTEGER", nullable: true), + ClientId = table.Column(type: "INTEGER", nullable: false), + Kills = table.Column(type: "INTEGER", nullable: false), + Deaths = table.Column(type: "INTEGER", nullable: false), + DamageDealt = table.Column(type: "INTEGER", nullable: false), + DamageReceived = table.Column(type: "INTEGER", nullable: false), + Headshots = table.Column(type: "INTEGER", nullable: false), + HeadshotKills = table.Column(type: "INTEGER", nullable: false), + Melees = table.Column(type: "INTEGER", nullable: false), + Downs = table.Column(type: "INTEGER", nullable: false), + Revives = table.Column(type: "INTEGER", nullable: false), + PointsEarned = table.Column(type: "INTEGER", nullable: false), + PointsSpent = table.Column(type: "INTEGER", nullable: false), + PerksConsumed = table.Column(type: "INTEGER", nullable: false), + PowerupsGrabbed = table.Column(type: "INTEGER", nullable: false), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EFZombieClientStats_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }); + + migrationBuilder.CreateTable( + name: "EFZombieEvents", + columns: table => new + { + ZombieEventLogId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventType = table.Column(type: "INTEGER", nullable: false), + SourceClientId = table.Column(type: "INTEGER", nullable: true), + AssociatedClientId = table.Column(type: "INTEGER", nullable: true), + NumericalValue = table.Column(type: "REAL", nullable: true), + TextualValue = table.Column(type: "TEXT", nullable: true), + MatchId = table.Column(type: "INTEGER", nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieEvents", x => x.ZombieEventLogId); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_AssociatedClientId", + column: x => x.AssociatedClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFClients_SourceClientId", + column: x => x.SourceClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieEvents_EFZombieMatches_MatchId", + column: x => x.MatchId, + principalTable: "EFZombieMatches", + principalColumn: "ZombieMatchId"); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatAggregates", + columns: table => new + { + ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ServerId = table.Column(type: "INTEGER", nullable: true), + AverageKillsPerDown = table.Column(type: "REAL", nullable: false), + AverageDowns = table.Column(type: "REAL", nullable: false), + AverageRevives = table.Column(type: "REAL", nullable: false), + HeadshotPercentage = table.Column(type: "REAL", nullable: false), + AlivePercentage = table.Column(type: "REAL", nullable: false), + AverageMelees = table.Column(type: "REAL", nullable: false), + AverageRoundReached = table.Column(type: "REAL", nullable: false), + AveragePoints = table.Column(type: "REAL", nullable: false), + HighestRound = table.Column(type: "INTEGER", nullable: false), + TotalRoundsPlayed = table.Column(type: "INTEGER", nullable: false), + TotalMatchesPlayed = table.Column(type: "INTEGER", nullable: false), + TotalMatchesCompleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatAggregates", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFServers_ServerId", + column: x => x.ServerId, + principalTable: "EFServers", + principalColumn: "ServerId"); + table.ForeignKey( + name: "FK_EFZombieClientStatAggregates_EFZombieClientStats_ZombieClientStatId", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieMatchClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieMatchClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieMatchClientStats_EFZombieClientStats_ZombieClientStatId", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieRoundClientStats", + columns: table => new + { + ZombieClientStatId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StartTime = table.Column(type: "TEXT", nullable: false), + EndTime = table.Column(type: "TEXT", nullable: true), + Duration = table.Column(type: "TEXT", nullable: true), + TimeAlive = table.Column(type: "TEXT", nullable: true), + RoundNumber = table.Column(type: "INTEGER", nullable: false), + Points = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundClientStats", x => x.ZombieClientStatId); + table.ForeignKey( + name: "FK_EFZombieRoundClientStats_EFZombieClientStats_ZombieClientStatId", + column: x => x.ZombieClientStatId, + principalTable: "EFZombieClientStats", + principalColumn: "ZombieClientStatId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EFZombieClientStatRecords", + columns: table => new + { + ZombieClientStatRecordId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: false), + ClientId = table.Column(type: "INTEGER", nullable: true), + RoundId = table.Column(type: "INTEGER", nullable: true), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieClientStatRecords", x => x.ZombieClientStatRecordId); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFClients_ClientId", + column: x => x.ClientId, + principalTable: "EFClients", + principalColumn: "ClientId"); + table.ForeignKey( + name: "FK_EFZombieClientStatRecords_EFZombieRoundClientStats_RoundId", + column: x => x.RoundId, + principalTable: "EFZombieRoundClientStats", + principalColumn: "ZombieClientStatId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_ClientId", + table: "EFClientStatTagValues", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFClientStatTagValues_StatTagId", + table: "EFClientStatTagValues", + column: "StatTagId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatAggregates_ServerId", + table: "EFZombieClientStatAggregates", + column: "ServerId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_ClientId", + table: "EFZombieClientStatRecords", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStatRecords_RoundId", + table: "EFZombieClientStatRecords", + column: "RoundId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_ClientId", + table: "EFZombieClientStats", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieClientStats_MatchId", + table: "EFZombieClientStats", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_AssociatedClientId", + table: "EFZombieEvents", + column: "AssociatedClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_MatchId", + table: "EFZombieEvents", + column: "MatchId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieEvents_SourceClientId", + table: "EFZombieEvents", + column: "SourceClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_MapId", + table: "EFZombieMatches", + column: "MapId"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropTable( + name: "EFClientStatTagValues"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatAggregates"); + + migrationBuilder.DropTable( + name: "EFZombieClientStatRecords"); + + migrationBuilder.DropTable( + name: "EFZombieEvents"); + + migrationBuilder.DropTable( + name: "EFZombieMatchClientStats"); + + migrationBuilder.DropTable( + name: "PerformanceBuckets"); + + migrationBuilder.DropTable( + name: "EFClientStatTags"); + + migrationBuilder.DropTable( + name: "EFZombieRoundClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieClientStats"); + + migrationBuilder.DropTable( + name: "EFZombieMatches"); + + migrationBuilder.DropIndex( + name: "IX_EFServers_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropIndex( + name: "IX_EFClientRankingHistory_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropIndex( + name: "IX_EFClientHitStatistics_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropColumn( + name: "PerformanceBucketId", + table: "EFClientHitStatistics"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.Designer.cs b/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.Designer.cs new file mode 100644 index 000000000..1d7ad7682 --- /dev/null +++ b/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.Designer.cs @@ -0,0 +1,2224 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260414171730_AddZombieEconomyStats")] + partial class AddZombieEconomyStats + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("TEXT"); + + b.Property("MatchStartDate") + .HasColumnType("TEXT"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.cs b/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.cs new file mode 100644 index 000000000..343e06a91 --- /dev/null +++ b/Data/Migrations/Sqlite/20260414171730_AddZombieEconomyStats.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieEconomyStats : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BoxUses", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DoorsOpened", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TrapsActivated", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BoxUses", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "BuildablesCompleted", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "DoorsOpened", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "TrapsActivated", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsPurchased", + table: "EFZombieClientStats"); + + migrationBuilder.DropColumn( + name: "WeaponsUpgraded", + table: "EFZombieClientStats"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.Designer.cs b/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.Designer.cs new file mode 100644 index 000000000..d085c02d6 --- /dev/null +++ b/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.Designer.cs @@ -0,0 +1,2224 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260418155355_DedupeEFMaps")] + partial class DedupeEFMaps + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("TEXT"); + + b.Property("MatchStartDate") + .HasColumnType("TEXT"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.cs b/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.cs new file mode 100644 index 000000000..32a95621c --- /dev/null +++ b/Data/Migrations/Sqlite/20260418155355_DedupeEFMaps.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class DedupeEFMaps : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Merge pre-existing duplicate EFMaps rows that share (Name, Game). These were + // produced by the pre-fix race in ServerDataCollector.GetOrCreateMap — two + // concurrent server scans both inserting the same (Name, Game) row. + // Re-point FK references onto the canonical (lowest) MapId per group, then + // delete the orphans. No schema change; data cleanup only. + + migrationBuilder.Sql(@" + UPDATE ""EFServerSnapshot"" + SET ""MapId"" = ( + SELECT MIN(canonical.""MapId"") + FROM ""EFMaps"" canonical + INNER JOIN ""EFMaps"" current ON canonical.""Name"" = current.""Name"" + AND canonical.""Game"" = current.""Game"" + WHERE current.""MapId"" = ""EFServerSnapshot"".""MapId"" + ) + WHERE ""MapId"" IS NOT NULL;"); + + migrationBuilder.Sql(@" + UPDATE ""EFZombieMatches"" + SET ""MapId"" = ( + SELECT MIN(canonical.""MapId"") + FROM ""EFMaps"" canonical + INNER JOIN ""EFMaps"" current ON canonical.""Name"" = current.""Name"" + AND canonical.""Game"" = current.""Game"" + WHERE current.""MapId"" = ""EFZombieMatches"".""MapId"" + ) + WHERE ""MapId"" IS NOT NULL;"); + + migrationBuilder.Sql(@" + DELETE FROM ""EFMaps"" + WHERE ""MapId"" NOT IN ( + SELECT MIN(""MapId"") FROM ""EFMaps"" GROUP BY ""Name"", ""Game"" + );"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Dedupe is not reversible — the orphan rows are gone. + } + } +} diff --git a/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs b/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs new file mode 100644 index 000000000..95488dbb0 --- /dev/null +++ b/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.Designer.cs @@ -0,0 +1,2234 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset")] + partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs b/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs new file mode 100644 index 000000000..2aff93d53 --- /dev/null +++ b/Data/Migrations/Sqlite/20260425193420_AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset.cs @@ -0,0 +1,340 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieStitchingAndAssistedRoundsAndSqliteDateTimeOffset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "EFZombieRoundClientStats", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "EFZombieRoundClientStats", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieMatches", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MatchStartDate", + table: "EFZombieMatches", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "MatchEndDate", + table: "EFZombieMatches", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieMatches", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AddColumn( + name: "GameMatchId", + table: "EFZombieMatches", + type: "TEXT", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats", + type: "INTEGER", + nullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieEvents", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieEvents", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieClientStats", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieClientStatRecords", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieClientStatRecords", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFClientStatTagValues", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFClientStatTagValues", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFClientStatTags", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFClientStatTags", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches", + columns: new[] { "ServerId", "GameMatchId", "MatchEndDate" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFZombieMatches_ServerId_GameMatchId_MatchEndDate", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "GameMatchId", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "AssistedRounds", + table: "EFZombieMatchClientStats"); + + migrationBuilder.DropColumn( + name: "SoloFromRound", + table: "EFZombieMatchClientStats"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "EFZombieRoundClientStats", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "EFZombieRoundClientStats", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieMatches", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MatchStartDate", + table: "EFZombieMatches", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "MatchEndDate", + table: "EFZombieMatches", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieMatches", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieEvents", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieEvents", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieClientStats", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieClientStats", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFZombieClientStatRecords", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFZombieClientStatRecords", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFClientStatTagValues", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFClientStatTagValues", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + name: "UpdatedDateTime", + table: "EFClientStatTags", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDateTime", + table: "EFClientStatTags", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + migrationBuilder.CreateIndex( + name: "IX_EFZombieMatches_ServerId", + table: "EFZombieMatches", + column: "ServerId"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.Designer.cs b/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.Designer.cs new file mode 100644 index 000000000..acf9f214e --- /dev/null +++ b/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.Designer.cs @@ -0,0 +1,2240 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260428195623_AddZombieMatchEasterEgg")] + partial class AddZombieMatchEasterEgg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.cs b/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.cs new file mode 100644 index 000000000..c44963f70 --- /dev/null +++ b/Data/Migrations/Sqlite/20260428195623_AddZombieMatchEasterEgg.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieMatchEasterEgg : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "EasterEggRound", + table: "EFZombieMatches", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EasterEggOccurredAt", + table: "EFZombieMatches"); + + migrationBuilder.DropColumn( + name: "EasterEggRound", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs b/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs new file mode 100644 index 000000000..8b2a6827b --- /dev/null +++ b/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.Designer.cs @@ -0,0 +1,2279 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260508225925_AddZombieRoundEmaSpeedAndSoloFactor")] + partial class AddZombieRoundEmaSpeedAndSoloFactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("PerformanceBuckets"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.cs b/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.cs new file mode 100644 index 000000000..0938ff596 --- /dev/null +++ b/Data/Migrations/Sqlite/20260508225925_AddZombieRoundEmaSpeedAndSoloFactor.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieRoundEmaSpeedAndSoloFactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates", + type: "REAL", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.AddColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates", + type: "REAL", + nullable: false, + defaultValue: 1.0); + + migrationBuilder.CreateTable( + name: "EFZombieRoundDurationEmas", + columns: table => new + { + MapId = table.Column(type: "INTEGER", nullable: false), + RoundNumber = table.Column(type: "INTEGER", nullable: false), + PlayerCount = table.Column(type: "INTEGER", nullable: false), + EmaSeconds = table.Column(type: "REAL", nullable: false), + SampleCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EFZombieRoundDurationEmas", x => new { x.MapId, x.RoundNumber, x.PlayerCount }); + table.ForeignKey( + name: "FK_EFZombieRoundDurationEmas_EFMaps_MapId", + column: x => x.MapId, + principalTable: "EFMaps", + principalColumn: "MapId", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFZombieRoundDurationEmas"); + + migrationBuilder.DropColumn( + name: "AverageRelativeSpeed", + table: "EFZombieClientStatAggregates"); + + migrationBuilder.DropColumn( + name: "AverageSoloFactor", + table: "EFZombieClientStatAggregates"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.Designer.cs b/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.Designer.cs new file mode 100644 index 000000000..a3e6f07e2 --- /dev/null +++ b/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.Designer.cs @@ -0,0 +1,2279 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260508231247_RenamePerformanceBucketsTable")] + partial class RenamePerformanceBucketsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.cs b/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.cs new file mode 100644 index 000000000..61680a861 --- /dev/null +++ b/Data/Migrations/Sqlite/20260508231247_RenamePerformanceBucketsTable.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class RenamePerformanceBucketsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "PerformanceBuckets", + newName: "EFPerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "EFPerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_EFClientHitStatistics_EFPerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics"); + + migrationBuilder.DropForeignKey( + name: "FK_EFClientRankingHistory_EFPerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory"); + + migrationBuilder.DropForeignKey( + name: "FK_EFServers_EFPerformanceBuckets_PerformanceBucketId", + table: "EFServers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_EFPerformanceBuckets", + table: "EFPerformanceBuckets"); + + migrationBuilder.RenameTable( + name: "EFPerformanceBuckets", + newName: "PerformanceBuckets"); + + migrationBuilder.AddPrimaryKey( + name: "PK_PerformanceBuckets", + table: "PerformanceBuckets", + column: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientHitStatistics_PerformanceBuckets_PerformanceBucketId", + table: "EFClientHitStatistics", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFClientRankingHistory_PerformanceBuckets_PerformanceBucketId", + table: "EFClientRankingHistory", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + + migrationBuilder.AddForeignKey( + name: "FK_EFServers_PerformanceBuckets_PerformanceBucketId", + table: "EFServers", + column: "PerformanceBucketId", + principalTable: "PerformanceBuckets", + principalColumn: "PerformanceBucketId"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.Designer.cs b/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.Designer.cs new file mode 100644 index 000000000..01b0161a1 --- /dev/null +++ b/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.Designer.cs @@ -0,0 +1,2282 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260509104019_AddZombieMatchCompletedFlag")] + partial class AddZombieMatchCompletedFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.cs b/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.cs new file mode 100644 index 000000000..9dc2e5c1d --- /dev/null +++ b/Data/Migrations/Sqlite/20260509104019_AddZombieMatchCompletedFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieMatchCompletedFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Completed", + table: "EFZombieMatches", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Completed", + table: "EFZombieMatches"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.Designer.cs b/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.Designer.cs new file mode 100644 index 000000000..f02aa4bed --- /dev/null +++ b/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.Designer.cs @@ -0,0 +1,2285 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260509155904_AddZombieRoundPlayerCountAtRoundStart")] + partial class AddZombieRoundPlayerCountAtRoundStart + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.cs b/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.cs new file mode 100644 index 000000000..62e242d64 --- /dev/null +++ b/Data/Migrations/Sqlite/20260509155904_AddZombieRoundPlayerCountAtRoundStart.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieRoundPlayerCountAtRoundStart : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PlayerCountAtRoundStart", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.Designer.cs b/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.Designer.cs new file mode 100644 index 000000000..9617e90f0 --- /dev/null +++ b/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.Designer.cs @@ -0,0 +1,2288 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260510172638_AddZombieRoundSpecialType")] + partial class AddZombieRoundSpecialType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorBackupCodes") + .HasColumnType("TEXT"); + + b.Property("TwoFactorSecret") + .HasColumnType("TEXT"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("PerformanceBucketId"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.HasIndex("PerformanceBucketId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("SpecialType") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("PerformanceBucket"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("ZombieClientStats") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.Navigation("Client"); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round") + .WithMany() + .HasForeignKey("RoundId"); + + b.Navigation("Client"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.HasOne("Data.Models.Client.EFClient", "AssociatedClient") + .WithMany() + .HasForeignKey("AssociatedClientId"); + + b.HasOne("Data.Models.Zombie.ZombieMatch", "Match") + .WithMany() + .HasForeignKey("MatchId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId"); + + b.Navigation("AssociatedClient"); + + b.Navigation("Match"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasOne("Data.Models.Zombie.ZombieClientStat", null) + .WithOne() + .HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.cs b/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.cs new file mode 100644 index 000000000..98974af5e --- /dev/null +++ b/Data/Migrations/Sqlite/20260510172638_AddZombieRoundSpecialType.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddZombieRoundSpecialType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SpecialType", + table: "EFZombieRoundClientStats"); + } + } +} diff --git a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs index e5ce6669b..3399ae77b 100644 --- a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs @@ -388,6 +388,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MeansOfDeathId") .HasColumnType("INTEGER"); + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + b.Property("ReceivedHitCount") .HasColumnType("INTEGER"); @@ -418,6 +421,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("MeansOfDeathId"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("ServerId"); b.HasIndex("WeaponAttachmentComboId"); @@ -444,6 +449,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Newest") .HasColumnType("INTEGER"); + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + b.Property("PerformanceMetric") .HasColumnType("REAL"); @@ -465,6 +473,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatedDateTime"); + b.HasIndex("PerformanceBucketId"); + b.HasIndex("Ranking"); b.HasIndex("ServerId"); @@ -495,6 +505,57 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFClientRatingHistory", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTag", b => + { + b.Property("ZombieStatTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("TagName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieStatTagId"); + + b.ToTable("EFClientStatTags", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.Property("ZombieClientStatTagValueId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("StatTagId") + .HasColumnType("INTEGER"); + + b.Property("StatValue") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatTagValueId"); + + b.HasIndex("ClientId"); + + b.HasIndex("StatTagId"); + + b.ToTable("EFClientStatTagValues", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.Property("ClientId") @@ -591,6 +652,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFHitLocationCounts", (string)null); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFPerformanceBucket", b => + { + b.Property("PerformanceBucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("PerformanceBucketId"); + + b.ToTable("EFPerformanceBuckets", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => { b.Property("RatingId") @@ -1126,11 +1206,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsPasswordProtected") .HasColumnType("INTEGER"); + b.Property("PerformanceBucketId") + .HasColumnType("INTEGER"); + b.Property("Port") .HasColumnType("INTEGER"); b.HasKey("ServerId"); + b.HasIndex("PerformanceBucketId"); + b.ToTable("EFServers", (string)null); }); @@ -1217,6 +1302,347 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Vector3", (string)null); }); + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b => + { + b.Property("ZombieClientStatId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoxUses") + .HasColumnType("INTEGER"); + + b.Property("BuildablesCompleted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("DamageDealt") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("DoorsOpened") + .HasColumnType("INTEGER"); + + b.Property("Downs") + .HasColumnType("INTEGER"); + + b.Property("HeadshotKills") + .HasColumnType("INTEGER"); + + b.Property("Headshots") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("Melees") + .HasColumnType("INTEGER"); + + b.Property("PerksConsumed") + .HasColumnType("INTEGER"); + + b.Property("PointsEarned") + .HasColumnType("INTEGER"); + + b.Property("PointsSpent") + .HasColumnType("INTEGER"); + + b.Property("PowerupsGrabbed") + .HasColumnType("INTEGER"); + + b.Property("Revives") + .HasColumnType("INTEGER"); + + b.Property("TrapsActivated") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("WeaponsPurchased") + .HasColumnType("INTEGER"); + + b.Property("WeaponsUpgraded") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieClientStatId"); + + b.HasIndex("ClientId"); + + b.HasIndex("MatchId"); + + b.ToTable("EFZombieClientStats", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b => + { + b.Property("ZombieClientStatRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ZombieClientStatRecordId"); + + b.HasIndex("ClientId"); + + b.HasIndex("RoundId"); + + b.ToTable("EFZombieClientStatRecords", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieEventLog", b => + { + b.Property("ZombieEventLogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssociatedClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumericalValue") + .HasColumnType("REAL"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("TextualValue") + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieEventLogId"); + + b.HasIndex("AssociatedClientId"); + + b.HasIndex("MatchId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("EFZombieEvents", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b => + { + b.Property("ZombieMatchId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("INTEGER"); + + b.Property("EasterEggOccurredAt") + .HasColumnType("INTEGER"); + + b.Property("EasterEggRound") + .HasColumnType("INTEGER"); + + b.Property("GameMatchId") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("MatchEndDate") + .HasColumnType("INTEGER"); + + b.Property("MatchStartDate") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("INTEGER"); + + b.HasKey("ZombieMatchId"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId", "GameMatchId", "MatchEndDate"); + + b.ToTable("EFZombieMatches", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundDurationEma", b => + { + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("EmaSeconds") + .HasColumnType("REAL"); + + b.Property("SampleCount") + .HasColumnType("INTEGER"); + + b.HasKey("MapId", "RoundNumber", "PlayerCount"); + + b.ToTable("EFZombieRoundDurationEmas", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AlivePercentage") + .HasColumnType("REAL"); + + b.Property("AverageDowns") + .HasColumnType("REAL"); + + b.Property("AverageKillsPerDown") + .HasColumnType("REAL"); + + b.Property("AverageMelees") + .HasColumnType("REAL"); + + b.Property("AveragePoints") + .HasColumnType("REAL"); + + b.Property("AverageRelativeSpeed") + .HasColumnType("REAL"); + + b.Property("AverageRevives") + .HasColumnType("REAL"); + + b.Property("AverageRoundReached") + .HasColumnType("REAL"); + + b.Property("AverageSoloFactor") + .HasColumnType("REAL"); + + b.Property("HeadshotPercentage") + .HasColumnType("REAL"); + + b.Property("HighestRound") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesCompleted") + .HasColumnType("INTEGER"); + + b.Property("TotalMatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("INTEGER"); + + b.HasIndex("ServerId"); + + b.ToTable("EFZombieClientStatAggregates", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("AssistedRounds") + .HasColumnType("INTEGER"); + + b.Property("SoloFromRound") + .HasColumnType("INTEGER"); + + b.ToTable("EFZombieMatchClientStats", (string)null); + }); + + modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b => + { + b.HasBaseType("Data.Models.Zombie.ZombieClientStat"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("INTEGER"); + + b.Property("PlayerCountAtRoundStart") + .HasColumnType("INTEGER"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("RoundNumber") + .HasColumnType("INTEGER"); + + b.Property("SpecialType") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("INTEGER"); + + b.Property("TimeAlive") + .HasColumnType("TEXT"); + + b.ToTable("EFZombieRoundClientStats", (string)null); + }); + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => { b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") @@ -1403,6 +1829,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("MeansOfDeathId"); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); @@ -1421,6 +1851,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("MeansOfDeath"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); b.Navigation("Weapon"); @@ -1436,12 +1868,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + b.HasOne("Data.Models.Server.EFServer", "Server") .WithMany() .HasForeignKey("ServerId"); b.Navigation("Client"); + b.Navigation("PerformanceBucket"); + b.Navigation("Server"); }); @@ -1456,6 +1894,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Client"); }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatTagValue", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatTag", "StatTag") + .WithMany() + .HasForeignKey("StatTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("StatTag"); + }); + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => { b.HasOne("Data.Models.Client.EFClient", "Client") @@ -1639,6 +2096,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SourceClient"); }); + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.HasOne("Data.Models.Client.Stats.EFPerformanceBucket", "PerformanceBucket") + .WithMany() + .HasForeignKey("PerformanceBucketId"); + + b.Navigation("PerformanceBucket"); + }); + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => { b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") @@ -1669,6 +2135,118 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.ZombieRoundDurationEma", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + 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"); @@ -1676,6 +2254,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Meta"); b.Navigation("ReceivedPenalties"); + + b.Navigation("ZombieClientStats"); }); modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => diff --git a/Data/Models/Client/EFClient.cs b/Data/Models/Client/EFClient.cs index f9a90fad6..c30ff6e5a 100644 --- a/Data/Models/Client/EFClient.cs +++ b/Data/Models/Client/EFClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Zombie; namespace Data.Models.Client { @@ -85,5 +86,6 @@ public enum Permission public virtual ICollection Meta { get; set; } public virtual ICollection ReceivedPenalties { get; set; } public virtual ICollection AdministeredPenalties { get; set; } + public virtual ICollection ZombieClientStats { get; set; } } } diff --git a/Data/Models/Client/Stats/EFClientHitStatistic.cs b/Data/Models/Client/Stats/EFClientHitStatistic.cs index ad25716b1..5539a64fc 100644 --- a/Data/Models/Client/Stats/EFClientHitStatistic.cs +++ b/Data/Models/Client/Stats/EFClientHitStatistic.cs @@ -42,6 +42,11 @@ public class EFClientHitStatistic : AuditFields [ForeignKey(nameof(WeaponAttachmentComboId))] public virtual EFWeaponAttachmentCombo WeaponAttachmentCombo { get; set; } + + public int? PerformanceBucketId { get; set; } + + [ForeignKey(nameof(PerformanceBucketId))] + public virtual EFPerformanceBucket PerformanceBucket { get; set; } /// /// how many hits the player got diff --git a/Data/Models/Client/Stats/EFClientRankingHistory.cs b/Data/Models/Client/Stats/EFClientRankingHistory.cs index 6d067900f..ff71a90a1 100644 --- a/Data/Models/Client/Stats/EFClientRankingHistory.cs +++ b/Data/Models/Client/Stats/EFClientRankingHistory.cs @@ -25,5 +25,9 @@ public class EFClientRankingHistory: AuditFields public int? Ranking { get; set; } public double? ZScore { get; set; } public double? PerformanceMetric { get; set; } + + public int? PerformanceBucketId { get; set; } + [ForeignKey(nameof(PerformanceBucketId))] + public EFPerformanceBucket PerformanceBucket { get; set; } } } diff --git a/Data/Models/Client/Stats/EFClientStatTag.cs b/Data/Models/Client/Stats/EFClientStatTag.cs new file mode 100644 index 000000000..2cf9a46e5 --- /dev/null +++ b/Data/Models/Client/Stats/EFClientStatTag.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Data.Models.Client.Stats; + +public class EFClientStatTag : DatedRecord +{ + [Key] + public int ZombieStatTagId { get; set; } + + [MaxLength(128)] + public string TagName { get; set; } +} diff --git a/Data/Models/Client/Stats/EFClientStatTagValue.cs b/Data/Models/Client/Stats/EFClientStatTagValue.cs new file mode 100644 index 000000000..6e0dd9c58 --- /dev/null +++ b/Data/Models/Client/Stats/EFClientStatTagValue.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.Models.Client.Stats; + +public class EFClientStatTagValue : DatedRecord +{ + [Key] + public long ZombieClientStatTagValueId { get; set; } + + public int? StatValue { get; set; } + + [Required] + public int StatTagId { get; set; } + + [ForeignKey(nameof(StatTagId))] + public EFClientStatTag StatTag { get; set; } + + public int ClientId { get; set; } + + [ForeignKey(nameof(ClientId))] + public EFClient Client { get; set; } +} diff --git a/Data/Models/Client/Stats/EFPerformanceBucket.cs b/Data/Models/Client/Stats/EFPerformanceBucket.cs new file mode 100644 index 000000000..f869a9c30 --- /dev/null +++ b/Data/Models/Client/Stats/EFPerformanceBucket.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; + +namespace Data.Models.Client.Stats; + +/// +/// Groups game servers into isolated ranking pools so that different game modes +/// (e.g. multiplayer, zombies, competitive) maintain separate leaderboards and +/// performance distributions. Each server is optionally assigned to a bucket via +/// ; servers without a bucket use global defaults. +/// Rankings, Z-scores, and hit statistics are all scoped per-bucket. +/// +public class EFPerformanceBucket +{ + [Key] public int PerformanceBucketId { get; set; } + + /// + /// Unique string identifier used in configuration and queries (e.g. "zombies", "competitive"). + /// Matched against . + /// + /// Stored canonical form: lower-case. The writer in + /// IW4MServer.UpdatePerformanceBucket lower-cases on insert (and + /// normalises the + /// runtime value), so case-sensitive equality comparisons against this + /// column must use a lower-case comparand. Code-side, run any user-supplied + /// bucket code through SharedLibraryCore.Helpers.PerformanceBucketCodes.Normalize + /// before comparing — settings.json lets users write capitalised codes + /// (e.g. "Zombies") for readability, but the DB only sees the + /// normalised form. + /// + /// + [MaxLength(256)] + public string Code { get; set; } + + /// + /// Human-readable display name shown in the web UI and leaderboards. + /// + [MaxLength(256)] + public string Name { get; set; } +} diff --git a/Data/Models/DatedRecord.cs b/Data/Models/DatedRecord.cs new file mode 100644 index 000000000..f7fabce46 --- /dev/null +++ b/Data/Models/DatedRecord.cs @@ -0,0 +1,10 @@ +using System; + +namespace Data.Models; + +public class DatedRecord : IdentifierRecord +{ + public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? UpdatedDateTime { get; set; } + public override long Id { get; } +} diff --git a/Data/Models/IdentifierRecord.cs b/Data/Models/IdentifierRecord.cs new file mode 100644 index 000000000..99da333f9 --- /dev/null +++ b/Data/Models/IdentifierRecord.cs @@ -0,0 +1,6 @@ +namespace Data.Models; + +public abstract class IdentifierRecord +{ + public abstract long Id { get; } +} diff --git a/Data/Models/Server/EFServer.cs b/Data/Models/Server/EFServer.cs index 70a49cd49..4afb623f2 100644 --- a/Data/Models/Server/EFServer.cs +++ b/Data/Models/Server/EFServer.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Data.Abstractions; +using Data.Models.Client.Stats; namespace Data.Models.Server { @@ -15,6 +16,9 @@ public class EFServer : SharedEntity, IUniqueId public Reference.Game? GameName { get; set; } public string HostName { get; set; } public bool IsPasswordProtected { get; set; } + public int? PerformanceBucketId { get; set; } + [ForeignKey(nameof(PerformanceBucketId))] + public EFPerformanceBucket PerformanceBucket { get; set; } public long Id => ServerId; public string Value => EndPoint; } diff --git a/Data/Models/Zombie/ZombieAggregateClientStat.cs b/Data/Models/Zombie/ZombieAggregateClientStat.cs new file mode 100644 index 000000000..eb74d54dd --- /dev/null +++ b/Data/Models/Zombie/ZombieAggregateClientStat.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations.Schema; +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; } + public double AverageDowns { get; set; } + public double AverageRevives { get; set; } + public double HeadshotPercentage { get; set; } + public double AlivePercentage { get; set; } + public double AverageMelees { get; set; } + public double AverageRoundReached { get; set; } + public double AveragePoints { get; set; } + public double AverageRelativeSpeed { get; set; } = 1.0; + public double AverageSoloFactor { get; set; } = 1.0; + + #endregion + + #region Totals + + public int HighestRound { get; set; } + public int TotalRoundsPlayed { get; set; } + public int TotalMatchesPlayed { get; set; } + public int TotalMatchesCompleted { get; set; } + + #endregion +} diff --git a/Data/Models/Zombie/ZombieClientStat.cs b/Data/Models/Zombie/ZombieClientStat.cs new file mode 100644 index 000000000..6076ab677 --- /dev/null +++ b/Data/Models/Zombie/ZombieClientStat.cs @@ -0,0 +1,43 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Client; + +namespace Data.Models.Zombie; + +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))] + public virtual ZombieMatch? Match { get; set; } + + public int ClientId { get; set; } + [ForeignKey(nameof(ClientId))] + public virtual EFClient Client { get; set; } + + public int Kills { get; set; } + public int Deaths { 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; } + public long PointsEarned { get; set; } + public long PointsSpent { get; set; } + public int PerksConsumed { get; set; } + public int PowerupsGrabbed { get; set; } + public int WeaponsPurchased { get; set; } + public int WeaponsUpgraded { get; set; } + public int BoxUses { get; set; } + public int DoorsOpened { get; set; } + public int TrapsActivated { get; set; } + public int BuildablesCompleted { get; set; } +} diff --git a/Data/Models/Zombie/ZombieClientStatRecord.cs b/Data/Models/Zombie/ZombieClientStatRecord.cs new file mode 100644 index 000000000..5003fb7c0 --- /dev/null +++ b/Data/Models/Zombie/ZombieClientStatRecord.cs @@ -0,0 +1,32 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Client; + +namespace Data.Models.Zombie; + +public enum RecordType +{ + Maximum, + Minimum +} + +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; + + public int? ClientId { get; set; } + [ForeignKey(nameof(ClientId))] + public virtual EFClient? Client { get; set; } + + public long? RoundId { get; set; } + [ForeignKey(nameof(RoundId))] + public virtual ZombieRoundClientStat? Round { get; set; } +} diff --git a/Data/Models/Zombie/ZombieEventLog.cs b/Data/Models/Zombie/ZombieEventLog.cs new file mode 100644 index 000000000..0ace82274 --- /dev/null +++ b/Data/Models/Zombie/ZombieEventLog.cs @@ -0,0 +1,121 @@ +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, + WeaponPurchased = 14, + WeaponUpgraded = 15, + BoxTake = 16, + BoxPass = 17, + BoxTeddy = 18, + DoorPurchased = 19, + TrapActivated = 20, + BuildComplete = 21, + WeaponAbandoned = 22, + EasterEggCompleted = 23, + /// + /// Per-step EE progress marker. holds + /// the step key (e.g. "t4_vr_radio_1"); + /// holds the round number at fire time. Match-level event (no SourceClientId). + /// Idempotent — premium handler dedups via (MatchId, EventType, TextualValue). + /// + EasterEggStep = 24, + /// + /// Map power activated. populated + /// when a player flipped the switch (use trigger), null when world-triggered + /// (scripted auto-activation, devgui). + /// holds the round number at fire time. + /// + PowerOn = 25, + /// + /// Map power lost — currently only fired by TranZit (bus power loss / pylon). + /// typically null since TranZit + /// power-off is a world event. + /// holds the round number at fire time. + /// + PowerOff = 26, + /// + /// Bank deposit — T6 Tranzit / Die Rise / Buried only. + /// holds the amount (always 1000; + /// engine charges $1000 per increment, $100 fee on withdrawal not deposit). + /// + BankDeposit = 27, + /// + /// Bank withdrawal — T6 Tranzit / Die Rise / Buried only. + /// holds the gross principal + /// (1000); the $100 fee deducted on top is not surfaced. + /// + BankWithdraw = 28, + /// + /// Weapon Locker store — T6 Tranzit / Die Rise / Buried only. + /// holds the engine weapon name. + /// + WeaponLockerStore = 29, + /// + /// Weapon Locker retrieve — T6 Tranzit / Die Rise / Buried only. + /// holds the engine weapon name. + /// + WeaponLockerRetrieve = 30, + /// + /// Gobble Gum activated — player consumed an "activated"-type gum (BO3/T7 only). + /// holds the BGB engine name + /// (e.g. zm_bgb_perkaholic). + /// + GobbleGumActivated = 31, + /// + /// Gobble Gum taken from machine — player paid the machine cost and grabbed + /// the gum (BO3/T7 only). holds + /// the BGB engine name; holds + /// the machine cost (often 0 for free, 1500 for paid). + /// + GobbleGumTaken = 32, + /// + /// Gobble Gum abandoned — player paid the machine but didn't grab; cost + /// forfeited (ghost-ball cases excluded; BO3/T7 only). + /// holds the BGB engine name; + /// holds the cost lost. + /// + GobbleGumAbandoned = 33 +} + +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; } +} diff --git a/Data/Models/Zombie/ZombieMatch.cs b/Data/Models/Zombie/ZombieMatch.cs new file mode 100644 index 000000000..367f6659c --- /dev/null +++ b/Data/Models/Zombie/ZombieMatch.cs @@ -0,0 +1,76 @@ +#nullable enable +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Client.Stats.Reference; +using Data.Models.Server; + +namespace Data.Models.Zombie; + +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; } + + public long? ServerId { get; set; } + [ForeignKey(nameof(ServerId))] + public virtual EFServer? Server { get; set; } + + public int ClientsCompleted { get; set; } + + /// + /// True when EndMatch was driven by a clean ExitLevel/ShutdownGame signal + /// from the parser. False for crash-killed matches (orphan-close path) and + /// pre-fix matches. Drives the API `completed` field — independent of + /// , which only counts intermission-reached + /// players (zero on host-quit even after a 49-round game). + /// + public bool Completed { get; set; } + + /// + /// Number of qualifying players (>50% round participation) in this match. + /// Calculated at match end. + /// + public int? PlayerCount { get; set; } + + /// + /// The highest round number reached in this match. + /// Calculated at match end. + /// + public int HighestRound { get; set; } + + public DateTimeOffset MatchStartDate { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? MatchEndDate { get; set; } + + /// + /// Per-map-load identifier emitted by the GSC's sv_iw4m_zm_matchid dvar. + /// Lets IW4MAdmin re-attach to an existing match row across restarts/reconnects + /// instead of creating a new orphaned match. Server-scoped via the (ServerId, + /// GameMatchId) lookup; null for matches predating the GSC dvar deploy. + /// + [MaxLength(64)] + public string? GameMatchId { get; set; } + + /// + /// Round number at the moment the EE-complete event fired. Captured from + /// MatchState.RoundNumber at fire time. Null on legacy matches, + /// when no EE fired, or when the round was indeterminate at fire time + /// (RoundNumber 0/negative — pre-round-1 edge). + /// + public int? EasterEggRound { get; set; } + + /// + /// UTC timestamp at the moment the EE-complete event fired. Authoritative + /// "EE happened" signal — non-null implies the EE was completed in this + /// match. Drives the scrubber timeline marker so it lands at the actual + /// EE moment instead of match-end. Null on legacy matches and when no + /// watcher fired. + /// + public DateTimeOffset? EasterEggOccurredAt { get; set; } +} diff --git a/Data/Models/Zombie/ZombieMatchClientStat.cs b/Data/Models/Zombie/ZombieMatchClientStat.cs new file mode 100644 index 000000000..c58a6e8e2 --- /dev/null +++ b/Data/Models/Zombie/ZombieMatchClientStat.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.Models.Zombie; + +public class ZombieMatchClientStat : ZombieClientStat +{ + [NotMapped] public int? JoinedRound { get; set; } + [NotMapped] public int? LastRoundReached { get; set; } + + /// + /// True when the row was created by carryover (auto-enrollment of an already-Connected + /// client at match-create) but the client hasn't yet emitted any per-client event. + /// Tentative rows are NOT persisted to DB and do NOT bump lifetime TotalMatchesPlayed. + /// First per-client event in RunCalculation calls PromoteTentativeIfNeeded + /// to clear the flag, persist the row, and bump lifetime stats. Clients who untrack + /// while still tentative are dropped entirely (no DB row, no lifetime pollution). + /// Defends against the carryover-of-leavers phantom pattern (see match 524). + /// + [NotMapped] public bool IsTentative { get; set; } + + /// + /// Number of rounds in this match where this player had at least one other tracked + /// player alongside them. Computed in FinalizePlayerCountAsync after match end. + /// Null on legacy rows that pre-date the badge feature. + /// + public int? AssistedRounds { get; set; } + + /// + /// First round at which this player became "solo to the end" — every round from this + /// number through the match's HighestRound was played alone. Null if the player was + /// either solo throughout (no assistance ever) OR was assisted up to and including the + /// final round. The "Solo from R<N>" badge uses this as both its trigger and its + /// display value. Null on legacy rows. + /// + public int? SoloFromRound { get; set; } +} diff --git a/Data/Models/Zombie/ZombieRoundClientStat.cs b/Data/Models/Zombie/ZombieRoundClientStat.cs new file mode 100644 index 000000000..1f11fb809 --- /dev/null +++ b/Data/Models/Zombie/ZombieRoundClientStat.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Data.Models.Zombie; + +public class ZombieRoundClientStat : ZombieClientStat +{ + public DateTimeOffset StartTime { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? EndTime { get; set; } + public TimeSpan? Duration { get; set; } + public TimeSpan? TimeAlive { get; set; } + public int RoundNumber { get; set; } + public int Points { get; set; } + + /// + /// Number of qualifying players in the match at the moment this round began. + /// Persisted snapshot of RoundState.PlayerCountAtRoundStart so the + /// round can be cross-referenced against the right EMA cell at read time + /// (drives match-detail / leaderboard pace tinting). Null on legacy rows + /// pre-dating this column — readers fall back to EFZombieMatch.PlayerCount. + /// + public int? PlayerCountAtRoundStart { get; set; } + + /// + /// Special-round type for this round, if applicable. Null on normal rounds. + /// Sourced from GSC GSE;ZW;round_special;<round>;<type> emissions and snapshotted from + /// MatchState.RoundSpecialTypes[RoundNumber] at round-end persistence. + /// Drives the Round Breakdown UI badge and gates the static Seconds-Per-Horde + /// calculation in !ztimings (special rounds replace the regular spawn budget + /// so SPH would render visibly wrong otherwise). Mid-round mini-bosses + /// (panzer/brutus/mechz/ghost/sloth) are NOT recorded — those add a small + /// fixed enemy count alongside regular zombies; SPH stays approximately correct. + /// Stored as int (EF Core enum default) — see + /// for the stable wire values. + /// + public ZombieSpecialRoundType? SpecialType { get; set; } + + /// + /// Mirrors ZombieMatchClientStat.IsTentative: round entries created by + /// StartNextRound for a tentative match-stat are also tentative until the + /// first event promotes the whole tree. Skipped from DB persistence while tentative. + /// + [NotMapped] public bool IsTentative { get; set; } +} diff --git a/Data/Models/Zombie/ZombieRoundDurationEma.cs b/Data/Models/Zombie/ZombieRoundDurationEma.cs new file mode 100644 index 000000000..e965ed5d5 --- /dev/null +++ b/Data/Models/Zombie/ZombieRoundDurationEma.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Client.Stats.Reference; + +namespace Data.Models.Zombie; + +public class ZombieRoundDurationEma +{ + public int MapId { get; set; } + [ForeignKey(nameof(MapId))] + public virtual EFMap Map { get; set; } + + public int RoundNumber { get; set; } + public int PlayerCount { get; set; } + public double EmaSeconds { get; set; } + public long SampleCount { get; set; } +} diff --git a/Data/Models/Zombie/ZombieSpecialRoundType.cs b/Data/Models/Zombie/ZombieSpecialRoundType.cs new file mode 100644 index 000000000..d95404a41 --- /dev/null +++ b/Data/Models/Zombie/ZombieSpecialRoundType.cs @@ -0,0 +1,90 @@ +namespace Data.Models.Zombie; + +/// +/// Special-round classifications surfaced from GSC GSE;ZW;round_special;<round>;<type> +/// emissions. A non-null value on +/// indicates the round replaced the regular zombie spawn pool — the static +/// Seconds-Per-Horde formula doesn't apply, and the Round Breakdown UI badges +/// the row. +/// +/// Values are stable wire identifiers (do not renumber) — the parser maps GSC +/// string tokens to these via ZombieSpecialRoundTypeExtensions.FromGsc. +/// Mid-round mini-bosses (panzer/brutus/mechz/ghost/sloth) are deliberately +/// NOT listed — those add a small fixed enemy count alongside regular zombies +/// and SPH stays approximately correct. +/// +public enum ZombieSpecialRoundType +{ + /// Hellhound rounds — universal across T4/T5/T6/T7. + Dog = 1, + + /// + /// Space Monkey rounds — T5 Ascension and T7 Ascension (Chronicles). + /// Shangri-La's GSC initialises the monkey_round flag too but the shipped + /// game gates it to "never fires" / dead-path. + /// + Monkey = 2, + + /// Leaper rounds — T6 Die Rise only. + Leaper = 3, + + /// Pentagon Thief rounds — T5 Five. + Thief = 4, + + /// Apothicon Servant ("wasp") rounds — T7 Revelations. + Wasp = 5, + + /// "Spiders from Mars" rounds — T7 Zetsubou No Shima. + Spider = 6, + + /// Three-robot rounds — T7 Origins-style maps. + Robot = 7, + + /// Quad-squad rounds — T7 Kino der Toten (Chronicles). + Quad = 8, + + /// Generic boss rounds — T7 Revelations boss encounter. + Boss = 9, + + /// Easter-egg round — T7 Gorod Krovi. + Ee = 10, +} + +public static class ZombieSpecialRoundTypeExtensions +{ + /// + /// Maps a GSC wire token to its enum value. Returns null on unknown tokens + /// so a future GSC type doesn't crash the parser before the C# side updates. + /// + public static ZombieSpecialRoundType? FromGsc(string token) => token switch + { + "dog" => ZombieSpecialRoundType.Dog, + "monkey" => ZombieSpecialRoundType.Monkey, + "leaper" => ZombieSpecialRoundType.Leaper, + "thief" => ZombieSpecialRoundType.Thief, + "wasp" => ZombieSpecialRoundType.Wasp, + "spider" => ZombieSpecialRoundType.Spider, + "robot" => ZombieSpecialRoundType.Robot, + "quad" => ZombieSpecialRoundType.Quad, + "boss" => ZombieSpecialRoundType.Boss, + "ee" => ZombieSpecialRoundType.Ee, + _ => null, + }; + + /// Inverse of — used for UI lookups (translation + /// keys, badge icons) keyed by the canonical lower-case token. + public static string ToToken(this ZombieSpecialRoundType type) => type switch + { + ZombieSpecialRoundType.Dog => "dog", + ZombieSpecialRoundType.Monkey => "monkey", + ZombieSpecialRoundType.Leaper => "leaper", + ZombieSpecialRoundType.Thief => "thief", + ZombieSpecialRoundType.Wasp => "wasp", + ZombieSpecialRoundType.Spider => "spider", + ZombieSpecialRoundType.Robot => "robot", + ZombieSpecialRoundType.Quad => "quad", + ZombieSpecialRoundType.Boss => "boss", + ZombieSpecialRoundType.Ee => "ee", + _ => string.Empty, + }; +} diff --git a/GameFiles/README.MD b/GameFiles/README.MD index 9c7c74c31..a0940151f 100644 --- a/GameFiles/README.MD +++ b/GameFiles/README.MD @@ -1,13 +1,72 @@ -# Game Interface +# Game Files -Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work. -But can also be used to read / write metadata from / to a profile and to get the player permission level. +GSC scripts deployed to Plutonium game servers for IW4MAdmin integration. Three systems: -## Installation Guide +## Game Interface +Core integration layer between IW4MAdmin and GSC. Required for all IW4MAdmin functionality beyond basic RCon. -The documentation can be found here: [GameInterface](https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface) +**Shared files** (deployed to all games): +- `_integration_base.gsc` — core event loop, command processing, player connect/disconnect hooks +- `_integration_shared.gsc` — shared utilities, latency probe (`MonitorLatencyProbe` for measuring log pipeline latency) +- `_integration_utility.gsh` — helper macros and constants -## Latency Probe +**Game-specific files:** +| File | Game | Mode | +|------|------|------| +| `_integration_t4.gsc` | World at War (T4) | MP | +| `_integration_t4zm.gsc` | World at War (T4) | ZM/SP | +| `_integration_t5.gsc` | Black Ops (T5) | MP | +| `_integration_t5zm.gsc` | Black Ops (T5) | ZM | +| `_integration_t6.gsc` | Black Ops II (T6) | MP + ZM | +| `_integration_t6zm_helper.gsc` | Black Ops II (T6) | ZM helper | +| `_integration_iw4x.gsc` | Modern Warfare 2 (IW4x) | MP | +| `_integration_iw5.gsc` | Modern Warfare 3 (IW5) | MP | +| `_integration_iw6.gsc` | Ghosts (IW6) | MP | +| `_integration_t6_file_bus.gsc` | Black Ops II (T6) | File bus transport | +| `example_module.gsc` | — | Template for custom modules | -The Game Interface includes a built-in latency probe (`MonitorLatencyProbe` in `_integration_shared.gsc`) that allows IW4MAdmin to measure log pipeline latency. This is automatic when the Game Interface is installed — no additional configuration is needed. +Full documentation: [GameInterface Wiki](https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface) + +## Zombie Stats + +GSC scripts that emit zombie-specific telemetry (kills, downs, revives, points, rounds, power-ups, box hits, Pack-a-Punch usage, Easter Egg progress, bank/locker on T6, Gobblegums on T7) to IW4MAdmin. These feed the ZombieStats plugin for stat tracking, leaderboards, and match history. + +| File | Game | Runtime | +|------|------|---------| +| `ZombieStats/_zm_stats_t4.gsc` | World at War (T4) | Plutonium | +| `ZombieStats/_zm_stats_t5.gsc` | Black Ops (T5) | Plutonium | +| `ZombieStats/_zm_stats_t6.gsc` | Black Ops II (T6) | Plutonium | +| `ZombieStats/_zm_stats_t7.gsc` (source) + `ZombieStats/_zm_stats_t7.compiled.gsc` (compiled) | Black Ops III (T7) | T7x AlterWare | + +See [`ZombieStats/README.md`](ZombieStats/README.md) and [`ZombieStats/FEATURE_MATRIX.md`](ZombieStats/FEATURE_MATRIX.md) for the per-engine emission inventory, build chain, and deploy paths. + +## AntiCheat + +Custom callback scripts for server-side anti-cheat. Game-specific: + +| Directory | Game | +|-----------|------| +| `AntiCheat/IW4x/` | Modern Warfare 2 (IW4x) | +| `AntiCheat/IW5/` | Modern Warfare 3 (IW5) | +| `AntiCheat/PT6/` | Black Ops II (Pluto T6) | + +## Installation + +Run `deploy.bat` to copy all scripts to Plutonium's local storage directories. Requires Plutonium to be installed at the default `%LOCALAPPDATA%\Plutonium` path. + +Currently deploys to: **T4**, **IW5**, **T5**, **T6**. T7 has a different runtime (T7x AlterWare, not Plutonium) — see `ZombieStats/README.md` for the T7 deploy path. + +### Manual Installation + +Copy files to your Plutonium storage scripts directory. Path varies by game: + +| Game | Base Path | +|------|-----------| +| T4 (WaW) | `%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts\` | +| T5 (BO1) | `%LOCALAPPDATA%\Plutonium\storage\t5\scripts\` | +| T6 (BO2) | `%LOCALAPPDATA%\Plutonium\storage\t6\scripts\` | +| IW5 (MW3) | `%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\` | +| T7 (BO3) | `\t7x\custom_scripts\` (T7x AlterWare — deploy `ZombieStats/_zm_stats_t7.compiled.gsc`) | + +Place shared Game Interface files in the base scripts directory. Game-specific MP scripts go in the `mp/` subdirectory. ZM scripts go in the appropriate ZM subdirectory (`sp/` for T4, `sp/zom/` for T5, `zm/` for T6). T7 is interpreted from compiled bytecode — see `ZombieStats/README.md`. diff --git a/GameFiles/ZombieStats/FEATURE_MATRIX.md b/GameFiles/ZombieStats/FEATURE_MATRIX.md new file mode 100644 index 000000000..2e4577329 --- /dev/null +++ b/GameFiles/ZombieStats/FEATURE_MATRIX.md @@ -0,0 +1,310 @@ +# Zombie Stats GSC — Feature Support Matrix + +Cross-engine inventory of what each `_zm_stats_t.gsc` instrumentation script +emits to IW4MAdmin. All four are on the consolidated `GSE;ZP;` (per-player) / +`GSE;ZW;` (world) wire dialect — no pre-consolidation `RS`/`ZR`/`PWR`/`EE`/`ZE` +codes remain. + +Legend: ✅ supported · ➖ not applicable to engine · ❌ deliberately not tracked + +--- + +## 0. File overview + +| | T4 (W@W / Pluto T4) | T5 (BO1 / Pluto T5) | T6 (BO2 / Pluto T6) | T7 (BO3 / T7x AlterWare) | +|---|---|---|---|---| +| Source file | `_zm_stats_t4.gsc` | `_zm_stats_t5.gsc` | `_zm_stats_t6.gsc` | `_zm_stats_t7.gsc` | +| Lines | 1,758 | 2,064 | 2,161 | 1,699 | +| Compile step | ➖ (interpreted) | ➖ | ➖ | ✅ `_zm_stats_t7.compiled.gsc` via Cerberus | +| Helper script ships | ➖ | ➖ | ➖ | ➖ (dev-only helper lives outside repo) | +| Engine entry | `level thread Init()` | same | same | `REGISTER_SYSTEM("zombie_stats", &__init__, undefined)` | +| Module import | `maps\_zombiemode_utility` | `maps\_zombiemode_utility` | `maps\mp\zombies\_zm_utility` | `#using scripts\shared\...` BO3 system | +| Function keyword | none (legacy GSC) | none | none | `function` | +| Notify hashes | named strings | named strings | named strings | `#"..."` hashed (BO3 EOL — frozen) | + +--- + +## 1. Wire format (emitted GSE codes) + +| Code | Meaning | T4 | T5 | T6 | T7 | +|---|---|---|---|---|---| +| `K` | Player kill / death | ✅ | ✅ | ✅ | ✅ | +| `D` | Player damage | ✅ | ✅ | ✅ | ✅ | +| `AD` | Actor (zombie) damage | ✅ | ✅ | ✅ | ✅ | +| `AK` | Actor killed | ✅ | ✅ | ✅ | ✅ | +| `RD` | Per-player round data | ✅ | ✅ | ✅ | ✅ | +| `RC` | Round complete | ✅ | ✅ | ✅ | ✅ | +| `ZP` | Per-player zone/economy event | ✅ | ✅ | ✅ | ✅ | +| `ZW` | World/round event | ✅ | ✅ | ✅ | ✅ | + +`RD` payload byte-identical across all four: +`;;;;`. +Kills / downs / revives / damage are derived server-side from the AK/AD/K/D +stream — never pre-aggregated in GSC. + +--- + +## 2. Match lifecycle + +| | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| Match-ID dvar (`sv_iw4m_zm_matchid`) | ✅ | ✅ | ✅ | ✅ | +| Round dvar (`sv_iw4m_zm_round`) | ✅ | ✅ | ✅ | ✅ | +| Random match-ID seed (~10¹² collision space) | ✅ | ✅ | ✅ | ✅ | +| Intermission emit (`isGameOver=1`) | ✅ | ✅ | ✅ | ✅ | +| Concurrent K/RD drain wait (0.1s) | ✅ | ✅ | ✅ | ✅ | +| ExitLevel / fast-restart cleanup | implicit via endon | same | same | same | + +Match-ID seed uses `randomint(1000000)+"_"+randomint(1000000)` not `gettime()` +because the engine clock returns 0 at init on T4/T5/T6. + +--- + +## 3. Round events + +| | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| `RD` per player per round | ✅ | ✅ | ✅ | ✅ | +| `RC;` | ✅ | ✅ | ✅ | ✅ | +| Round-special detection (`ZW;round_special;;`) | ✅ dog only | ✅ dog/monkey/thief | ✅ dog/leaper | ✅ dog/monkey/wasp/spider/robot/quad/boss/ee | + +**Special-round flag sources per engine:** + +| Type | Flag | T4 | T5 | T6 | T7 | +|---|---|---|---|---|---| +| dog | `dog_round` | ✅ | ✅ | ✅ | ✅ | +| monkey | `monkey_round` | ➖ | ✅ (Ascension) | ➖ | ✅ | +| thief | `thief_round` | ➖ | ✅ (Five) | ➖ | ➖ | +| leaper | `leaper_round` | ➖ | ➖ | ✅ (Die Rise) | ➖ | +| wasp | `wasp_round` | ➖ | ➖ | ➖ | ✅ | +| spider | `spiders_from_mars_round` | ➖ | ➖ | ➖ | ✅ | +| robot | `three_robot_round` | ➖ | ➖ | ➖ | ✅ | +| quad | `special_quad_round` | ➖ | ➖ | ➖ | ✅ | +| boss | `boss_round` | ➖ | ➖ | ➖ | ✅ | +| ee | `ee_round` | ➖ | ➖ | ➖ | ✅ | + +All four guard with `IsDefined(level.flag[name])` — T4 lacks `flag_exists()`, +others kept consistent for portability. + +--- + +## 4. Zombies remaining + +| | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| `ZW;zombies;;;` 5s change-gated poll | ✅ | ✅ | ✅ | ✅ | + +Source: `level.zombie_total` + `get_enemy_count()` (T4/T5/T6) or BO3 equivalent. + +--- + +## 5. Player economy events (`GSE;ZP;;...`) + +| Subtype | Format | T4 | T5 | T6 | T7 | +|---|---|---|---|---|---| +| Door buy | `door;buy;;` | ✅ | ✅ | ✅ | ✅ | +| Weapon buy (wallbuy) | `weapon;buy;;` | ✅ | ✅ | ✅ | ✅ | +| PaP upgrade | `weapon;upgrade;` | ✅ | ✅ | ✅ | ✅ | +| PaP abandon (timeout/disc.) | `weapon;abandon;` | ✅ | ✅ | ✅ | ✅ | +| Perk buy | `perk;buy;;` | ✅ (weapon-poll) | ✅ (notify) | ✅ (notify) | ✅ (notify) | +| Perk lost (QR-auto / Tombstone / Who's Who) | — | ❌ | ❌ | ❌ | ❌ | +| Mystery box outcome | `box;take\|pass\|teddy;...` | ✅ | ✅ | ✅ | ✅ | +| Box move | — | ❌ | ❌ | ❌ | ❌ | +| Power-up grab | `powerup;grab;` | ✅ | ✅ | ✅ | ✅ | +| Trap activate | `trap;activate;;` | ✅ electric | ✅ electric+turret | ✅ | ✅ | +| Buildable complete | `build;complete;` | ➖ | ➖ | ✅ | ✅ (craftables) | +| Craftable complete | `build;complete;` | ➖ | ➖ | ✅ | ✅ | +| Gobblegum activate | `gum;activate;` | ➖ | ➖ | ➖ | ✅ | +| Gobblegum take (machine) | `gum;take;;` | ➖ | ➖ | ➖ | ✅ | +| Gobblegum leave (refund-filter) | `gum;leave;;` | ➖ | ➖ | ➖ | ✅ | +| Bank deposit | `bank;deposit;` | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Bank withdraw | `bank;withdraw;` | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Weapon locker store | `locker;store;` | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Weapon locker retrieve | `locker;retrieve;` | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Revive (co-op) | `revive;` | ✅ | ✅ | ✅ | ✅ | +| Self-revive (solo QR / Who's Who / Self Revive gum) | `revive;self` | ➖ (no self-revive in W@W) | ✅ | ✅ | ✅ | +| Down | `down` | ✅ | ✅ | ✅ | ✅ | +| Zombified | `zombified` | ✅ | ✅ | ✅ | ✅ | + +Box detection: notify-driven with 3-tier user resolution + scoped teddy +suppression. PaP detection: lock-first attribution (`WatchPapTakenFlag` / +`WatchPapTimeoutFlag` / `WatchPapTriggerForBuyer` / `VerifyPapBuyerLock` / +`WatchPapOutcome` / `WatchPapDisconnectFlag`). All four engines have full +disconnect-cleanup coverage; T4/T5 synthesise the `pap_player_disconnected` +notify via the `WatchPapBuyerDisconnect` helper since their engines don't +emit it natively (T6/T7 do). + +Perk detection diverges per engine: + +| Engine | Mechanism | Why | +|---|---|---| +| T4 | Weapon-switch substring `IsSubStr(weapon, "zombie_perk")` | T4 has no `perk_bought` notify | +| T5 | `perk_bought` notify | Override-safe vs Ascension/Shangri-La `monkey_perk_bought` | +| T6 | `perk_bought` notify | Override-safe vs Die Rise achievement hook | +| T7 | `#"perk_bought"` notify | Standard BO3 path | + +Power-up: proximity-poll on `script_model` entities with `powerup_name` set. +T4 explicitly does NOT hook `level.zombie_powerup_grab_func` — hooking it +disables the effect. + +--- + +## 6. World events (`GSE;ZW;...`) + +| Subtype | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| `power;on;world` | ✅ | ✅ | ✅ | ✅ | +| `power;on;player;` | ✅ | ✅ | ✅ | ✅ | +| `power;off;world` | ➖ (W@W maps never power off) | ✅ | ✅ Tranzit | ✅ | +| `round_special;;` | ✅ | ✅ | ✅ | ✅ | +| `zombies;;;` | ✅ | ✅ | ✅ | ✅ | +| `easter_egg;step;` | ✅ | ✅ | ✅ | ✅ | +| `easter_egg;complete;` | ✅ canonical | ✅ canonical | ✅ canonical | ✅ derived | + +Two equally-valid signal channels for the same outcome (`EasterEggOccurredAt` +gets set either way). Per-quest `HasCanonicalNotify` flag in +`MapEasterEggConfig.cs` decides which the server expects: + +- **Canonical** (T4/T5/T6 + any T7 quest with a named terminal notify): + GSC waits on the engine's terminal flag/notify and emits + `easter_egg;complete;`. Server marks complete on receipt. +- **Derived** (every T7 quest currently): GSC emits only `easter_egg;step` + for each configured step. Server marks complete when all steps for the + quest have logged. + +T7 uses derivation because most BO3 main-quest terminal flags are hashed +in the shiversoftdev dump — a named-notify wait isn't always available. +Step-based derivation covers all 14 T7 maps using per-step flags that DO +have source names. Cases where T7 has a clean terminal flag (e.g. +`zm_zod` `ee_complete`) appear as the final step in the quest's step +list, so derivation still picks them up. + +--- + +## 7. Easter Egg coverage (per map) + +### T4 (Pluto W@W) + +| Map | Main quest | Song EE | Notes | +|---|---|---|---| +| Nacht der Untoten | ❌ | ❌ | Pluto T4 entity hook broken | +| Verrückt | ❌ | ✅ song step (`level.eggs`) | | +| Shi No Numa | ❌ | ✅ song step (`level.eggs`) | | +| Der Riese | ✅ steps + flytrap + 3 meteors | ❌ | No canonical "complete" | + +### T5 (Pluto BO1) + +| Map | Main quest | Song EE | Notes | +|---|---|---|---| +| Kino der Toten | ❌ | ✅ song (shared `HookT5SongTriggers`) | | +| Five | ❌ | ✅ song | | +| Ascension | ✅ complete (`HookAscensionCasimir`) | ✅ | Teddy bears + Casimir flag | +| Call of the Dead | ✅ complete | ✅ ensemble (`HookCallOfDeadEnsemble`) | | +| Shangri-La | ✅ complete | ✅ (`HookShangriLaSidequest`) | | +| Moon | ✅ complete (`HookMoonRichtofen`) | ✅ | | + +### T6 (Pluto BO2) + +| Map | Main quest | Song EE | Map-specific | +|---|---|---|---| +| Tranzit | ✅ Maxis/Rich branching | ✅ song bears | Gramophone placement | +| Nuketown Zombies | — | ✅ meteor counter | Bears | +| Die Rise | ✅ Maxis/Rich + terminal + bears | ✅ | | +| Mob of the Dead | ✅ (`pop_goes_the_weasel_achieved`) | ✅ Rusty Cage / Nixie 115+935 | Multi-stage Pop Goes the Weasel | +| Buried | ✅ stages + terminals + bears | ✅ | Wisp stage | +| Origins | ✅ (`tomb_sidequest_complete`) | ✅ 3 counter songs + Little Lost Girl | 4 staffs | + +### T7 (T7x BO3) + +14/14 maps with EE step coverage. `easter_egg;complete` is a deliberate stub +in `WaitForEasterEggComplete` — terminal notify hooks live in +`WaitForT7EasterEggSteps` and completion derives from the last step server-side. + +| Map (engine name) | Main | Song | Side / upgrade quests | +|---|---|---|---| +| `zm_zod` (SoE) | ✅ 8 main-quest flags | ✅ 3 song states | Arnie upgrade (hashed `#"hash_21edb6b6"`), Shield, Bouncing Bettys; ❌ Apothicon Sword per-character (deferred) | +| `zm_factory` (The Giant) | ✅ | ✅ | Flytrap (3-target) + secret-perk pad | +| `zm_castle` (Der Eisendrache) | ✅ 8 steps | ✅ 2 songs + music box + disco | 4 elemental bow upgrades (weapon-substring poll) | +| `zm_island` (Zetsubou) | ✅ | ✅ | KT-4 base+upgrade + 4 skull rituals + 3 spider EE | +| `zm_stalingrad` (Gorod Krovi) | ✅ 6 steps | ✅ 3-song state polls | 5-step Dragon Gauntlet quest (`gauntlet_step_2/3/4/complete`) | +| `zm_genesis` (Revelations) | ✅ 13 steps | ✅ | Li'l Arnie prereq+done | +| `zm_tomb` (Origins) | ✅ | ✅ | 4 staff upgrades | +| `zm_cosmodrome` (Ascension) | ✅ | ✅ | | +| `zm_theater` (Kino) | ✅ | ✅ | | +| `zm_temple` (Shangri-La) | ✅ | ✅ | | +| `zm_moon` (Moon) | ✅ | ✅ | | +| `zm_prototype` (Nacht) | ✅ HnS `snd_zhdegg_activate` | ➖ | | +| `zm_asylum` (Verrückt) | ✅ HnS | ➖ | | +| `zm_sumpf` (Shi No Numa) | ✅ HnS | ✅ | | + +Shared T7 helpers: `WatchT7FlagStep`, `WatchT7MusicStateStep`, +`WatchWeaponSubstringUpgrade` (generic weapon-inventory substring poller — +covers DE bows, Origins staffs, SoE shield/Bettys), `WaitForT7FlagInit`, +`PlayerHasWeaponSubstr`. + +--- + +## 8. Map-specific mechanics + +| Mechanic | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| Flytrap (Der Riese / Factory) | ✅ panel | ➖ | ➖ | ✅ 3-target | +| Meteor counter songs | ✅ 3 | ✅ (`WatchT5MeteorCounterSong`) | ✅ Nuketown / Origins | ➖ | +| Auto-turret trap | ➖ | ✅ Ascension PaP turrets | ➖ | ➖ | +| Buildables (`_zm_buildables`) | ➖ | ➖ | ✅ | ➖ | +| Craftables (`_zm_craftables`) | ➖ | ➖ | ✅ | ✅ | +| Bank deposit/withdraw | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Weapon locker store/retrieve | ➖ | ➖ | ✅ Tranzit/DieRise/Buried | ➖ | +| Wonder-weapon upgrade attribution | ➖ | ➖ | Origins staffs (PaP path) | DE bows, GK gauntlet, SoE shield/Bettys/Arnie, Zetsubou KT-4 | +| Music-state polling | ➖ | ➖ | ➖ | ✅ | +| Hashed terminal-notify EE | ➖ | ➖ | ➖ | ✅ (SoE, GK) | +| Gobblegum machines | ➖ | ➖ | ➖ | ✅ | + +--- + +## 9. Player connection + +| | T4 | T5 | T6 | T7 | +|---|---|---|---|---| +| `level "connecting"` wait | ✅ | ✅ | ✅ | ✅ (`#"connecting"`) | +| Per-player watchers threaded | revive, zombified, perk | + perk_bought | + perk_bought | + perk_bought, gum activate | +| Explicit disconnect emission | ❌ (endon) | ❌ | ❌ | ❌ | + +--- + +## 10. Deliberately not tracked (any engine) + +- Perk loss (Quick Revive auto / Tombstone / Who's Who) — downstream classification only +- Permaperks (T6 Tranzit-line persistent buffs) — out of scope +- Tactical/lethal equipment (claymores, grenades, monkey bombs, EMPs) +- Box move location changes (detected internally for teddy-suppression scope, never emitted) +- T4 self-revive — W@W has no self-revive mechanic; reviver always != self + +--- + +## 11. Known gaps + +| Gap | Engine | Notes | +|---|---|---| +| Nacht der Untoten EE | T4 | Pluto T4 entity hook broken | +| T4 perk-buy poll | T4 | No `perk_bought` notify exists; weapon-switch poll is only path. Edge case: perk bought + downed within 0.1s tick drops the emission | +| Apothicon Sword per-character | T7 (SoE) | Hashed flags, deferred (4 separate quests) | +| Live-test 12/14 maps | T7 | Only zm_factory + zm_sumpf live-verified | +| Live-test bank/locker | T6 | New emission paths added; need Tranzit/Die Rise/Buried verification | +| Gobblegum C# downstream | T7 | Events emitted, no premium handlers yet | + +--- + +## 12. Compile / deploy + +Only T7 needs compilation: + +- Source: `_zm_stats_t7.gsc` +- Compiled: `_zm_stats_t7.compiled.gsc` (~37 KB; double-extension passes T7x's `filename.endsWith(".gsc")` suffix gate and disambiguates from the source filename) +- Magic bytes: `80 47 53 43 0d 0a` (`ÇGSC\r\n`) +- Toolchain: `linker_modtools.exe` + `Cerberus.CLI.exe` (PowerShell only — DLL search) +- Known benign noise: T7x logs `[DB] Error: Could not find scriptparsetree "custom_scripts/..."` on every custom_scripts/ load — script still executes correctly (verified by event flow in `games_zm.log`). Believed to be a secondary DB asset registry lookup running after the primary runtime load succeeded. No known suppression. +- See [`README.md`](README.md) for full build chain + gotchas, and [`t7-gsc-compile-chain.md`](../../../../.claude/projects/C--Users-Amos-RiderProjects--Cloned-IW4MAdmin/memory/t7-gsc-compile-chain.md) + +T4/T5/T6 are interpreted by Pluto runtime directly — drop the `.gsc` in the +appropriate `scripts/zm/` (or game equivalent) and reload. diff --git a/GameFiles/ZombieStats/README.md b/GameFiles/ZombieStats/README.md new file mode 100644 index 000000000..3875b1851 --- /dev/null +++ b/GameFiles/ZombieStats/README.md @@ -0,0 +1,133 @@ +# ZombieStats GSC + +Per-engine GSC instrumentation that emits zombie-mode telemetry to IW4MAdmin via the server game log. Feeds the `ZombieStats` plugin (free) + `ZombieStatsPremium` plugin (private) for stat tracking, leaderboards, match history, Easter Egg detection, pace metrics, and timeline rendering. + +## Files + +| File | Game | Runtime | Compile | +|------|------|---------|---------| +| `_zm_stats_t4.gsc` | World at War (T4) | Plutonium T4 | interpreted | +| `_zm_stats_t5.gsc` | Black Ops 1 (T5) | Plutonium T5 | interpreted | +| `_zm_stats_t6.gsc` | Black Ops 2 (T6) | Plutonium T6 | interpreted | +| `_zm_stats_t7.gsc` | Black Ops 3 (T7) | T7x AlterWare | source | +| `_zm_stats_t7.compiled.gsc` | Black Ops 3 (T7) | T7x AlterWare | **compiled bytecode** (~37 KB; deploy this) | +| `FEATURE_MATRIX.md` | — | — | cross-engine emission inventory | + +## Wire format + +All four files emit a consolidated `GSE;...` log format on stdout: + +| Code | Meaning | +|------|---------| +| `K` | Player kill / death | +| `D` | Player damage | +| `AD` | Actor (zombie) damage | +| `AK` | Actor killed | +| `RD` | Per-player round data | +| `RC` | Round complete | +| `ZP` | Per-player zone/economy event (perks, powerups, weapons, box, doors, traps, builds, revives, gum, bank, locker) | +| `ZW` | World/round event (power, specials, zombies-remaining, easter-egg) | + +See [`FEATURE_MATRIX.md`](FEATURE_MATRIX.md) for the full subtype inventory, per-engine support, and known gaps. + +## Installation + +### T4 / T5 / T6 (Plutonium — interpreted) + +Drop the `.gsc` into the appropriate Plutonium storage scripts directory and reload the map. Plutonium parses the file at level start. + +| Game | Path | +|------|------| +| T4 | `%LOCALAPPDATA%\Plutonium\storage\t4\scripts\sp\` | +| T5 | `%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom\` | +| T6 | `%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm\` | + +### T7 (T7x AlterWare — compiled bytecode) + +T7 is the only engine that requires compilation. The runtime loads compiled GSC bytecode from: + +``` +\t7x\custom_scripts\ +``` + +Deploy `_zm_stats_t7.compiled.gsc` from this directory into `\t7x\custom_scripts\`. The file is identified internally by magic bytes `80 47 53 43 0D 0A` (`ÇGSC\r\n`). + +**Suffix gate** — T7x enforces `filename.endsWith(".gsc")` on every file in `custom_scripts/`. Cerberus's native `.gscc` output is rejected as `failed to load due to invalid suffix`. The `.compiled.gsc` double-extension passes the gate (last 4 chars = `.gsc`) and disambiguates from the `_zm_stats_t7.gsc` source filename. + +**Benign DB error** — T7x logs `[DB] Error: Could not find scriptparsetree "custom_scripts/"` on every custom_scripts/ load, regardless of compile settings. Confirmed harmless: the script loads and executes correctly, events flow normally to the game log. Believed to come from T7x's secondary DB asset registry lookup running after the primary runtime load succeeded. No known suppression. Verified 2026-05-14 against running T7x server with full event emission in `games_zm.log`. + +## T7 build chain + +Required when `_zm_stats_t7.gsc` changes. PowerShell only — Bash/MSYS can't load the required Windows DLLs. + +```powershell +$root = "C:\Users\Amos\_OtherProjects\CoD Scripts\T7\_COMPILATION_TOOLS\ModTools\T7 Mod Tools Stripped" +$name = "_zm_stats_t7" +$env:TA_GAME_PATH = "$root\" +$env:TA_LOCAL_ASSET_CACHE = "$root\share\assetconvert\" +$env:TA_TOOLS_PATH = "$root\" +$proj = "$root\usermaps\$name" +Remove-Item -Recurse -Force "$proj" -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path "$proj\scripts","$proj\zone_source\loc" | Out-Null + +# Source must keep its .gsc extension (NOT .compiled.gsc) — the linker uses +# everything after the first dot as the extension for #using resolution. +Copy-Item "\GameFiles\ZombieStats\_zm_stats_t7.gsc" "$proj\scripts\$name.gsc" -Force +Set-Content "$proj\zone_source\$name.zone" "scriptparsetree,scripts/$name.gsc" +Set-Content "$proj\zone_source\loc\$name.zone" "" +& "$root\bin\linker_modtools.exe" -language english -modsource $name +"" | & "C:\Users\Amos\_OtherProjects\CoD Scripts\T7\_COMPILATION_TOOLS\Cerberus\Cerberus.CLI.exe" "$proj\zone\$name.ff" + +# Cerberus extracts to: ...\ExtractedScripts\Black Ops III\scripts\$name.gscc +# Rename .gscc -> .compiled.gsc and copy back to the repo: +Copy-Item ".\Cerberus\ExtractedScripts\Black Ops III\scripts\$name.gscc" ` + "\GameFiles\ZombieStats\$name.compiled.gsc" -Force +``` + +**Gotchas:** + +1. Stripped tools need MSVC++ 2012 x64 Redist (`MSVCP110.dll` / `MSVCR110.dll`) — install `vcredist_x64.exe`. +2. Project must live under `usermaps\\`, not `mods\\` (linker anchors `-modsource` to `usermaps\`). +3. The zone filename must match the `-modsource` argument. +4. The localized pass needs an empty `zone_source\loc\.zone` file. +5. Cerberus blocks on a "press Enter to exit" prompt — pipe empty string to release. +6. Always invoke `linker_modtools.exe` via PowerShell. The Bash/MSYS exec wrapper doesn't honour the Windows DLL search path. +7. **Source filename must end in `.gsc` only** — not `.compiled.gsc`. The linker treats everything after the first `.` as the extension for `#using` directive resolution; a `.compiled.gsc` source breaks every `#using scripts\shared\X;` because it tries to resolve `scripts\shared\X.compiled.gsc`. Apply the `.compiled.gsc` rename to the Cerberus output, not the source. + +## Architecture + +### Detection patterns + +| Mechanic | Pattern | Engines | +|----------|---------|---------| +| Perk buy | T4: weapon-switch poll (no engine notify exists); T5/T6/T7: `perk_bought` notify | all | +| Powerup grab | Proximity poll on `script_model` entities — do NOT hook `level.zombie_powerup_grab_func` (replacing it disables the effect) | all | +| Pack-a-Punch | Lock-first attribution (`WatchPapTriggerForBuyer` + `WatchPapTakenFlag` + `WatchPapTimeoutFlag` + `WatchPapDisconnectFlag` + `WatchPapOutcome`) | all | +| Mystery box | Notify-driven with 3-tier user resolution + scoped teddy-suppression | all | +| Bank | Poll `self.account_value` deltas (deposits silent; withdrawals emit a notify but we poll for both) | T6 Tranzit/DieRise/Buried | +| Weapon locker | Poll `self.stored_weapon_data` transitions (avoid namespace call to prevent load errors on no-locker maps) | T6 Tranzit/DieRise/Buried | +| Easter Eggs | T4/T5/T6: canonical terminal-notify wait → `easter_egg;complete`. T7: per-step emission → server-side derives completion when all configured steps log. Both paths set `EasterEggOccurredAt` | all | +| Special rounds | Per-round `level.flag["_round"]` poll → `ZW;round_special;;` | T4: dog. T5: dog/monkey/thief. T6: dog/leaper. T7: dog/monkey/wasp/spider/robot/quad/boss/ee | +| Gobble Gums | `bgb_activation` player notify + `user_grabbed_bgb` machine notify + `bgb_machine_accessed` (refund-filtered for ghost balls) | T7 only | + +### Match ID stitching + +All four engines seed `sv_iw4m_zm_matchid` at init with `randomint(1000000) + "_" + randomint(1000000)` (~10¹² collision space). `gettime()` is deliberately avoided because the engine clock returns 0 at init on T4/T5/T6. The lookup index is `(ServerId, GameMatchId)` so cross-server collisions are harmless. + +### Concurrent K/RD drain + +All four wait 0.1s between K (death) emission and RD/RC (round data / complete) to prevent the C# side's concurrent processing from rolling up RD before the K death increment lands. + +## Caveats + +- **Special rounds break Seconds Per Horde** unless the SPH calculator skips them via the `ZW;round_special` signal (handled server-side). +- **T7 hash literals are frozen** because BO3 is end-of-life — `#"hash_21edb6b6"` (SoE Arnie) and `#"hash_6460283a"` (GK terminal) will not change but cannot be reverse-resolved to names (verified absent from the 3287-name candidate dict via `hash-name.ps1`). +- **T4 perk-buy** edge case: if a player buys a perk and is downed within 0.1s, the emission is dropped (no `perk_bought` notify exists on W@W; weapon-switch poll is the only path). +- **Self-revive** is emitted as `revive;self` on T5/T6/T7 only. T4 has no self-revive mechanic in W@W. + +## Related + +- [`FEATURE_MATRIX.md`](FEATURE_MATRIX.md) — cross-engine feature support matrix +- [`../README.md`](../README.md) — GameFiles overview (Game Interface, AntiCheat, ZombieStats) +- `Plugins/ZombieStats/` — free plugin (event parsing, IZombieStatsEnhancer interface) +- `_PRIVATE/ZombieStatsPremium/` — premium plugin (match history, leaderboards, EE detection, skill scoring) diff --git a/GameFiles/ZombieStats/_zm_stats_t4.gsc b/GameFiles/ZombieStats/_zm_stats_t4.gsc new file mode 100644 index 000000000..3f72b76ef --- /dev/null +++ b/GameFiles/ZombieStats/_zm_stats_t4.gsc @@ -0,0 +1,1833 @@ +#include maps\_utility; +#include common_scripts\utility; +#include maps\_zombiemode_utility; + +Init() +{ + // Seed the bootstrap dvar so IW4MAdmin can recover the current round + // when it starts (or reconnects to RCon) mid-match. Updated after every + // RC event in PrintPlayerRoundData. Defaults to round 1 here so a + // bootstrap during the very first round still resolves correctly. + setdvar( "sv_iw4m_zm_round", 1 ); + + // Stable per-match ID so IW4MAdmin can stitch a restarted/reconnected + // process back onto the existing EFZombieMatch row instead of creating + // a new orphaned match. Two randomints give ~10^12 collision space — + // overkill for the "at most a few open matches per server" lookup. + // gettime() returns 0 at Init time on T4 (engine clock not yet running), + // so we don't use it here. The lookup index is (ServerId, GameMatchId) + // so cross-server collisions are harmless either way. + // Set once per Init (= once per map load). + setdvar( "sv_iw4m_zm_matchid", "" + randomint( 1000000 ) + "_" + randomint( 1000000 ) ); + + thread WaitForRoundChange(); + thread WaitForPlayerConnect(); + thread WaitForPowerupSpawned(); + thread WaitForWeaponPurchases(); + thread WaitForPackAPunch(); + thread WaitForDoorPurchases(); + // Box detection: all four stock T4 maps (Nacht/Verrückt/Shi No Numa/ + // Der Riese) plus Der-Riese-derived customs. No map-name check — + // outcome resolution polls user_grabbed_weapon notify + self.timedOut + // + iw4m_box_teddy_marker, all of which exist uniformly across the + // stock maps. See the header comment above WaitForMysteryBox. + thread WaitForMysteryBox(); + thread WaitForBoxTeddySuppression(); + thread WaitForTrapActivations(); + thread WaitForEasterEggComplete(); + thread WaitForEasterEggSteps(); + thread WatchPowerSwitches(); + thread WatchPowerStateChanges(); + thread WatchZombiesRemaining(); + + // --- Zombie Event Log Format --- // + // Combat events (legacy format): + // AK, AD, K, D = kills/damage (unchanged) + // RD, RC = round data/complete (unchanged) + // + // Unified ZP/ZW format: + // ZP;{player};down = player downed + // ZP;{player};revive;{reviver} = player co-op revived + // ZP;{player};revive;self = self-revive (T5+; T4 never emits — no self-revive mechanic in W@W) + // ZP;{player};perk;buy;{perkName};{cost} = perk purchased (weapon-switch poll — T4 has no perk_bought notify) + // ZP;{player};powerup;grab;{powerupName} = powerup grabbed + // ZP;{player};weapon;buy;{weaponName};{cost} = wall weapon purchase + // ZP;{player};weapon;upgrade;{old};{new};{cost} = pack-a-punch + // ZP;{player};weapon;abandon;{weapon};{cost} = pap timeout + // ZP;{player};box;take;{weaponName};{cost} = box weapon taken + // ZP;{player};box;pass;{weaponName};{cost} = box weapon passed + // ZP;{player};box;teddy;{cost} = teddy bear (box moves) + // ZP;{player};door;buy;{cost} = door/debris opened + // ZP;{player};trap;activate;{trapType};{cost} = trap activated + // ZW;round_special;{round};{type} = dog round (T4 only has dog_round flag) + // ZW;zombies;{round};{remaining};{alive} = engine zombies-left poll + // ZW;power;on;world|player;[info] = power activated (T4 never powers off) + // ZW;easter_egg;step;{key} = EE progress + // ZW;easter_egg;complete;{map} = EE finalised + + SetupCallbacks(); + thread WatchdogCallbacks(); +} + +SetupCallbacks() +{ + waittillframeend; + + // zombie damage events + level.callbackActorDamageOriginal = level.callbackActorDamage; + level.callbackActorKilledOriginal = level.callbackActorKilled; + level.callbackActorDamage = ::OnActorDamage; + level.callbackActorKilled = ::OnActorKilled; + + // player damage events + level.callbackPlayerDamageOriginal = level.callbackPlayerDamage; + level.callbackPlayerDamage = ::OnPlayerDamaged; + + // down/revive events + level.callbackPlayerLastStandOriginal = level.callbackPlayerLastStand; + level.callbackPlayerLastStand = ::OnPlayerDowned; + + // Powerups are observed via proximity polling in WaitForPowerupSpawned + // / WaitForPowerupGrab, NOT by hooking level.zombie_powerup_grab_func. + // Hooking that callback replaces the engine's own grab function, which + // stops the powerup effect from being applied (Max Ammo wouldn't refill, + // Insta-Kill wouldn't trigger, etc.). +} + +///////////////////////////////////////////////////////// +// Re-installs our combat-event hooks if a map script +// overwrites them post-init. Stock T4 maps don't do this, +// but custom maps occasionally chain or replace the +// callbacks — without this watchdog we'd silently lose +// AD/AK/D/down events for the rest of the game. +///////////////////////////////////////////////////////// +WatchdogCallbacks() +{ + // Give map scripts time to finish their own init before we start + // policing — most overrides happen during the first second. + wait ( 1 ); + + for ( ;; ) + { + if ( level.callbackActorDamage != ::OnActorDamage ) + { + level.callbackActorDamageOriginal = level.callbackActorDamage; + level.callbackActorDamage = ::OnActorDamage; + } + + if ( level.callbackActorKilled != ::OnActorKilled ) + { + level.callbackActorKilledOriginal = level.callbackActorKilled; + level.callbackActorKilled = ::OnActorKilled; + } + + if ( level.callbackPlayerDamage != ::OnPlayerDamaged ) + { + level.callbackPlayerDamageOriginal = level.callbackPlayerDamage; + level.callbackPlayerDamage = ::OnPlayerDamaged; + } + + if ( level.callbackPlayerLastStand != ::OnPlayerDowned ) + { + level.callbackPlayerLastStandOriginal = level.callbackPlayerLastStand; + level.callbackPlayerLastStand = ::OnPlayerDowned; + } + + // Once stable this is essentially free — five-second polling + // is fine because callback overrides are init-time events. + wait ( 5 ); + } +} + +//-----------------// +//---- Waiters ----// +//-----------------// + +///////////////////////////////////////////////////////// +// Waits until a player connects and spawns the +// monitoring threads +///////////////////////////////////////////////////////// +WaitForPlayerConnect() +{ + for ( ;; ) + { + level waittill( "connecting", player ); + + // the PlayerRevived callback is not setup to allow overriding + // so we need to wait for the hard-coded player notify + player thread WaitForPlayerRevive(); + + // verruckt does not track the perks as stats like der reise and shi no, + // so we wait for the weapon to switch to perk weapon bottle + player thread WaitForPlayerWeaponSwitch(); + + // zm mode does not actually kill a player after down timer expires + // they get put into spectator without a kill callback + player thread WaitForPlayerZombified(); + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player revive timer expires +// Prints a "Kill" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerZombified() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // zombified notify occurs when a player is moved to spectator + // after downed timer expires + self waittill( "zombified" ); + playerInfo = BuildPlayerInfoString( self ); + + LogPrint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player is revived +// Prints a "Player Revived" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerRevive() +{ + self endon ( "disconnect" ); + + for ( ;; ) + { + self waittill( "player_revived", reviver ); + + // give time for the weapon switch to occur + wait ( 0.1 ); + + if ( !IsDefined( reviver ) || !IsPlayer( reviver ) ) + { + players = get_players(); + + // reviver only passed on der riese, so we have check if anyone + // has used the revive weapon recently instead + for ( i = 0; i < players.size; i++ ) + { + didPerformRevive = IsPlayer( players[i] ) && + IsDefined( players[i].lastUsedSyrette ) && + gettime() - players[i].lastUsedSyrette <= 250; + + if ( didPerformRevive ) + { + reviver = players[i]; + players[i].lastUsedSyrette = 0; + break; + } + } + } + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;" + BuildPlayerInfoString( reviver ) + "\n" ); + } +} + +///////////////////////////////////////////////////////// +// Waits until player changes weapons and checks to see +// if weapon is a perk weapon. Prints to gamelog if true. +// +// Engine limitation: T4 has NO perk_bought notify and NO +// level.perk_bought_func hookpoint. vending_trigger_think +// in _zombiemode_perks.gsc grants the perk via SetPerk() +// + increments player.stats["perks"], but never emits any +// player notify we could subscribe to. Weapon-switch poll +// is therefore the only detection path on T4. Edge case: +// if a player buys a perk and is downed/killed before the +// next 0.1s tick, the perk-bought emission is dropped. +// T5/T6/T7 all moved to notify-driven detection. +///////////////////////////////////////////////////////// +WaitForPlayerWeaponSwitch() +{ + self endon( "disconnect" ); + + self waittill( "spawned_player" ); + currentWeapon = self getCurrentWeapon(); + + ///# + // todo: remove + //self.score = 1000000; + //#/ + + for ( ;; ) + { + wait ( 0.1 ); + + if ( !IsAlive ( self ) ) + { + continue; + } + + newWeapon = self getCurrentWeapon(); + + if ( currentWeapon != newWeapon ) + { + if ( currentWeapon == "syrette" ) + { + self.lastUsedSyrette = gettime(); + } + + currentWeapon = newWeapon; + + if ( IsSubStr( currentWeapon, "zombie_perk" ) ) + { + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";perk;buy;" + currentWeapon + ";0\n" ); + } + } + } +} + +///////////////////////////////////////////////////////// +// Periodically checks active script_models to see if any +// have a defined powerup_name. If so monitors for pickup +///////////////////////////////////////////////////////// +WaitForPowerupSpawned() +{ + powerupEntCount = 0; + + for ( ;; ) + { + // the powerup ent is not named and there are + // no events to tell us when one is spawned + // so we need to periodically check for changes + // and wait for a player to get in range + // additionally, overriding the level.zombie_powerup_grab_func + // prevents original powerup code from running + models = GetEntArray( "script_model", "classname" ); + powerupEnts = []; + + for ( i = 0; i < models.size; i++ ) + { + if( IsDefined( models[i].powerup_name ) && !IsDefined( models[i].isWaiting ) ) + { + powerupEnts[powerupEnts.size] = models[i]; + } + } + + if ( powerupEnts.size != 0 && powerupEnts.size != powerupEntCount ) + { + // we only want to start a new thread if the size increases + // if it's decreased that means a powerup despawned + if ( powerupEnts.size >= powerupEntCount ) + { + array_thread( powerupEnts, ::WaitForPowerupGrab ); + } + } + + powerupEntCount = powerupEnts.size; + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until a player gets within proximity of a powerup +// or the powerup despawns. Write powerup to game log +///////////////////////////////////////////////////////// +WaitForPowerupGrab() +{ + self.isWaiting = true; + + self endon( "powerup_timedout" ); + self endon( "powerup_grabbed" ); + + while ( IsDefined( self ) ) + { + players = Get_Players(); + + for ( i = 0; i < players.size; i++ ) + { + // this is not ideal, but this is the only way + // to properly replicate how the powerup grab + // is determined in the original code + if ( Distance( players[i].origin, self.origin ) < 64 ) + { + powerup = "unknown"; + + if ( IsDefined( self.powerup_name ) ) + { + powerup = self.powerup_name; + } + + self.isWaiting = false; + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( players[i] ) + ";powerup;grab;" + powerup + "\n" ); + + return; + } + } + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until the game is over or new round is initalized +// Writes round data to game log +///////////////////////////////////////////////////////// +WaitForRoundChange() +{ + for ( ;; ) + { + // intermission occurs when "game over" screen appears + // between_round_over occurs when the next round setup has completed + result = level waittill_any_return( "intermission", "between_round_over" ); + + /# + PrintLn( "WaitForRoundStart TRIGGERED" ); + #/ + + players = get_players(); + + for ( i = 0; i < players.size; i++ ) + { + // they were downed and not revived, so we already printed the event + if ( ( IsDefined( players[i].is_zombie ) && players[i].is_zombie ) ) + { + continue; + } + + // if there are no zombies alive, then the game is not over + if ( Get_Enemy_Count() == 0 ) + { + continue; + } + + // game is over so we print out their death + playerInfo = BuildPlayerInfoString( players[i] ); + LogPrint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } + + // IW4MAdmin reads the game log and processes events concurrently. + // When K (death) and RD (round data) events are emitted in the same + // server frame, they arrive simultaneously and IW4MAdmin may process + // the RD event's stat rollup before the K event's death increment, + // causing Deaths to be missing from match/aggregate totals. + // This wait ensures the K events are written to the log and processed + // before RD/RC events arrive. + wait ( 0.1 ); + + isGameOver = IsDefined( result ) && result == "intermission"; + PrintPlayerRoundData( isGameOver ); + + if ( isGameOver ) + { + break; + } + + // Detect special-round type for the round about to begin and emit a + // GSE;ZW;round_special;; line so IW4MAdmin can flag the round in the + // breakdown UI and skip Seconds-Per-Horde for round types where the + // static budget formula doesn't apply (dogs replace the regular + // zombie spawn budget; SPH would render visibly wrong otherwise). + // T4 only has dog rounds (Shi No Numa / Der Riese hellhounds); + // flag_exists guards keep us safe on maps that never initialised + // the flag (e.g. Nacht der Untoten, Verruckt). + EmitSpecialRoundIfAny(); + } +} + +EmitSpecialRoundIfAny() +{ + // T4 doesn't expose flag_exists() — guard with IsDefined on the underlying + // level.flag dict entry (portable across T4/T5/T6 and avoids T4's flag() + // assertEx on uninitialised flag names). + specialType = ""; + if ( IsDefined( level.flag ) && IsDefined( level.flag[ "dog_round" ] ) && level.flag[ "dog_round" ] ) + { + specialType = "dog"; + } + + if ( specialType != "" ) + { + logPrint( "GSE;ZW;round_special;" + level.round_number + ";" + specialType + "\n" ); + } +} + +//-------------------// +//---- Callbacks ----// +//-------------------// + +OnActorDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // we only want to log damage if they aren't going to die + if ( IsDefined( self.health ) && iDamage < self.health ) + { + // Cap reported damage at the victim's max HP — the engine can pass + // iDamage values far in excess of what the zombie could actually absorb + // (splash / environmental damage at high rounds). + reportedDamage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && reportedDamage > self.maxhealth ) + { + reportedDamage = self.maxhealth; + } + + LogPrint( "GSE;AD;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + reportedDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + } + + [[level.callbackActorDamageOriginal]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset ); +} + +OnActorKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, iTimeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // Cap kill damage at the victim's max HP so the final blow doesn't + // include overkill / engine-inflated iDamage. + damage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && damage > self.maxhealth ) + { + damage = self.maxhealth; + } + + LogPrint( "GSE;AK;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + damage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[level.callbackActorKilledOriginal]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, iTimeOffset ); +} + +OnPlayerDamaged( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + LogPrint( "GSE;D;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + iDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[level.callbackPlayerDamageOriginal]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ); +} + +OnPlayerDowned( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ) +{ + // sometimes this callback can be executed multiple times while the player is still downed + // this struct is set to undefined when they die or get revived + if ( IsDefined( self.revivetrigger ) ) + { + return; + } + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";down\n" ); + + [[level.callbackPlayerLastStandOriginal]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ); +} + +//-----------------// +//---- Helpers ----// +//-----------------// + +PrintPlayerRoundData( isGameOver ) +{ + // Skip emission entirely if level.round_number is undefined — fires during + // post-game-over shutdown / exit_level cleanup with no round context. Without + // this guard, currentRound defaults to 1 and emits a spurious GSE;RC;1 between + // the legit final RC and ExitLevel (see match 1614). + if ( !IsDefined( level.round_number ) ) + { + return; + } + + players = get_players(); + currentRound = level.round_number; + + for( i = 0; i < players.size; i++ ) + { + // Skip players who never spawned (e.g. joined mid-game into spectator) + // to avoid crediting them with starting points they never earned + if ( IsDefined( players[i].sessionstate ) && players[i].sessionstate == "spectator" ) + { + continue; + } + + totalScore = 0; + currentScore = 0; + + if ( IsDefined ( players[i].score_total ) ) + { + totalScore = players[i].score_total; + } + + if ( IsDefined ( players[i].score ) ) + { + currentScore = players[i].score; + } + + LogPrint( "GSE;RD;" + BuildPlayerInfoString( players[i] ) + ";" + totalScore + ";" + currentScore + ";" + currentRound + ";" + isGameOver + "\n" ); + } + + // Ensure all RD events are processed before RC triggers StartNextRound + // which clears round states. Without this wait, RC can race ahead of + // late-arriving RD events due to IW4MAdmin's concurrent event processing. + wait ( 0.1 ); + + setdvar( "sv_iw4m_zm_round", currentRound ); + LogPrint( "GSE;RC;" + currentRound + "\n" ); +} + +///////////////////////////////////////////////////////// +// Economy event hooks — wall buys, box, doors, traps +// T4 uses entity-based trigger listeners. +// No buildables in T4. Traps are per-map (electric only). +///////////////////////////////////////////////////////// + +WaitForWeaponPurchases() +{ + wait ( 2 ); + + triggers = getEntArray( "weapon_upgrade", "targetname" ); + + for ( i = 0; i < triggers.size; i++ ) + { + triggers[i] thread WatchWeaponPurchase(); + } +} + +// Wall buy triggers use targetname "weapon_upgrade" and have a +// .zombie_weapon_upgrade property with the weapon name. Cost is in +// level.zombie_weapons[weaponName].cost (NOT on the trigger entity — +// trigger.zombie_cost is only used for cabinet first-purchase pricing). +// +// Note: Pack-a-Punch is handled separately by WaitForPackAPunch. +// PaP uses "zombie_vending_upgrade" triggers — a completely different +// entity type. Earlier versions tried to detect PaP here via +// IsSubStr("_upgraded") but wall buy triggers never have upgraded +// weapon names on them. +WatchWeaponPurchase() +{ + for ( ;; ) + { + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + if ( !IsDefined( self.zombie_weapon_upgrade ) ) + { + continue; + } + + weaponName = self.zombie_weapon_upgrade; + + cost = 0; + if ( IsDefined( level.zombie_weapons ) && IsDefined( level.zombie_weapons[weaponName] ) && IsDefined( level.zombie_weapons[weaponName].cost ) ) + { + cost = level.zombie_weapons[weaponName].cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + // Trigger fires on any interaction, even if player can't afford it + if ( IsDefined( player.score ) && player.score < cost ) + { + continue; + } + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";weapon;buy;" + weaponName + ";" + cost + "\n" ); + } +} + +///////////////////////////////////////////////////////// +// T4 Pack-a-Punch detection (Der Riese + Der-Riese-derived custom maps). +// +// Engine reference: `_zombiemode_perks.gsc::vending_upgrade()` per +// "zombie_vending_upgrade" trigger entity. Key state/notifies: +// - self waittill("trigger", player) — BUY press +// - self.current_weapon = currentWeapon — set after engine accepts buy +// (gates: score>=5000, weapon +// in level.zombie_include_weapons +// with "_upgraded" variant, +// !laststand, !throwing, +// !switching) +// - ~3.5s upgrade animation +// - self notify("pap_taken") — engine fires when buyer grabs +// - self notify("pap_timeout") — engine fires after ~15s no grab +// - self.current_weapon = "" — iter ends +// +// Detection mirrors the box-detection design: +// - WatchPapTakenFlag / WatchPapTimeoutFlag — bridge engine notifies to +// per-iter flags for outcome resolution +// - WatchPapTriggerForBuyer — lock-first buyer capture with +// affordability + upgradeable gates client-side replicating engine's own +// checks. VerifyPapBuyerLock unlocks 0.25s later if engine didn't accept +// (handles the laststand/throwing/switching gates we don't replicate). +// - WatchPapOutcome — iter loop. Phase 1 polls +// current_weapon empty→non-empty (engine accepted a buy). Phase 2 polls +// non-empty→empty (iter ended). Resolves outcome from flag state with +// buyer-weapon match check, emits the appropriate event. +// +// Emits: +// - weapon;upgrade;{old};{new};5000 — pap_taken (player took the upgrade) +// - weapon;abandon;{weapon};5000 — pap_timeout (player walked away) +// +// Per-trigger state: +// - self.iw4m_pap_taken_flag — set by WatchPapTakenFlag, reset per iter +// - self.iw4m_pap_timeout_flag — set by WatchPapTimeoutFlag, reset per iter +// - self.iw4m_pap_buyer — locked by WatchPapTriggerForBuyer, +// unlocked by VerifyPapBuyerLock if rejected, +// reset per iter at Phase 1 entry +// - self.iw4m_pap_buyer_weapon — captured at lock time for the match check +///////////////////////////////////////////////////////// +WaitForPackAPunch() +{ + wait ( 2 ); + + triggers = getEntArray( "zombie_vending_upgrade", "targetname" ); + + if ( !IsDefined( triggers ) || triggers.size == 0 ) + { + return; + } + + for ( i = 0; i < triggers.size; i++ ) + { + triggers[i].iw4m_pap_buyer = undefined; + triggers[i].iw4m_pap_buyer_weapon = undefined; + triggers[i].iw4m_pap_taken_flag = false; + triggers[i].iw4m_pap_timeout_flag = false; + triggers[i].iw4m_pap_disconnect_flag = false; + + triggers[i] thread WatchPapOutcome(); + triggers[i] thread WatchPapTriggerForBuyer(); + triggers[i] thread WatchPapTakenFlag(); + triggers[i] thread WatchPapTimeoutFlag(); + triggers[i] thread WatchPapDisconnectFlag(); + } +} + +// First-notify-wins. Engine has stale per-iter threads (wait_for_player_to_take, +// wait_for_timeout) that can outlive their iter and fire pap_taken/pap_timeout +// AFTER another notify has already resolved the iter. Ignoring later notifies +// prevents misclassification (e.g., abandon emitted as upgrade when a stale +// take notify fires after timeout cleared the iter). +WatchPapTakenFlag() +{ + for ( ;; ) + { + self waittill( "pap_taken" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_taken_flag = true; + } +} + +WatchPapTimeoutFlag() +{ + for ( ;; ) + { + self waittill( "pap_timeout" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_timeout_flag = true; + } +} + +// T4 engine does NOT emit pap_player_disconnected (T6/T7's _zm_perks.gsc does; +// T4's _zombiemode_perks.gsc has no equivalent). We synthesise it via the +// WatchPapBuyerDisconnect helper threaded at lock time — watches the buyer's +// engine "disconnect" notify and re-emits as "pap_player_disconnected" on the +// trigger. Lets the rest of the pattern stay aligned with T6/T7 verbatim. +WatchPapDisconnectFlag() +{ + for ( ;; ) + { + self waittill( "pap_player_disconnected" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_disconnect_flag = true; + } +} + +WatchPapBuyerDisconnect() +{ + self endon( "pap_taken" ); + self endon( "pap_timeout" ); + + buyer = self.iw4m_pap_buyer; + if ( !IsDefined( buyer ) || !IsPlayer( buyer ) ) + { + return; + } + + buyer waittill( "disconnect" ); + self notify( "pap_player_disconnected" ); +} + +WatchPapTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + // Phase2 path: engine already accepted a buy (current_weapon set). + // The buyer is the player whose weapon engine just took — their + // GetCurrentWeapon() is now empty/none. Late F-pressers in phase2 + // still hold their own weapon, so this discriminates cleanly. + // Lock immediately with engine's current_weapon as authority; skip + // verify (engine already accepted). Without this path, scheduler + // ordering that runs the engine handler before ours causes legit + // buyers to be rejected as phase2 late-pressers, dropping the emit. + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + buyerWeapon = who GetCurrentWeapon(); + if ( IsDefined( buyerWeapon ) && buyerWeapon != "" && buyerWeapon != "none" ) + { + continue; + } + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = self.current_weapon; + self thread WatchPapBuyerDisconnect(); + continue; + } + + // Phase1 path: engine hasn't accepted yet. Replicate engine gates + // (score, upgradeable weapon) so we don't lock on rejected presses. + if ( !IsDefined( who.score ) || who.score < 5000 ) + { + continue; + } + + // T4 weapon naming: bare name with "_upgraded" variant in + // level.zombie_include_weapons (no _zm suffix as in T5/T6). + weapon = who GetCurrentWeapon(); + if ( weapon == "" || weapon == "none" ) + { + continue; + } + if ( !IsDefined( level.zombie_include_weapons ) || !IsDefined( level.zombie_include_weapons[weapon] ) ) + { + continue; + } + if ( !IsDefined( level.zombie_include_weapons[weapon + "_upgraded"] ) ) + { + continue; + } + + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = weapon; + self thread WatchPapBuyerDisconnect(); + + // Verify engine actually accepted within ~5 frames; unlock otherwise. + // Handles engine-side gates we don't replicate (laststand, throwing + // grenade, switching weapons). + self thread VerifyPapBuyerLock(); + } +} + +VerifyPapBuyerLock() +{ + // Poll for engine acceptance up to 2s. Single 0.25s wait was too short on + // T6 (engine sometimes delays setting self.current_weapon past 0.25s, + // unlocking legit buyer → later stale F-press re-locks wrong weapon → + // false mismatch → skipped emit). Polling fix is identical across T4/T5/T6 + // even though only T6 was observed failing — defensive consistency. + timeoutMs = 2000; + pollMs = 50; + elapsedMs = 0; + while ( elapsedMs < timeoutMs ) + { + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + return; + } + wait ( 0.05 ); + elapsedMs = elapsedMs + pollMs; + } + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + } +} + +WatchPapOutcome() +{ + for ( ;; ) + { + // Reset per-iter state + self.iw4m_pap_taken_flag = false; + self.iw4m_pap_timeout_flag = false; + self.iw4m_pap_disconnect_flag = false; + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + + // Phase 1: wait for engine to accept a buy (current_weapon set). + while ( !IsDefined( self.current_weapon ) || self.current_weapon == "" ) + { + wait ( 0.05 ); + } + + oldWeapon = self.current_weapon; + + // Phase 2: wait for iter boundary. Boundary = current_weapon changes + // (clears OR engine immediately starts a new iter with a different + // weapon in the same frame our poll would otherwise miss). Detecting + // weapon-change as an exit prevents losing back-to-back iters when + // engine clears + re-sets within one 50ms poll window. + while ( IsDefined( self.current_weapon ) && self.current_weapon == oldWeapon ) + { + wait ( 0.05 ); + } + + // Disconnect short-circuits emission (no player to credit). + if ( self.iw4m_pap_disconnect_flag ) + { + continue; + } + + // Resolve outcome from notify flags. + isTaken = self.iw4m_pap_taken_flag; + isTimeout = self.iw4m_pap_timeout_flag; + + // Emit policy: engine_weapon (oldWeapon) is authoritative for what got + // upgraded/abandoned. Locked buyer is best-effort attribution. Lock-vs- + // engine weapon mismatch happens when player switches weapons between + // F-presses or when scheduler ordering causes our lock to fire after + // engine commits — engine's choice wins. Skip emission only when no + // buyer was ever locked. + if ( !IsDefined( self.iw4m_pap_buyer ) || !IsPlayer( self.iw4m_pap_buyer ) ) + { + continue; + } + + if ( isTaken ) + { + newWeapon = oldWeapon + "_upgraded"; + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;upgrade;" + oldWeapon + ";" + newWeapon + ";5000\n" ); + } + else if ( isTimeout ) + { + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;abandon;" + oldWeapon + ";5000\n" ); + } + } +} + +WaitForDoorPurchases() +{ + wait ( 2 ); + + doors = getEntArray( "zombie_door", "targetname" ); + debris = getEntArray( "zombie_debris", "targetname" ); + + for ( i = 0; i < doors.size; i++ ) + { + doors[i] thread WatchDoorPurchase(); + } + + for ( i = 0; i < debris.size; i++ ) + { + debris[i] thread WatchDoorPurchase(); + } +} + +// Door triggers fire on ANY interaction, even if the player can't afford it. +// The game checks score internally before opening. We must do the same check +// to avoid logging failed purchase attempts. +WatchDoorPurchase() +{ + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + return; + } + + if ( !IsDefined( player.score ) || player.score < cost ) + { + return; + } + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";door;buy;" + cost + "\n" ); +} + +///////////////////////////////////////////////////////// +// T4 Mystery Box Detection (all stock T4 maps + Der-Riese-derived customs). +// +// Engine reference: `_zombiemode_weapons.gsc` per map. Key entities/state: +// - Chest trigger: targetname "treasure_chest_use" (all stock T4 maps) +// - Entity chain: chest → lid (self.target) → weapon_spawn_org (lid.target) +// - weapon_spawn_org.weapon_string: cycles ~40 times during the ~3.9s +// randomization animation; final value persists until the next pull. +// - self.chest_user: assigned in treasure_chest_think on Der-Riese-style +// chests only (Nacht/Verrückt/Shi No Numa don't set it). NOT relied on +// for outcome detection — set+cleared inside one cooperative-scheduler +// frame on instant grabs even on Der Riese, so polling can't see it. +// - self.timedOut: false at the top of every treasure_chest_think iter on +// all four stock maps, true after the 12s no-grab timeout. Used here as +// both the per-chest "is supported chest" discriminator (no map-name +// check) AND the pass outcome signal. +// - self notify("user_grabbed_weapon"): fires immediately before the +// weapon is given on all four stock maps + Der-Riese-derived customs +// (Nacht L323, Verrückt L447, Shi No L535, Der Riese L820). Primary +// take outcome signal — captured by WatchUserGrabbedFlag into +// self.iw4m_box_user_grabbed. +// - level notify("weapon_fly_away_start"): fires on the teddy-bear path +// (Verrückt + Shi No + Der Riese; Nacht has no teddy). Captured by +// WaitForBoxTeddySuppression into self.iw4m_box_teddy_marker, scoped +// to chests in late_phase so the mark can't bleed across iterations. +// - "trigger" notify on the chest entity: fires whenever a player presses +// USE on the chest. Recorded as self.iw4m_box_last_trigger and used +// as the buyer-attribution fallback when chest_user wasn't captured +// (always the case on Nacht/Verrückt/Shi No and on teddy paths). +// +// Outcome resolution (WatchBoxOutcome): +// Phase 3 polls three independent flags every 0.1s with a 25s hard cap. +// First-to-fire wins; priority is teddy > take > pass for the rare case +// that more than one is set. If no flag fires we skip emission rather +// than guess. Buyer comes from chest_user when available (Der Riese +// patient grabs), else from the trigger fallback. +// +// Per-chest state used by this subsystem: +// - self.iw4m_box_user_grabbed — set by WatchUserGrabbedFlag, reset per iter +// - self.iw4m_box_teddy_marker — set by suppression, consumed in Phase 3 +// - self.iw4m_box_in_late_phase — true between Phase 1 exit and iter end +// - self.iw4m_box_last_trigger — most recent player who pressed USE on +// this chest; reset at iter end +///////////////////////////////////////////////////////// +WaitForMysteryBox() +{ + wait ( 5 ); + + chests = getEntArray( "treasure_chest_use", "targetname" ); + + if ( !IsDefined( chests ) || chests.size == 0 ) + { + return; + } + + for ( i = 0; i < chests.size; i++ ) + { + chests[i].iw4m_box_teddy_marker = false; + chests[i].iw4m_box_in_late_phase = false; + chests[i].iw4m_box_user_grabbed = false; + + // Pre-cache weapon_spawn_org via lid traversal so WatchBoxOutcome + // can read weapon_string without re-traversing every poll. + if ( IsDefined( chests[i].target ) ) + { + lid = getent( chests[i].target, "targetname" ); + if ( IsDefined( lid ) && IsDefined( lid.target ) ) + { + weaponSpawnOrg = getent( lid.target, "targetname" ); + if ( IsDefined( weaponSpawnOrg ) ) + { + chests[i].iw4m_weapon_spawn_org = weaponSpawnOrg; + } + } + } + + chests[i] thread WatchBoxOutcome(); + chests[i] thread WatchBoxTriggerForBuyer(); + chests[i] thread WatchUserGrabbedFlag(); + } +} + +///////////////////////////////////////////////////////// +// Sets a per-chest "take" flag on the engine's user_grabbed_weapon +// notify. Fires on all four stock T4 maps (Nacht L323, Verrückt L447, +// Shi No L535, Der Riese L820), inside treasure_chest_think on `self` +// (the chest trigger) immediately before treasure_chest_give_weapon. +// +// Primary outcome signal for take detection — chest_user is unreliable +// across the maps that don't assign it (Nacht/Verrückt/Shi No), and even +// on Der Riese it gets set+cleared inside one frame on instant grabs. +// WatchBoxOutcome polls this flag (plus self.timedOut for pass and +// iw4m_box_teddy_marker for teddy) so we work uniformly across all maps. +///////////////////////////////////////////////////////// +WatchUserGrabbedFlag() +{ + for ( ;; ) + { + self waittill( "user_grabbed_weapon" ); + self.iw4m_box_user_grabbed = true; + } +} + +///////////////////////////////////////////////////////// +// Teddy bear suppression. +// +// Engine fires `level notify("weapon_fly_away_start")` on the teddy path +// (Verrückt + Shi No + Der Riese; Nacht has no teddy). We mark only chests +// in `iw4m_box_in_late_phase == true` so an idle chest that hasn't entered +// Phase 1 since the previous teddy can't carry a stale marker forward. On +// stock maps only one chest is active at a time, so exactly one chest gets +// marked per teddy event. +///////////////////////////////////////////////////////// +WaitForBoxTeddySuppression() +{ + wait ( 5 ); + + for ( ;; ) + { + level waittill( "weapon_fly_away_start" ); + + chests = getEntArray( "treasure_chest_use", "targetname" ); + if ( !IsDefined( chests ) || chests.size == 0 ) + { + continue; + } + + for ( k = 0; k < chests.size; k++ ) + { + if ( IsDefined( chests[k].iw4m_box_in_late_phase ) && chests[k].iw4m_box_in_late_phase ) + { + chests[k].iw4m_box_teddy_marker = true; + } + } + } +} + +///////////////////////////////////////////////////////// +// Parallel ground-truth capture of who pressed USE on the chest. +// +// `treasure_chest_think` consumes "trigger" via `self waittill("trigger", user)` +// to read the buyer. GSC's notify model lets multiple threads waittill on the +// same notify and all of them resume — so this thread peacefully co-exists, +// recording the buyer on `self.iw4m_box_last_trigger`. Phase 3 uses this as +// the buyer source whenever chest_user wasn't captured (always the case on +// Nacht/Verrückt/Shi No, and on teddy paths everywhere). +// +// Lock-on-first-valid-press: we record only the FIRST trigger of an +// iteration that comes from a player who could afford the buy, then ignore +// every subsequent press until iter end (which clears the lock). This is +// the buyer because: +// - The engine enforces `grabber == user` in treasure_chest_think on all +// four stock T4 maps + Der-Riese-derived customs (Nacht L319, Verrückt +// L443, Shi No L531, Der Riese L816). Other players' F presses fire +// "trigger" but the engine discards them. +// - The buyer's own grab press fires trigger again, but they're already +// locked so it's a noop. +// - Without the lock, in multiplayer, another player's noise press would +// overwrite the real buyer mid-iter and we'd misattribute. +// +// Affordability gate: a player can press F with insufficient funds — trigger +// fires but the engine rejects. Without the score check we'd lock the wrong +// player and never update when the real buyer presses. Cost is read from +// level.zombie_treasure_chest_cost (Der Riese-style global, defaults to 950). +///////////////////////////////////////////////////////// +WatchBoxTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_box_last_trigger ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) + { + cost = level.zombie_treasure_chest_cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + if ( !IsDefined( who.score ) || who.score < cost ) + { + continue; + } + + self.iw4m_box_last_trigger = who; + } +} + +WatchBoxOutcome() +{ + if ( !IsDefined( self.iw4m_weapon_spawn_org ) ) + { + return; + } + weaponSpawnOrg = self.iw4m_weapon_spawn_org; + + // Sentinel — never matches any real weapon name. Guards the first-iter + // case where weapon_string is still undefined. On subsequent iters, + // prevWeapon holds the previous iter's final weapon (stable until the + // next animation begins). + prevWeapon = "__none__"; + + for ( ;; ) + { + self.iw4m_box_user_grabbed = false; + + // Phase 1: wait for weapon_string to start changing (animation begins). + animStarted = false; + firstChanged = undefined; + while ( !animStarted ) + { + if ( IsDefined( weaponSpawnOrg.weapon_string ) ) + { + if ( weaponSpawnOrg.weapon_string != prevWeapon ) + { + animStarted = true; + firstChanged = weaponSpawnOrg.weapon_string; + } + else + { + prevWeapon = weaponSpawnOrg.weapon_string; + } + } + + if ( !animStarted ) + { + wait ( 0.1 ); + } + } + + self.iw4m_box_in_late_phase = true; + + // Phase 2: wait for weapon_string to stabilize (5 consecutive equal + // polls × 0.1s = ~0.5s of stability). Also opportunistically capture + // chest_user — present on Der Riese patient grabs, missed on instant + // grabs and absent entirely on Nacht/Verrückt/Shi No. + stableFrames = 0; + stableWeapon = firstChanged; + capturedUser = undefined; + while ( stableFrames < 5 ) + { + wait ( 0.1 ); + + currentWeapon = undefined; + if ( IsDefined( weaponSpawnOrg.weapon_string ) ) + { + currentWeapon = weaponSpawnOrg.weapon_string; + } + + if ( IsDefined( currentWeapon ) && currentWeapon == stableWeapon ) + { + stableFrames = stableFrames + 1; + } + else + { + stableFrames = 0; + if ( IsDefined( currentWeapon ) ) + { + stableWeapon = currentWeapon; + } + } + + if ( !IsDefined( capturedUser ) && IsDefined( self.chest_user ) && IsPlayer( self.chest_user ) ) + { + capturedUser = self.chest_user; + } + } + + weaponName = stableWeapon; + if ( !IsDefined( weaponName ) ) + { + weaponName = "undef"; + } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) + { + cost = level.zombie_treasure_chest_cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + // timedOutDefined is the per-chest "is supported chest" gate — only + // chests whose treasure_chest_think initialises self.timedOut emit + // outcomes. All four stock maps qualify; arbitrary custom chest + // implementations that don't init timedOut silently no-op. + timedOutDefined = 0; + if ( IsDefined( self.timedOut ) ) + { + timedOutDefined = 1; + } + + user = capturedUser; + if ( !IsDefined( user ) && IsDefined( self.chest_user ) && IsPlayer( self.chest_user ) ) + { + user = self.chest_user; + } + + // Phase 3: wait for one of three independent outcome signals — take + // (user_grabbed_weapon notify → iw4m_box_user_grabbed), pass (engine + // sets self.timedOut after 12s), or teddy (weapon_fly_away_start → + // iw4m_box_teddy_marker). 25s hard cap covers 12s timeout + grab + // animation + safety; hitting the cap means no signal fired and we + // skip emission rather than guessing. + closeWaits = 0; + maxWaits = 250; + for ( ;; ) + { + if ( self.iw4m_box_user_grabbed ) + { + break; + } + if ( IsDefined( self.timedOut ) && self.timedOut ) + { + break; + } + if ( IsDefined( self.iw4m_box_teddy_marker ) && self.iw4m_box_teddy_marker ) + { + break; + } + if ( closeWaits >= maxWaits ) + { + break; + } + wait ( 0.1 ); + closeWaits = closeWaits + 1; + } + + isTake = false; + if ( self.iw4m_box_user_grabbed ) + { + isTake = true; + } + isPass = false; + if ( IsDefined( self.timedOut ) && self.timedOut ) + { + isPass = true; + } + isTeddy = false; + if ( IsDefined( self.iw4m_box_teddy_marker ) && self.iw4m_box_teddy_marker ) + { + isTeddy = true; + } + self.iw4m_box_teddy_marker = false; + + // Buyer fallback — typical on Nacht/Verrückt/Shi No (no chest_user) + // and on teddy paths everywhere (chest_user never set on teddy). + // Gated on timedOutDefined so we don't emit for chests we don't + // understand the iteration boundaries of. + if ( !IsDefined( user ) && timedOutDefined == 1 + && IsDefined( self.iw4m_box_last_trigger ) && IsPlayer( self.iw4m_box_last_trigger ) ) + { + user = self.iw4m_box_last_trigger; + } + + // Outcome priority: teddy > take > pass. Mutually exclusive in + // practice; priority is defensive against engine weirdness. + if ( IsDefined( user ) ) + { + if ( isTeddy ) + { + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;teddy;" + cost + "\n" ); + } + else if ( isTake ) + { + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;take;" + weaponName + ";" + cost + "\n" ); + } + else if ( isPass ) + { + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;pass;" + weaponName + ";" + cost + "\n" ); + } + } + + self.iw4m_box_last_trigger = undefined; + self.iw4m_box_in_late_phase = false; + self.iw4m_box_user_grabbed = false; + + // Seed prevWeapon so next iter's Phase 1 waits for the NEXT animation. + prevWeapon = weaponName; + } +} + +///////////////////////////////////////////////////////// +// T4 traps are per-map with unique targetnames: +// Der Riese: warehouse_electric_trap, wuen_electric_trap, bridge_electric_trap +// Verrückt: gas_access +// Shi No Numa: elec_trap_trig, pendulum_buy_trigger +// +// Instead of listing every name, scan all trigger_use entities +// and match targetnames containing "trap" or known keywords. +// This catches custom map traps too (anything with "trap" in name). +///////////////////////////////////////////////////////// +WaitForTrapActivations() +{ + wait ( 2 ); + + allTriggers = getEntArray( "trigger_use", "classname" ); + + for ( i = 0; i < allTriggers.size; i++ ) + { + if ( !IsDefined( allTriggers[i].targetname ) ) + { + continue; + } + + name = allTriggers[i].targetname; + + // Match any trigger with "trap" in the name (covers electric_trap, fan_trap, acid_trap, etc.) + // Also match known non-standard names: + // Verrückt: gas_access (electric traps) + // Shi No Numa: pendulum_buy_trigger (flogger trap) + if ( IsSubStr( name, "trap" ) || name == "gas_access" || name == "pendulum_buy_trigger" ) + { + allTriggers[i] thread WatchTrapActivation(); + } + } +} + +WatchTrapActivation() +{ + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + for ( ;; ) + { + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + LogPrint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";trap;activate;electric;" + cost + "\n" ); + } +} + +//-----------------------// +//---- Utility/Infra ----// +//-----------------------// + +BuildPlayerInfoString( entity ) +{ + if ( IsPlayer( entity ) ) + { + guid = entity getGuid(); + clientNumber = entity getEntityNumber(); + team = entity.team; + name = entity.playername; + + if ( !IsDefined( name ) ) + { + name = "null"; + } + + return guid + ";" + clientNumber + ";" + team + ";" + name; + } + + return "-1;-1;axis;Zombie"; +} + +///////////////////////////////////////////////////////// +// Easter Egg main quest detection. +// +// T4 has only one map with a tracked EE — Der Riese's "Fly Trap" +// (achievement DLC3_ZOMBIE_ANTI_GRAVITY at nazi_zombie_factory.gsc:1530). +// The engine fires achievement_notify directly with no `level notify`. +// +// Phases: +// 1. Player shoots fly trap control panel with an upgraded weapon +// → flag_set("hide_and_seek") [phase START — NOT completion] +// 2. Player must then shoot all 3 hidden targets +// (ee_exp_monkey, ee_bowie_bear, ee_perk_bear). Each shot increments +// level.flytrap_counter. When counter == 3, EE is complete. +// +// We poll level.flytrap_counter rather than flag_wait on the per-target +// flags because those flags are flag_init'd inside hide_and_seek_target() +// only after flytrap() runs — flag_wait on an uninitialized flag asserts. +// Polling an integer is safe regardless of init order. +// +// Other T4 maps (Nacht, Verruckt, Shi No Numa) have no main EE → no-op. +///////////////////////////////////////////////////////// +WaitForEasterEggComplete() +{ + level endon( "end_game" ); + + if ( level.script != "nazi_zombie_factory" ) + { + logprint( "[ZM-EE] No EE watcher configured for map=" + level.script + "\n" ); + return; + } + + logprint( "[ZM-EE] Watcher armed: map=" + level.script + " counter=level.flytrap_counter target=3\n" ); + + // Step 1: panel-shot phase-start. Stock script flag_set("hide_and_seek") right + // after the panel is shot with an upgraded weapon. The watcher polls IsDefined + // before calling flag_wait, since stock flag_init runs inside flytrap() — not + // main — so the flag isn't guaranteed to exist at level-init time. + thread WatchDerRieseFlytrapPanel(); + + // Steps 2-4: poll level.flytrap_counter and emit a step event each time it + // ticks up. Engine is single-threaded so a ticked-past value would be missed + // if we slept too long between checks; 0.5s easily covers human reaction time. + prev = 0; + while ( !IsDefined( level.flytrap_counter ) || level.flytrap_counter < 3 ) + { + if ( IsDefined( level.flytrap_counter ) && level.flytrap_counter > prev ) + { + // Fire one step event per integer tick (covers the rare case the + // counter jumps by >1, though stock script increments by exactly 1). + for ( i = prev + 1; i <= level.flytrap_counter && i <= 3; i++ ) + { + stepKey = "t4_dr_flytrap_target_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } + prev = level.flytrap_counter; + } + wait ( 0.5 ); + } + + // The 2→3 tick exits the while early so the final step never enters the loop + // body. Flush any unobserved steps now (covers prev=2→3, plus pathological + // multi-tick cases where counter jumped >1 across the exit boundary). + for ( i = prev + 1; i <= 3; i++ ) + { + stepKey = "t4_dr_flytrap_target_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } + + if ( IsDefined( level.iw4m_ee_fired ) && level.iw4m_ee_fired ) + { + logprint( "[ZM-EE] Suppressed re-emit on map=" + level.script + " (already fired)\n" ); + return; + } + level.iw4m_ee_fired = true; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] EE complete fired for map=" + level.script + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;complete;" + level.script + "\n" ); +} + +// Panel-shot phase-start. Stock factory.gsc flag_set("hide_and_seek") inside +// flytrap() once the upgraded-weapon shot lands; block on the flag rather than +// hooking the entity directly since the panel target ent isn't named consistently. +WatchDerRieseFlytrapPanel() +{ + level endon( "end_game" ); + + // Poll until flag_init has run. Stock flytrap() calls flag_init("hide_and_seek") + // partway through level setup — calling flag_wait before that point would do + // `!level.flag["hide_and_seek"]` on undefined and throw a runtime cast error. + while ( !IsDefined( level.flag ) || !IsDefined( level.flag[ "hide_and_seek" ] ) ) + { + wait ( 0.5 ); + } + + flag_wait( "hide_and_seek" ); + + logprint( "[ZM-EE] Step fired: t4_dr_flytrap_panel\n" ); + logprint( "GSE;ZW;easter_egg;step;t4_dr_flytrap_panel\n" ); +} + +///////////////////////////////////////////////////////// +// Easter Egg STEP detection — song-egg progression. +// +// Each supported stock T4 map has ONE song EE, modelled as a single +// completion step. Der Riese is the exception: 3 distinct meteor stones +// to find, modelled as 3 steps. +// +// • Verrückt (nazi_zombie_asylum): press E on `toilet` 3x. Stock +// toilet_useage() sets level.eggs=1 + setmusicstate("eggs") on the +// 3rd trigger. Poll for level.eggs == 1. +// +// • Shi No Numa (nazi_zombie_sumpf): press E on `toilet` (the phone) +// 4x — off-hook + dial 9-1-1. Stock sets level.eggs=1 + setmusic- +// state("eggs") after the dial sequence. Poll for level.eggs == 1. +// +// • Der Riese (nazi_zombie_factory): three meteor USE-triggers, +// unchanged from prior implementation. +// +// • Nacht der Untoten (nazi_zombie_prototype): NOT supported. Stock's +// kzmb radio damage path is broken on Pluto T4 dedicated servers — +// the entity exists but never receives the "damage" notify, so no +// reliable hook is available. Verified empirically; map omitted. +// +// All paths gate on first-player-connect: Pluto T4 dedicated servers +// pause level-time when no players are connected, so 3-second waits +// effectively block forever on an empty lobby and stock entity init +// may complete after our level-init thread starts. +// +// Custom maps that don't use these entity names silently no-op. +///////////////////////////////////////////////////////// +WaitForEasterEggSteps() +{ + level endon( "end_game" ); + + // Pluto T4 dedi: level-time only ticks with players connected. Block until + // the first one shows up so subsequent waits / getent calls behave normally. + while ( getplayers().size == 0 ) + { + wait ( 1 ); + } + + switch ( level.script ) + { + case "nazi_zombie_asylum": HookEggsFlagSong( "t4_vr_song" ); return; + case "nazi_zombie_sumpf": HookEggsFlagSong( "t4_sh_song" ); return; + case "nazi_zombie_factory": HookDerRieseMeteors(); return; + default: + logprint( "[ZM-EE] No step watcher configured for map=" + level.script + "\n" ); + return; + } +} + +// Verrückt + Shi No Numa — both stock GSCs set level.eggs=1 + +// setmusicstate("eggs") at the moment the song trigger fires (after the 3rd +// toilet press on Verrückt, after the dial sequence on Shi No). Polling +// level.eggs is map-agnostic and survives any GSC trigger-sequence quirks. +HookEggsFlagSong( stepKey ) +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: " + stepKey + " (polling level.eggs)\n" ); + + // 0.5s matches the cadence used by WatchDerRieseFlytrapPanel/flytrap_counter + // — well within human reaction time, low CPU cost. + while ( !IsDefined( level.eggs ) || level.eggs != 1 ) + { + wait ( 0.5 ); + } + + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); +} + +// Use-trigger path (Der Riese — meteor stones, press E to interact). +// Stock nazi_zombie_factory::main spawns three meteor_egg() threads on +// targetnames meteor_one / meteor_two / meteor_three, each emitting +// "trigger" exactly once when used. +HookDerRieseMeteors() +{ + level endon( "end_game" ); + + targetNames = []; + targetNames[0] = "meteor_one"; + targetNames[1] = "meteor_two"; + targetNames[2] = "meteor_three"; + + // Per-target poll because stock factory::main runs the meteor_egg setup + // alongside other level scripts; entity availability isn't strictly ordered. + for ( i = 0; i < targetNames.size; i++ ) + { + thread HookOneDerRieseMeteor( targetNames[i], i + 1 ); + } +} + +HookOneDerRieseMeteor( targetName, oneBasedIndex ) +{ + level endon( "end_game" ); + + trig = undefined; + for ( attempt = 0; attempt < 30; attempt++ ) + { + trig = getent( targetName, "targetname" ); + if ( IsDefined( trig ) ) + { + break; + } + wait ( 1 ); + } + + if ( !IsDefined( trig ) ) + { + logprint( "[ZM-EE] Der Riese meteor not found: " + targetName + " (custom map?)\n" ); + return; + } + + stepKey = "t4_dr_meteor_" + oneBasedIndex; + logprint( "[ZM-EE] Step watcher armed: " + stepKey + " target=" + targetName + "\n" ); + + // Stock script ALSO hooks waittill("trigger", player) on this entity to + // increment level.meteor_counter. waittill is broadcast — both threads + // receive the notify, so we don't interfere with the stock counter logic. + trig waittill( "trigger" ); + + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); +} + +///////////////////////////////////////////////////////// +// PWR — Map power state monitoring (T4) +///////////////////////////////////////////////////////// +// +// T4 has no unified "power_on" flag (the T5/T6 _zm.gsc system doesn't exist +// here). Detection instead piggybacks on the per-perk activation notifies +// fired from each perk machine when power comes online — every T4 zombie +// map with power (Verrückt, Shi No Numa, Der Riese) fires +// "specialty_quickrevive_power_on" since QR is universal across them. Nacht +// has no perks at all, so the notify never fires there — correct behaviour. +// +// One-shot dedup via a level flag — multiple perk notifies fire in sequence +// when the master switch is hit; we want a single power_on event per match. +// T4 has no power-off mechanic; only the on side is implemented. +// +// Player attribution mirrors T5/T6: parallel use-trigger watcher records the +// activator on level for the most recent ~5s, applied to the power-on emit +// if recent. Falls back to world otherwise. +///////////////////////////////////////////////////////// + +WatchPowerSwitches() +{ + level endon( "end_game" ); + + wait ( 2 ); + + candidates = []; + candidates[0] = "use_power_switch"; + candidates[1] = "power_switch_trig"; + candidates[2] = "power_button"; + + triggers = []; + for ( c = 0; c < candidates.size; c++ ) + { + ents = getentarray( candidates[c], "targetname" ); + for ( i = 0; i < ents.size; i++ ) + { + triggers[triggers.size] = ents[i]; + } + } + + for ( t = 0; t < triggers.size; t++ ) + { + triggers[t] thread WatchSinglePowerSwitch(); + } +} + +WatchSinglePowerSwitch() +{ + level endon( "end_game" ); + self endon( "death" ); + + for ( ;; ) + { + self waittill( "trigger", who ); + if ( IsPlayer( who ) ) + { + level._iw4m_power_activator = who; + level._iw4m_power_activator_time = gettime(); + } + } +} + +WatchPowerStateChanges() +{ + level endon( "end_game" ); + + // Wait for the canonical T4 power-on signal — fires on every map that + // has a power switch + perk machines (Verrückt, Shi No Numa, Der Riese). + // Nacht has no perks → notify never fires → no event ever emitted. + level waittill( "specialty_quickrevive_power_on" ); + + if ( IsDefined( level._iw4m_power_emitted ) && level._iw4m_power_emitted ) + { + return; + } + level._iw4m_power_emitted = true; + + activator = undefined; + if ( IsDefined( level._iw4m_power_activator ) && IsDefined( level._iw4m_power_activator_time ) ) + { + if ( gettime() - level._iw4m_power_activator_time < 5000 ) + { + activator = level._iw4m_power_activator; + } + } + + if ( IsDefined( activator ) ) + { + logPrint( "GSE;ZW;power;on;player;" + BuildPlayerInfoString( activator ) + "\n" ); + } + else + { + logPrint( "GSE;ZW;power;on;world\n" ); + } +} + +// Periodic emission of (round, zombies-remaining-to-spawn, currently-alive). Lets +// the live modal compute true "zombies cleared" = budget - remaining - alive, +// which captures trap kills / environmental kills / friendly grenade splash — +// all things that don't credit to a player's kill count but still reduce the +// round's spawn pool. Without this, live SPH would systematically over-estimate +// pace on trap-heavy strategies (Verruckt electric trap, Der Riese teleporters). +// +// Throttle: every 5s, only emits when remaining or alive changed since last +// emission. Matches the live-modal poll cadence — no point emitting faster +// than the UI refreshes. ~12 lines/min on an active round, none during +// intermission. +WatchZombiesRemaining() +{ + level endon( "end_game" ); + last_remaining = -1; + last_alive = -1; + while ( true ) + { + wait( 5 ); + if ( !IsDefined( level.zombie_total ) || !IsDefined( level.round_number ) ) + { + continue; + } + remaining = level.zombie_total; + alive = get_enemy_count(); + if ( remaining == last_remaining && alive == last_alive ) + { + continue; + } + logPrint( "GSE;ZW;zombies;" + level.round_number + ";" + remaining + ";" + alive + "\n" ); + last_remaining = remaining; + last_alive = alive; + } +} diff --git a/GameFiles/ZombieStats/_zm_stats_t5.gsc b/GameFiles/ZombieStats/_zm_stats_t5.gsc new file mode 100644 index 000000000..0c3f4b825 --- /dev/null +++ b/GameFiles/ZombieStats/_zm_stats_t5.gsc @@ -0,0 +1,2117 @@ +#include maps\_utility; +#include common_scripts\utility; +#include maps\_zombiemode_utility; + +// ───────────────────────────────────────────────────────────────── +// T5 Zombie Stats — Game Log Event Emitter +// ───────────────────────────────────────────────────────────────── +// +// This script is the T5 (Black Ops 1) port of _zm_stats_t4.gsc. +// T5 shares T4's CamelCase callback naming and stable callback +// chain, but has one key difference: +// +// - Ascension and Shangri La override level.perk_bought_func +// with ::monkey_perk_bought for monkey round perk tracking, +// silently discarding any prior hook. +// +// To avoid this, we use the "perk_bought" player notify fired by +// _zombiemode_perks::give_perk() on every perk acquisition, +// rather than hooking level.perk_bought_func directly. +// +// ───────────────────────────────────────────────────────────────── + +init() +{ + // Seed the bootstrap dvar so IW4MAdmin can recover the current round + // when it starts (or reconnects to RCon) mid-match. Updated after every + // RC event in PrintPlayerRoundData. Defaults to round 1 here so a + // bootstrap during the very first round still resolves correctly. + setdvar( "sv_iw4m_zm_round", 1 ); + + // Stable per-match ID so IW4MAdmin can stitch a restarted/reconnected + // process back onto the existing EFZombieMatch row instead of creating + // a new orphaned match. Two randomints give ~10^12 collision space — + // overkill for the "at most a few open matches per server" lookup. + // gettime() returns 0 at init time (engine clock not yet running), so + // we don't use it here. The lookup index is (ServerId, GameMatchId) + // so cross-server collisions are harmless either way. + // Set once per init (= once per map load). + setdvar( "sv_iw4m_zm_matchid", "" + randomint( 1000000 ) + "_" + randomint( 1000000 ) ); + + thread WaitForRoundChange(); + thread WaitForPlayerConnect(); + thread WaitForPowerupSpawned(); + thread WaitForWeaponPurchases(); + thread WaitForDoorPurchases(); + // T5 box detection — Kino-style chests + Der-Riese-derived custom + // maps. Notify-driven (randomization_done / box_spin_done on + // chest_origin) with 3-tier user resolution + scoped teddy + // suppression. See header comment above WaitForMysteryBox. + thread WaitForMysteryBox(); + thread WaitForBoxTeddySuppression(); + thread WaitForPackAPunch(); + thread WaitForTrapActivations(); + thread WaitForAutoTurrets(); + thread WaitForEasterEggComplete(); + thread WaitForEasterEggSteps(); + thread WatchPowerSwitches(); + thread WatchPowerStateChanges(); + thread WatchZombiesRemaining(); + + // --- Zombie Event Log Format --- // + // Combat events (legacy format): AK, AD, K, D, RD, RC + // Unified ZE format: down, revive, perk, powerup, weapon, box, door, trap + + SetupCallbacks(); + thread WatchdogCallbacks(); +} + +SetupCallbacks() +{ + waittillframeend; + + // zombie damage events + // T5 uses CamelCase callback names (same as T4) + level.callbackActorDamageOriginal = level.callbackActorDamage; + level.callbackActorKilledOriginal = level.callbackActorKilled; + level.callbackActorDamage = ::OnActorDamage; + level.callbackActorKilled = ::OnActorKilled; + + // player damage events + level.callbackPlayerDamageOriginal = level.callbackPlayerDamage; + level.callbackPlayerDamage = ::OnPlayerDamaged; + + // down/revive events + level.callbackPlayerLastStandOriginal = level.callbackPlayerLastStand; + level.callbackPlayerLastStand = ::OnPlayerDowned; +} + +///////////////////////////////////////////////////////// +// Re-installs our combat-event hooks if a map script +// overwrites them post-init. Stock T5 maps don't do this, +// but custom maps occasionally chain or replace the +// callbacks — without this watchdog we'd silently lose +// AD/AK/D/down events for the rest of the game. +///////////////////////////////////////////////////////// +WatchdogCallbacks() +{ + // Give map scripts time to finish their own init before we start + // policing — most overrides happen during the first second. + wait ( 1 ); + + for ( ;; ) + { + if ( level.callbackActorDamage != ::OnActorDamage ) + { + level.callbackActorDamageOriginal = level.callbackActorDamage; + level.callbackActorDamage = ::OnActorDamage; + } + + if ( level.callbackActorKilled != ::OnActorKilled ) + { + level.callbackActorKilledOriginal = level.callbackActorKilled; + level.callbackActorKilled = ::OnActorKilled; + } + + if ( level.callbackPlayerDamage != ::OnPlayerDamaged ) + { + level.callbackPlayerDamageOriginal = level.callbackPlayerDamage; + level.callbackPlayerDamage = ::OnPlayerDamaged; + } + + if ( level.callbackPlayerLastStand != ::OnPlayerDowned ) + { + level.callbackPlayerLastStandOriginal = level.callbackPlayerLastStand; + level.callbackPlayerLastStand = ::OnPlayerDowned; + } + + // Once stable this is essentially free — five-second polling + // is fine because callback overrides are init-time events. + wait ( 5 ); + } +} + +//-----------------// +//---- Waiters ----// +//-----------------// + +///////////////////////////////////////////////////////// +// Waits until a player connects and spawns the +// monitoring threads +///////////////////////////////////////////////////////// +WaitForPlayerConnect() +{ + for ( ;; ) + { + level waittill( "connecting", player ); + + // T5 passes the reviver as a parameter to the player_revived notify + // so we do not need weapon switch monitoring to identify the reviver + player thread WaitForPlayerRevive(); + + // zm mode does not actually kill a player after down timer expires + // they get put into spectator without a kill callback + player thread WaitForPlayerZombified(); + + // _zombiemode_perks::give_perk() fires "perk_bought" on the player + // for every perk acquisition on every map. This is more reliable than + // level.perk_bought_func which can be overridden by map-specific + // scripts (e.g. Ascension/Shangri La monkey round tracking) + player thread WaitForPerkBought(); + // PaP moved to trigger-based polling in init() — see WatchPackAPunch + + ///# + // todo: remove — debug: give max points for testing + // Must wait for spawned_player or game overwrites score with default + //player thread DebugGiveScore(); + //#/ + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player revive timer expires +// Prints a "Kill" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerZombified() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // zombified notify occurs when a player is moved to spectator + // after downed timer expires + self waittill( "zombified" ); + playerInfo = BuildPlayerInfoString( self ); + + logPrint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player is revived +// Prints a "Player Revived" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerRevive() +{ + self endon ( "disconnect" ); + + for ( ;; ) + { + // T5 passes the reviver as a parameter to the notify + self waittill( "player_revived", reviver ); + + // Self-revive (solo Quick Revive auto): reviver==self. Emit distinct + // subtype so downstream classification doesn't need to compare guids. + if ( IsDefined( reviver ) && IsPlayer( reviver ) && reviver == self ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;self\n" ); + } + else + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;" + BuildPlayerInfoString( reviver ) + "\n" ); + } + } +} + +///////////////////////////////////////////////////////// +// Waits for the "perk_bought" notify fired by +// _zombiemode_perks::give_perk() whenever a perk is +// acquired. This works on all maps regardless of +// level.perk_bought_func overrides. +///////////////////////////////////////////////////////// +WaitForPerkBought() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + self waittill( "perk_bought", perk ); + + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";perk;buy;" + perk + ";0\n" ); + } +} + +///////////////////////////////////////////////////////// +// Periodically checks active script_models to see if any +// have a defined powerup_name. If so monitors for pickup +///////////////////////////////////////////////////////// +WaitForPowerupSpawned() +{ + powerupEntCount = 0; + + for ( ;; ) + { + // the powerup ent is not named and there are + // no events to tell us when one is spawned + // so we need to periodically check for changes + // and wait for a player to get in range + // additionally, overriding the level.zombie_powerup_grab_func + // prevents original powerup code from running + models = GetEntArray( "script_model", "classname" ); + powerupEnts = []; + + for ( i = 0; i < models.size; i++ ) + { + if( IsDefined( models[i].powerup_name ) && !IsDefined( models[i].isWaiting ) ) + { + powerupEnts[powerupEnts.size] = models[i]; + } + } + + if ( powerupEnts.size != 0 && powerupEnts.size != powerupEntCount ) + { + // we only want to start a new thread if the size increases + // if it's decreased that means a powerup despawned + if ( powerupEnts.size >= powerupEntCount ) + { + array_thread( powerupEnts, ::WaitForPowerupGrab ); + } + } + + powerupEntCount = powerupEnts.size; + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until a player gets within proximity of a powerup +// or the powerup despawns. Write powerup to game log +///////////////////////////////////////////////////////// +WaitForPowerupGrab() +{ + self.isWaiting = true; + + self endon( "powerup_timedout" ); + self endon( "powerup_grabbed" ); + + while ( IsDefined( self ) ) + { + players = get_players(); + + for ( i = 0; i < players.size; i++ ) + { + // this is not ideal, but this is the only way + // to properly replicate how the powerup grab + // is determined in the original code + if ( Distance( players[i].origin, self.origin ) < 64 ) + { + powerup = "unknown"; + + if ( IsDefined( self.powerup_name ) ) + { + powerup = self.powerup_name; + } + + self.isWaiting = false; + + logPrint( "GSE;ZP;" + BuildPlayerInfoString( players[i] ) + ";powerup;grab;" + powerup + "\n" ); + + return; + } + } + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until the game is over or new round is initalized +// Writes round data to game log +///////////////////////////////////////////////////////// +WaitForRoundChange() +{ + for ( ;; ) + { + // intermission occurs when "game over" screen appears + // between_round_over occurs when the next round setup has completed + result = level waittill_any_return( "intermission", "between_round_over" ); + + /# + println( "WaitForRoundStart TRIGGERED" ); + #/ + + players = get_players(); + + for ( i = 0; i < players.size; i++ ) + { + // they were downed and not revived, so we already printed the event + if ( ( IsDefined( players[i].is_zombie ) && players[i].is_zombie ) ) + { + continue; + } + + // if there are no zombies alive, then the game is not over + if ( get_enemy_count() == 0 ) + { + continue; + } + + // game is over so we print out their death + playerInfo = BuildPlayerInfoString( players[i] ); + logPrint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } + + // IW4MAdmin reads the game log and processes events concurrently. + // When K (death) and RD (round data) events are emitted in the same + // server frame, they arrive simultaneously and IW4MAdmin may process + // the RD event's stat rollup before the K event's death increment, + // causing Deaths to be missing from match/aggregate totals. + // This wait ensures the K events are written to the log and processed + // before RD/RC events arrive. + wait ( 0.1 ); + + isGameOver = IsDefined( result ) && result == "intermission"; + PrintPlayerRoundData( isGameOver ); + + if ( isGameOver ) + { + break; + } + + // Detect special-round type for the round about to begin and emit a + // GSE;ZW;round_special;; line so IW4MAdmin can flag the round in the + // breakdown UI and skip Seconds-Per-Horde for round types where the + // static budget formula doesn't apply (full special rounds replace the + // regular zombie spawn budget; SPH would render visibly wrong otherwise). + // T5 maps where these flags exist: dog_round (universal), monkey_round + // (Ascension Space Monkeys), thief_round (Five Pentagon Thief). + // flag_exists guards keep us safe on maps that never initialised the flag. + EmitSpecialRoundIfAny(); + } +} + +EmitSpecialRoundIfAny() +{ + // Use IsDefined-based flag checks for cross-game consistency with T4 (which + // doesn't expose flag_exists()) and to avoid asserting on maps that never + // initialised the flag (Verruckt has no dogs, Five has no monkeys, etc). + specialType = ""; + if ( IsDefined( level.flag ) && IsDefined( level.flag[ "dog_round" ] ) && level.flag[ "dog_round" ] ) + { + specialType = "dog"; + } + else if ( IsDefined( level.flag ) && IsDefined( level.flag[ "monkey_round" ] ) && level.flag[ "monkey_round" ] ) + { + specialType = "monkey"; + } + else if ( IsDefined( level.flag ) && IsDefined( level.flag[ "thief_round" ] ) && level.flag[ "thief_round" ] ) + { + specialType = "thief"; + } + + if ( specialType != "" ) + { + logPrint( "GSE;ZW;round_special;" + level.round_number + ";" + specialType + "\n" ); + } +} + +//-------------------// +//---- Callbacks ----// +//-------------------// + +// T5 actor damage signature matches T4: (eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset) +OnActorDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // we only want to log damage if they aren't going to die + if ( IsDefined( self.health ) && iDamage < self.health ) + { + // Cap reported damage at the victim's max HP — the engine can pass + // iDamage values far in excess of what the zombie could actually absorb + // (splash / environmental damage at high rounds). + reportedDamage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && reportedDamage > self.maxhealth ) + { + reportedDamage = self.maxhealth; + } + + logPrint( "GSE;AD;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + reportedDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + } + + [[ level.callbackActorDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset ); +} + +// T5 actor killed signature matches T4: (eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, iTimeOffset) +OnActorKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, iTimeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // Cap kill damage at the victim's max HP so the final blow doesn't + // include overkill / engine-inflated iDamage. + damage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && damage > self.maxhealth ) + { + damage = self.maxhealth; + } + + logPrint( "GSE;AK;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + damage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[ level.callbackActorKilledOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, iTimeOffset ); +} + +// T5 player damage signature matches T4: (eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset) +OnPlayerDamaged( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + logPrint( "GSE;D;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + iDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[ level.callbackPlayerDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, timeOffset ); +} + +// T5 laststand signature matches T4: (eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration) +OnPlayerDowned( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ) +{ + // sometimes this callback can be executed multiple times while the player is still downed + // this struct is set to undefined when they die or get revived + if ( IsDefined( self.revivetrigger ) ) + { + return; + } + + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";down\n" ); + + [[ level.callbackPlayerLastStandOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ); +} + +//-----------------// +//---- Helpers ----// +//-----------------// + +PrintPlayerRoundData( isGameOver ) +{ + // Skip emission entirely if level.round_number is undefined — fires during + // post-game-over shutdown / exit_level cleanup with no round context. Without + // this guard, currentRound defaults to 1 and emits a spurious GSE;RC;1 between + // the legit final RC and ExitLevel (see match 1614). + if ( !IsDefined( level.round_number ) ) + { + return; + } + + players = get_players(); + currentRound = level.round_number; + + for( i = 0; i < players.size; i++ ) + { + // Skip players who never spawned (e.g. joined mid-game into spectator) + // to avoid crediting them with starting points they never earned + if ( IsDefined( players[i].sessionstate ) && players[i].sessionstate == "spectator" ) + { + continue; + } + + totalScore = 0; + currentScore = 0; + + if ( IsDefined ( players[i].score_total ) ) + { + totalScore = players[i].score_total; + } + + if ( IsDefined ( players[i].score ) ) + { + currentScore = players[i].score; + } + + logPrint( "GSE;RD;" + BuildPlayerInfoString( players[i] ) + ";" + totalScore + ";" + currentScore + ";" + currentRound + ";" + isGameOver + "\n" ); + } + + // Ensure all RD events are processed before RC triggers StartNextRound + // which clears round states. Without this wait, RC can race ahead of + // late-arriving RD events due to IW4MAdmin's concurrent event processing. + wait ( 0.1 ); + + setdvar( "sv_iw4m_zm_round", currentRound ); + logPrint( "GSE;RC;" + currentRound + "\n" ); +} + +///////////////////////////////////////////////////////// +// Economy event hooks — wall buys, box, PaP, doors, traps +// T5 uses entity-based trigger listeners (no level notifies +// for weapon_bought like T6) +///////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////// +// T5 Pack-a-Punch — DEBUG INSTRUMENTED BUILD +// +// Mirrors the T4 redesign (notify-driven, lock-first buyer attribution, +// distinct take/timeout outcomes via pap_taken/pap_timeout notifies). +// See _zm_stats_t4.gsc for the design rationale and engine flow notes. +// +// T5 engine reference (`_zombiemode_perks.gsc::vending_weapon_upgrade`): +// - Trigger targetname: "zombie_vending_upgrade" (same as T4) +// - Field: self.current_weapon (set after engine accepts buy) +// - Take notify: self notify("pap_taken") ~L562 +// - Timeout notify: self notify("pap_timeout") ~L601 +// - Cost: 5000 hardcoded +// - Timeout: level.packapunch_timeout = 15s +// +// Same flow as T4 — direct port of the design. +///////////////////////////////////////////////////////// +WaitForPackAPunch() +{ + wait ( 2 ); + + triggers = getEntArray( "zombie_vending_upgrade", "targetname" ); + + if ( !IsDefined( triggers ) || triggers.size == 0 ) + { + return; + } + + for ( i = 0; i < triggers.size; i++ ) + { + triggers[i].iw4m_pap_buyer = undefined; + triggers[i].iw4m_pap_buyer_weapon = undefined; + triggers[i].iw4m_pap_taken_flag = false; + triggers[i].iw4m_pap_timeout_flag = false; + triggers[i].iw4m_pap_disconnect_flag = false; + + triggers[i] thread WatchPapOutcome(); + triggers[i] thread WatchPapTriggerForBuyer(); + triggers[i] thread WatchPapTakenFlag(); + triggers[i] thread WatchPapTimeoutFlag(); + triggers[i] thread WatchPapDisconnectFlag(); + } +} + +// First-notify-wins. Engine has stale per-iter threads (wait_for_player_to_take, +// wait_for_timeout) that can outlive their iter and fire pap_taken/pap_timeout +// AFTER another notify has already resolved the iter. Ignoring later notifies +// prevents misclassification (e.g., abandon emitted as upgrade when a stale +// take notify fires after timeout cleared the iter). +WatchPapTakenFlag() +{ + for ( ;; ) + { + self waittill( "pap_taken" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_taken_flag = true; + } +} + +WatchPapTimeoutFlag() +{ + for ( ;; ) + { + self waittill( "pap_timeout" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_timeout_flag = true; + } +} + +// T5 engine does NOT emit pap_player_disconnected (T6/T7's _zm_perks.gsc does; +// T5's _zombiemode_perks.gsc has no equivalent). We synthesise it via the +// WatchPapBuyerDisconnect helper threaded at lock time — watches the buyer's +// engine "disconnect" notify and re-emits as "pap_player_disconnected" on the +// trigger. Lets the rest of the pattern stay aligned with T6/T7 verbatim. +WatchPapDisconnectFlag() +{ + for ( ;; ) + { + self waittill( "pap_player_disconnected" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_disconnect_flag = true; + } +} + +WatchPapBuyerDisconnect() +{ + self endon( "pap_taken" ); + self endon( "pap_timeout" ); + + buyer = self.iw4m_pap_buyer; + if ( !IsDefined( buyer ) || !IsPlayer( buyer ) ) + { + return; + } + + buyer waittill( "disconnect" ); + self notify( "pap_player_disconnected" ); +} + +WatchPapTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + // Phase2 path: engine already accepted a buy (current_weapon set). + // The buyer is the player whose weapon engine just took — their + // GetCurrentWeapon() is now empty/none. Late F-pressers in phase2 + // still hold their own weapon, so this discriminates cleanly. + // Lock immediately with engine's current_weapon as authority; skip + // verify (engine already accepted). Without this path, scheduler + // ordering that runs the engine handler before ours causes legit + // buyers to be rejected as phase2 late-pressers, dropping the emit. + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + buyerWeapon = who GetCurrentWeapon(); + if ( IsDefined( buyerWeapon ) && buyerWeapon != "" && buyerWeapon != "none" ) + { + continue; + } + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = self.current_weapon; + self thread WatchPapBuyerDisconnect(); + continue; + } + + // Phase1 path: engine hasn't accepted yet. Replicate engine gates + // (score, upgradeable weapon) so we don't lock on rejected presses. + if ( !IsDefined( who.score ) || who.score < 5000 ) + { + continue; + } + + // T5 weapons have _zm suffix; upgrade variant is at + // level.zombie_weapons[weapon].upgrade_name (NOT weapon + "_upgraded" + // — that produces "weapon_zm_upgraded" vs real "weapon_upgraded_zm"). + weapon = who GetCurrentWeapon(); + if ( weapon == "" || weapon == "none" ) + { + continue; + } + if ( !IsDefined( level.zombie_weapons ) || !IsDefined( level.zombie_weapons[weapon] ) ) + { + continue; + } + if ( !IsDefined( level.zombie_weapons[weapon].upgrade_name ) ) + { + continue; + } + + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = weapon; + self thread WatchPapBuyerDisconnect(); + + // Verify engine actually accepted; unlock otherwise. Handles engine-side + // gates we don't replicate (laststand, throwing grenade, switching). + self thread VerifyPapBuyerLock(); + } +} + +VerifyPapBuyerLock() +{ + // Poll for engine acceptance up to 2s. Single 0.25s wait was too short on + // T6 (engine sometimes delays setting self.current_weapon past 0.25s, + // unlocking legit buyer → later stale F-press re-locks wrong weapon → + // false mismatch → skipped emit). Polling fix is identical across + // T4/T5/T6 even though only T6 was observed failing — defensive + // consistency. + timeoutMs = 2000; + pollMs = 50; + elapsedMs = 0; + while ( elapsedMs < timeoutMs ) + { + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + return; + } + wait ( 0.05 ); + elapsedMs = elapsedMs + pollMs; + } + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + } +} + +WatchPapOutcome() +{ + for ( ;; ) + { + // Reset per-iter state + self.iw4m_pap_taken_flag = false; + self.iw4m_pap_timeout_flag = false; + self.iw4m_pap_disconnect_flag = false; + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + + // Phase 1: wait for engine to accept a buy (current_weapon set). + while ( !IsDefined( self.current_weapon ) || self.current_weapon == "" ) + { + wait ( 0.05 ); + } + + oldWeapon = self.current_weapon; + + // Phase 2: wait for iter boundary. Boundary = current_weapon changes + // (clears OR engine immediately starts a new iter with a different + // weapon in the same frame our poll would otherwise miss). Detecting + // weapon-change as an exit prevents losing back-to-back iters when + // engine clears + re-sets within one 50ms poll window. + while ( IsDefined( self.current_weapon ) && self.current_weapon == oldWeapon ) + { + wait ( 0.05 ); + } + + // Disconnect short-circuits emission (no player to credit). + if ( self.iw4m_pap_disconnect_flag ) + { + continue; + } + + // Resolve outcome from notify flags. + isTaken = self.iw4m_pap_taken_flag; + isTimeout = self.iw4m_pap_timeout_flag; + + // Emit policy: engine_weapon (oldWeapon) is authoritative for what got + // upgraded/abandoned. Locked buyer is best-effort attribution. Lock-vs- + // engine weapon mismatch happens when player switches weapons between + // F-presses or when scheduler ordering causes our lock to fire after + // engine commits — engine's choice wins. Skip emission only when no + // buyer was ever locked. + if ( !IsDefined( self.iw4m_pap_buyer ) || !IsPlayer( self.iw4m_pap_buyer ) ) + { + continue; + } + + // Resolve actual cost engine charged. T5 engine sets self.cost + // on the trigger (5000 base, 1000 during bonfire sale). T5 has no + // attachment_cost mechanic (no re-PaP). + cost = 5000; + if ( IsDefined( self.cost ) ) { cost = self.cost; } + + if ( isTaken ) + { + newWeapon = oldWeapon + "_upgraded"; + if ( IsDefined( level.zombie_weapons ) && IsDefined( level.zombie_weapons[oldWeapon] ) && IsDefined( level.zombie_weapons[oldWeapon].upgrade_name ) ) + { + newWeapon = level.zombie_weapons[oldWeapon].upgrade_name; + } + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;upgrade;" + oldWeapon + ";" + newWeapon + ";" + cost + "\n" ); + } + else if ( isTimeout ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;abandon;" + oldWeapon + ";" + cost + "\n" ); + } + } +} + +WaitForWeaponPurchases() +{ + wait ( 2 ); + + triggers = getEntArray( "weapon_upgrade", "targetname" ); + + for ( i = 0; i < triggers.size; i++ ) + { + triggers[i] thread WatchWeaponPurchase(); + } +} + +WatchWeaponPurchase() +{ + for ( ;; ) + { + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + if ( !IsDefined( self.zombie_weapon_upgrade ) ) + { + continue; + } + + weaponName = self.zombie_weapon_upgrade; + + // Cost stored in level.zombie_weapons table, with zombie_cost as fallback + cost = 0; + if ( IsDefined( level.zombie_weapons ) ) + { + if ( IsDefined( level.zombie_weapons[weaponName] ) ) + { + if ( IsDefined( level.zombie_weapons[weaponName].cost ) ) + { + cost = level.zombie_weapons[weaponName].cost; + } + } + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + // Check player can afford it — trigger fires on any interaction + if ( IsDefined( player.score ) ) + { + if ( player.score < cost ) + { + continue; + } + } + + logPrint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";weapon;buy;" + weaponName + ";" + cost + "\n" ); + } +} + +WaitForDoorPurchases() +{ + wait ( 2 ); + + doors = getEntArray( "zombie_door", "targetname" ); + debris = getEntArray( "zombie_debris", "targetname" ); + + for ( i = 0; i < doors.size; i++ ) + { + doors[i] thread WatchDoorPurchase(); + } + + for ( i = 0; i < debris.size; i++ ) + { + debris[i] thread WatchDoorPurchase(); + } +} + +// Door triggers fire on ANY interaction, even if the player can't afford it. +// Check score before logging to avoid false positives. +WatchDoorPurchase() +{ + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + return; + } + + if ( !IsDefined( player.score ) || player.score < cost ) + { + return; + } + + logPrint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";door;buy;" + cost + "\n" ); +} + +///////////////////////////////////////////////////////// +// T5 Mystery Box Detection (Kino-style chests + Der-Riese-derived +// custom maps). +// +// Engine reference: `_zombiemode_weapons.gsc` from the T5 ZM scripts. +// Key entities/state: +// - Chest trigger: targetname "treasure_chest_use" +// - self.chest_origin: pre-cached on the chest at init (L948 ref). +// Holds .weapon_string which cycles during randomization, then is +// cleared by the engine at L2227. +// - self.chest_user: assigned BEFORE randomization on T5 (L1124/1131, +// unlike T4 which sets it AFTER), cleared at L1296. The pre-anim +// assignment gives a wider window than T4 — chest_user is reliably +// observable at randomization_done time on the normal grab path. +// - self.timedOut: false at top of every iter (L1156), true on 12s +// no-grab timeout (L1242). Used as the per-chest "is Der-Riese- +// -style chest" discriminator (no map-name check) for tier-3 +// resolution gating. +// - chest_origin "randomization_done" notify (L2145): emitted once per +// iter after the spin completes, with weapon_string already final. +// We use this as the iter-start signal. +// - chest_origin "box_spin_done" notify (L2228): emitted at the end of +// treasure_chest_weapon_spawn after grab/timeout/teddy. Used as the +// iter-end signal. self.timedOut is safe to read here because the +// engine then does `wait 3` at L1286 before resetting it. +// - level "weapon_fly_away_start" notify (L2150): fires on the teddy +// path ~0.5s after randomization_done. +// +// Why notify-driven instead of polling: the original polling design +// conflated multiple engine box iterations into one detection during +// F-spam (chest_user transitioned undefined→defined inside one poll +// interval). Each engine pull emits exactly one randomization_done + +// one box_spin_done, so notify boundaries give 1:1 mapping. +// +// 3-tier user resolution (same shape as T4): +// 1. capturedUser snapshotted at randomization_done resume +// 2. live self.chest_user at box_spin_done resume +// 3. self.iw4m_box_last_trigger (parallel waittill ground truth) +// — gated on IsDefined(self.timedOut) +// Tier 3 is what catches teddy attribution: the engine drops from L2145 +// → L1296 in one frame on the teddy path (no yield), so chest_user has +// already been cleared by the time we resume — both tier 1 and tier 2 +// miss. The trigger waittill captured the buyer earlier. +// +// Teddy bears: emit `box;teddy;{cost}` matching T6's shape. weapon_string +// is set undefined by the engine at L2123 before randomization_done, so +// no weapon name is reported. +// +// Per-chest state: +// - self.iw4m_box_teddy_marker — set by suppression, consumed at +// box_spin_done +// - self.iw4m_box_in_late_phase — true between rand_done and iter +// end; suppression only marks chests with this flag +// - self.iw4m_box_last_trigger — parallel trigger capture, cleared +// at iter end +// +// T5 GSC quirk: do NOT rely on short-circuit `IsDefined(x) && x` — +// nested ifs throughout. +///////////////////////////////////////////////////////// +WaitForMysteryBox() +{ + wait ( 5 ); + + chests = getEntArray( "treasure_chest_use", "targetname" ); + + if ( !IsDefined( chests ) ) + { + return; + } + + if ( chests.size == 0 ) + { + return; + } + + for ( i = 0; i < chests.size; i++ ) + { + chests[i].iw4m_box_teddy_marker = false; + chests[i].iw4m_box_in_late_phase = false; + + chests[i] thread WatchBoxOutcome(); + chests[i] thread WatchBoxTriggerForBuyer(); + } +} + +///////////////////////////////////////////////////////// +// Teddy bear suppression — same scoping rule as T4: only mark chests +// flagged as iw4m_box_in_late_phase, otherwise level-scoped marking +// bleeds across iterations. +///////////////////////////////////////////////////////// +WaitForBoxTeddySuppression() +{ + wait ( 5 ); + for ( ;; ) + { + level waittill( "weapon_fly_away_start" ); + + chests = getEntArray( "treasure_chest_use", "targetname" ); + if ( !IsDefined( chests ) ) + { + continue; + } + if ( chests.size == 0 ) + { + continue; + } + + for ( k = 0; k < chests.size; k++ ) + { + if ( IsDefined( chests[k].iw4m_box_in_late_phase ) ) + { + if ( chests[k].iw4m_box_in_late_phase ) + { + chests[k].iw4m_box_teddy_marker = true; + } + } + } + } +} + +///////////////////////////////////////////////////////// +// Parallel ground-truth capture of who pressed USE on the chest. +// +// Multiple GSC threads can waittill the same notify and all resume, +// so this peacefully co-exists with treasure_chest_think's two +// waittills (BUY at L1080, GRAB at L1201). +// +// Lock-on-first-valid-press: record only the FIRST trigger of an +// iteration that comes from a player who could afford the buy, then +// ignore every subsequent press until iter end clears the lock. This +// is the buyer because the engine enforces buyer-only-can-grab +// (treasure_chest_think rejects "trigger" from non-buyers). The +// buyer's own GRAB press fires trigger again but they're already +// locked so it's a noop. Without the lock, in multi-player F-spam, +// another player's noise press would overwrite the real buyer. +// +// Affordability gate: replicates the engine's own score check so a +// player pressing F with insufficient funds doesn't lock attribution. +///////////////////////////////////////////////////////// +WatchBoxTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_box_last_trigger ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) + { + cost = level.zombie_treasure_chest_cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + if ( !IsDefined( who.score ) || who.score < cost ) + { + continue; + } + + self.iw4m_box_last_trigger = who; + } +} + +WatchBoxOutcome() +{ + if ( !IsDefined( self.chest_origin ) ) + { + return; + } + + for ( ;; ) + { + // Iter starts when the engine signals randomization is done. + self.chest_origin waittill( "randomization_done" ); + + // Mark for teddy suppression scoping. Cleared at iter end. + self.iw4m_box_in_late_phase = true; + + // Snapshot weapon + buyer immediately. weapon_string is undefined + // on the teddy path (engine clears it at L2123 BEFORE rand_done). + // chest_user may also be undefined on teddy because the engine + // drops L2145 → L1296 in one frame without yielding — that's why + // tier-3 trigger fallback is required for teddy attribution. + weaponName = "undef"; + if ( IsDefined( self.chest_origin.weapon_string ) ) + { + weaponName = self.chest_origin.weapon_string; + } + + capturedUser = undefined; + if ( IsDefined( self.chest_user ) ) + { + if ( IsPlayer( self.chest_user ) ) + { + capturedUser = self.chest_user; + } + } + + // Wait for outcome. + self.chest_origin waittill( "box_spin_done" ); + + // Safe to read final state — engine sleeps `wait 3` at L1286 + // before resetting timedOut for the next iter. + timedOutDefined = 0; + timedOutValue = false; + if ( IsDefined( self.timedOut ) ) + { + timedOutDefined = 1; + if ( self.timedOut ) + { + timedOutValue = true; + } + } + + isTeddy = false; + if ( IsDefined( self.iw4m_box_teddy_marker ) ) + { + if ( self.iw4m_box_teddy_marker ) + { + isTeddy = true; + } + } + self.iw4m_box_teddy_marker = false; + + // 3-tier user resolution. + user = capturedUser; + if ( !IsDefined( user ) ) + { + if ( IsDefined( self.chest_user ) ) + { + if ( IsPlayer( self.chest_user ) ) + { + user = self.chest_user; + } + } + } + if ( !IsDefined( user ) ) + { + if ( timedOutDefined == 1 ) + { + if ( IsDefined( self.iw4m_box_last_trigger ) ) + { + if ( IsPlayer( self.iw4m_box_last_trigger ) ) + { + user = self.iw4m_box_last_trigger; + } + } + } + } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) + { + cost = level.zombie_treasure_chest_cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + if ( IsDefined( user ) ) + { + if ( isTeddy ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;teddy;" + cost + "\n" ); + } + else if ( timedOutValue ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;pass;" + weaponName + ";" + cost + "\n" ); + } + else + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;take;" + weaponName + ";" + cost + "\n" ); + } + } + + // Per-iter cleanup. + self.iw4m_box_last_trigger = undefined; + self.iw4m_box_in_late_phase = false; + } +} + +///////////////////////////////////////////////////////// +// T5 Trap Detection — POLLING _trap_in_use +// +// T5's trap system only fires "trap_activate" notify from +// trap_activate_electric(). Fire, rotating, and flipper traps +// do NOT fire this notify — they just run their activate function. +// Auto turrets (Kino) use a completely separate system. +// +// Solution: poll trap._trap_in_use instead. This flag is set to 1 +// by the game's trap_think() (line 317 ref) when ANY trap type is +// purchased, regardless of type. It's set to 0 on cooldown. +///////////////////////////////////////////////////////// +WaitForTrapActivations() +{ + wait ( 2 ); + + traps = getEntArray( "zombie_trap", "targetname" ); + + for ( i = 0; i < traps.size; i++ ) + { + traps[i] thread WatchTrapActivation(); + } + + // Also scan for non-standard trap triggers (custom maps) + allTriggers = getEntArray( "trigger_use", "classname" ); + + for ( i = 0; i < allTriggers.size; i++ ) + { + if ( !IsDefined( allTriggers[i].targetname ) ) + { + continue; + } + + name = allTriggers[i].targetname; + + if ( name == "zombie_trap" ) + { + continue; + } + + if ( IsSubStr( name, "trap" ) ) + { + allTriggers[i] thread WatchTrapActivation(); + } + } +} + +// Polls _trap_in_use instead of waittill("trap_activate"). +// _trap_in_use is set to 1 on purchase (all trap types), 0 on cooldown. +WatchTrapActivation() +{ + trapType = "trap"; + if ( IsDefined( self.script_noteworthy ) ) + { + trapType = self.script_noteworthy; + } + + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + for ( ;; ) + { + // Wait for trap to be activated (purchased) + // T5 GSC doesn't short-circuit || — use nested ifs + while ( true ) + { + if ( IsDefined( self._trap_in_use ) ) + { + if ( self._trap_in_use == 1 ) + { + break; + } + } + wait ( 0.2 ); + } + + players = getPlayers(); + closest = undefined; + closestDist = 99999; + + for ( i = 0; i < players.size; i++ ) + { + if ( !IsAlive( players[i] ) ) + { + continue; + } + + dist = distance( players[i].origin, self.origin ); + if ( dist < closestDist ) + { + closestDist = dist; + closest = players[i]; + } + } + + if ( IsDefined( closest ) ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( closest ) + ";trap;activate;" + trapType + ";" + cost + "\n" ); + } + + // Wait for trap to finish and cool down (_trap_in_use goes back to 0) + // before polling for the next activation + while ( IsDefined( self._trap_in_use ) ) + { + if ( self._trap_in_use != 1 ) + { + break; + } + wait ( 1 ); + } + } +} + +///////////////////////////////////////////////////////// +// T5 Auto Turret Detection +// +// Used on: Kino, Ascension, Call of the Dead, Five. +// Separate system from zombie_trap — uses _zombiemode_auto_turret module. +// Entities: script_noteworthy = "auto_turret_trigger" +// State: self.turret_active (true when active, false on cooldown) +// Cost: level.auto_turret_cost (default 1500) +///////////////////////////////////////////////////////// +WaitForAutoTurrets() +{ + wait ( 2 ); + + if ( !IsDefined( level.auto_turret_array ) ) + { + return; + } + + for ( i = 0; i < level.auto_turret_array.size; i++ ) + { + level.auto_turret_array[i] thread WatchAutoTurret(); + } +} + +WatchAutoTurret() +{ + cost = 1500; + if ( IsDefined( level.auto_turret_cost ) ) + { + cost = level.auto_turret_cost; + } + + for ( ;; ) + { + // Poll for turret_active going true (set on purchase) + while ( true ) + { + if ( IsDefined( self.turret_active ) ) + { + if ( self.turret_active ) + { + break; + } + } + wait ( 0.2 ); + } + + // Find nearest player + players = get_players(); + closest = undefined; + closestDist = 99999; + + for ( i = 0; i < players.size; i++ ) + { + if ( !IsAlive( players[i] ) ) + { + continue; + } + + dist = distance( players[i].origin, self.origin ); + if ( dist < closestDist ) + { + closestDist = dist; + closest = players[i]; + } + } + + if ( IsDefined( closest ) ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( closest ) + ";trap;activate;turret;" + cost + "\n" ); + } + + // Wait for turret to deactivate before re-polling + while ( true ) + { + if ( IsDefined( self.turret_active ) ) + { + if ( !self.turret_active ) + { + break; + } + } + wait ( 1 ); + } + } +} + +//-----------------------// +//---- Utility/Infra ----// +//-----------------------// + +///# +// todo: remove +DebugGiveScore() +{ + self endon( "disconnect" ); + self waittill( "spawned_player" ); + wait ( 0.5 ); + self.score = 1000000; +} +//#/ + +BuildPlayerInfoString( entity ) +{ + if ( IsPlayer( entity ) ) + { + guid = entity getGuid(); + clientNumber = entity getEntityNumber(); + team = entity.team; + name = entity.playername; + + if ( !IsDefined( name ) ) + { + name = "null"; + } + + return guid + ";" + clientNumber + ";" + team + ";" + name; + } + + return "-1;-1;axis;Zombie"; +} + +///////////////////////////////////////////////////////// +// Easter Egg main quest detection. +// +// Each T5 map with a "main" sidequest has a level notify fired when the +// quest reaches its terminal state. Ascension is the exception — its +// terminal is a flag (`weapons_combined`), not a notify, so its canonical +// emission lives inside HookAscensionCasimir below alongside the per-step +// flag watchers (single source of truth for the same flag). +// +// Reference: pulled from t5-scripts-main per-map *_sq.gsc / *_achievement.gsc. +// Custom maps / Five / Kino / Verruckt / Nacht / Shi No / Dead Ops have no +// main EE → silently no-op (debug log records "no watcher configured"). +///////////////////////////////////////////////////////// +WaitForEasterEggComplete() +{ + level endon( "end_game" ); + + notifyName = ""; + switch ( level.script ) + { + case "zombie_coast": notifyName = "coast_easter_egg_achieved"; break; // Call of the Dead + case "zombie_temple": notifyName = "temple_sidequest_achieved"; break; // Shangri-La + case "zombie_moon": notifyName = "moon_sidequest_big_bang_achieved"; break; // Moon + // zombie_cosmodrome: terminal is a flag, handled in HookAscensionCasimir. + default: + logprint( "[ZM-EE] No EE watcher configured for map=" + level.script + "\n" ); + return; + } + + logprint( "[ZM-EE] Watcher armed: map=" + level.script + " notify=" + notifyName + "\n" ); + + level waittill( notifyName ); + + if ( IsDefined( level.iw4m_ee_fired ) && level.iw4m_ee_fired ) + { + logprint( "[ZM-EE] Suppressed re-emit on map=" + level.script + " (already fired)\n" ); + return; + } + level.iw4m_ee_fired = true; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] EE complete fired for map=" + level.script + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;complete;" + level.script + "\n" ); +} + +///////////////////////////////////////////////////////// +// Easter Egg STEP detection — song-egg progression. +// +// T5 stock song-EE pattern is uniform across maps: N entities share a +// single targetname; each emits "trigger" on USE (press F); a stock +// per-map *_amb.gsc handler increments a level counter and fires +// change_zombie_music("egg") at counter==N. We hook the same trigger +// notify in parallel — broadcast, so we don't interfere with stock +// counter logic — and emit one step per entity. +// +// • Kino der Toten (zombie_theater) — `meteor_egg_trigger` × 3 +// • Five (zombie_pentagon) — `secret_phone_trig` × 3 +// +// All paths gate on first-player-connect: dedicated servers may pause +// level-time when no players are connected, so subsequent waits behave +// normally only after someone joins. +// +// Within a map, step number reflects iteration order from getentarray +// (stable per spawn) — not a physical mapping. From a "did they hit +// all N" perspective this is irrelevant. +// +// Custom maps that don't use these entity names silently no-op. +///////////////////////////////////////////////////////// +WaitForEasterEggSteps() +{ + level endon( "end_game" ); + + while ( getplayers().size == 0 ) + { + wait ( 1 ); + } + + switch ( level.script ) + { + case "zombie_theater": HookT5SongTriggers( "meteor_egg_trigger", "t5_kn_meteor" ); return; + case "zombie_pentagon": HookT5SongTriggers( "secret_phone_trig", "t5_fv_phone" ); return; + case "zombie_cosmodrome": HookAscension(); return; + case "zombie_coast": HookCallOfDead(); return; + case "zombie_temple": HookShangriLa(); return; + case "zombie_moon": HookMoon(); return; + default: + logprint( "[ZM-EE] No step watcher configured for map=" + level.script + "\n" ); + return; + } +} + +// Generic T5 song-EE hook — N USE-triggers sharing one targetname, each +// fires "trigger" on press. stepKeyPrefix gets _<1-based index> appended. +HookT5SongTriggers( targetName, stepKeyPrefix ) +{ + level endon( "end_game" ); + + // Poll for the entity batch — stock _amb scripts thread their setup with + // a leading wait, so triggers may not exist yet at our level-init time. + // 30s ceiling is well past stock init. + triggers = undefined; + for ( attempt = 0; attempt < 30; attempt++ ) + { + triggers = getentarray( targetName, "targetname" ); + if ( IsDefined( triggers ) && triggers.size > 0 ) + { + break; + } + wait ( 1 ); + } + + if ( !IsDefined( triggers ) || triggers.size == 0 ) + { + logprint( "[ZM-EE] " + level.script + ": no '" + targetName + "' entities found after polling (custom map?)\n" ); + return; + } + + logprint( "[ZM-EE] Step watcher armed: " + stepKeyPrefix + " count=" + triggers.size + "\n" ); + + for ( i = 0; i < triggers.size; i++ ) + { + triggers[i] thread WatchT5SongTriggerStep( stepKeyPrefix, i + 1 ); + } +} + +WatchT5SongTriggerStep( stepKeyPrefix, oneBasedIndex ) +{ + self endon( "death" ); + level endon( "end_game" ); + + stepKey = stepKeyPrefix + "_" + oneBasedIndex; + + self waittill( "trigger" ); + + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); +} + +///////////////////////////////////////////////////////// +// Ascension (zombie_cosmodrome) — two parallel quests: +// +// Song EE — three teddy bears (mus_teddybear). Stock spawns a runtime +// trigger_radius beneath each bear (player must touch + use), so we +// can't waittill("trigger") on the bear entity directly. Instead we +// poll level.teddybear_counter — same pattern as T4 Der Riese flytrap. +// +// Casimir Mechanism — six named flags, set as each step completes +// (zombie_cosmodrome_eggs.gsc::init flag_inits all six). Step 6's flag +// `weapons_combined` is also the canonical terminal for the main EE, +// so we emit BOTH the step event and the canonical map-level event +// from the same watcher (with the iw4m_ee_fired guard to prevent +// re-emit, mirroring WaitForEasterEggComplete's contract). +// +// Step 3 (`switches_synced`) is hard-gated to 4 players in stock script +// (`pressed == 4`), so the quest can't physically complete sub-4P. Premium +// config sets MinPlayers=4 on the casimir quest to hide it from leaderboard +// renders for sub-4P matches; step events still flow regardless. +///////////////////////////////////////////////////////// +HookAscension() +{ + level endon( "end_game" ); + + thread HookAscensionTeddyBears(); + thread HookAscensionCasimir(); +} + +HookAscensionTeddyBears() +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: t5_as_bear (polling level.teddybear_counter)\n" ); + + // Same loop shape as T4 Der Riese flytrap_counter. Engine is single- + // threaded so a ticked-past value would be missed if we slept too long; + // 0.5s easily covers human reaction time between bear interactions. + prev = 0; + while ( !IsDefined( level.teddybear_counter ) || level.teddybear_counter < 3 ) + { + if ( IsDefined( level.teddybear_counter ) && level.teddybear_counter > prev ) + { + for ( i = prev + 1; i <= level.teddybear_counter && i <= 3; i++ ) + { + stepKey = "t5_as_bear_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } + prev = level.teddybear_counter; + } + wait ( 0.5 ); + } + + // 2→3 tick exits the loop early — flush any unobserved steps. + for ( i = prev + 1; i <= 3; i++ ) + { + stepKey = "t5_as_bear_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } +} + +HookAscensionCasimir() +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: t5_as_cm (6 Casimir flags)\n" ); + + // Order matches stock zombie_cosmodrome_eggs.gsc::init flag_init order + // and the community walkthrough sequence (1=teleport, 2=power, 3=switches, + // 4=plate, 5=lander, 6=weapons). Stock physically gates step 3 to 4P; the + // others can be solo'd in script terms but the quest can't complete without + // step 3. Premium UI hides this quest entirely for sub-4P matches. + thread WatchT5Flag( "target_teleported", "t5_as_cm_1", false ); + thread WatchT5Flag( "rerouted_power", "t5_as_cm_2", false ); + thread WatchT5Flag( "switches_synced", "t5_as_cm_3", false ); + thread WatchT5Flag( "pressure_sustained", "t5_as_cm_4", false ); + thread WatchT5Flag( "passkey_confirmed", "t5_as_cm_5", false ); + thread WatchT5Flag( "weapons_combined", "t5_as_cm_6", true ); // canonical terminal +} + +WatchT5Flag( flagName, stepKey, isCanonical ) +{ + level endon( "end_game" ); + + // Stock flag_init runs in zombie_cosmodrome_eggs::init, but we may race + // it depending on script load order. Poll until level.flag[] exists + // before calling flag_wait, which would otherwise deref undefined and + // throw a runtime cast error (same gotcha as T4 Der Riese hide_and_seek). + while ( !IsDefined( level.flag ) || !IsDefined( level.flag[ flagName ] ) ) + { + wait ( 0.5 ); + } + + flag_wait( flagName ); + + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + + if ( !isCanonical ) + { + return; + } + + // Canonical terminal — also emit the map-level EE-complete event. + if ( IsDefined( level.iw4m_ee_fired ) && level.iw4m_ee_fired ) + { + logprint( "[ZM-EE] Suppressed re-emit on map=" + level.script + " (already fired)\n" ); + return; + } + level.iw4m_ee_fired = true; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] EE complete fired for map=" + level.script + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;complete;" + level.script + "\n" ); +} + +///////////////////////////////////////////////////////// +// Call of the Dead (zombie_coast) — two parallel quests: +// +// Song EE — three Element 115 fragments. Stock zombie_coast_amb.gsc +// spawns runtime trigger_radius beneath each `mus_easteregg` STRUCT +// (note: structs, not entities — getstructarray) and increments +// level.meteor_counter on each touch+use. Same poll pattern as +// Ascension teddy bears; can't waittill on a struct. +// +// Ensemble Cast — 9 named flag steps, terminal `dmf` flag set +// immediately before the existing `coast_easter_egg_achieved` notify. +// We DO NOT mark dmf as canonical here because WaitForEasterEggComplete +// already handles the notify — emitting the canonical from both paths +// would double-fire. Premium config sets MinPlayers=2 on the Ensemble +// Cast quest because: +// • Step 3 (vodka, flag `bd`) PHYSICALLY can't fire solo — virgo +// thread isn't spawned for `players.size <= 1` and there's no +// side-effect path. +// • Steps 4 (morse `aca`), 6 (foghorns `bp`), 7 (dials `ss`) DO +// fire automatically in solo via stock script side-effects (door- +// knock condition / metal_horse solo branch), so a solo run can +// complete the EE — but the resulting "all coop steps done" +// progression would advertise actions the player never performed. +// Hide entirely on solo runs, render normally on 2P+. +///////////////////////////////////////////////////////// +HookCallOfDead() +{ + level endon( "end_game" ); + + thread WatchT5MeteorCounterSong( "t5_cd_fragment" ); + thread HookCallOfDeadEnsemble(); +} + +HookCallOfDeadEnsemble() +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: t5_cd_ec (9 Ensemble Cast flags)\n" ); + + // Order matches community walkthrough. Each maps to a flag set in + // zombie_coast_eggs.gsc when that step's terminal action completes. + // None are marked canonical — the canonical map-level event is fired + // from WaitForEasterEggComplete on the existing `coast_easter_egg_achieved` + // notify, which immediately follows `dmf` flag-set in stock script. + thread WatchT5Flag( "ffd", "t5_cd_ec_1", false ); // step 2 — Fuse delivered + thread WatchT5Flag( "hgd", "t5_cd_ec_2", false ); // step 3 — Generators destroyed (4) + thread WatchT5Flag( "bd", "t5_cd_ec_3", false ); // step 4 — Vodka delivered (coop-only, never solo) + thread WatchT5Flag( "aca", "t5_cd_ec_4", false ); // step 5 — Morse code (auto-set in solo) + thread WatchT5Flag( "shs", "t5_cd_ec_5", false ); // step 6 — Ship's bridge (wheel + levers) + thread WatchT5Flag( "bp", "t5_cd_ec_6", false ); // step 7 — Foghorns (auto-set in solo) + thread WatchT5Flag( "ss", "t5_cd_ec_7", false ); // step 8 — Tower dials (auto-set in solo) + thread WatchT5Flag( "re", "t5_cd_ec_8", false ); // step 9 — Sacrifice / Vrill device retrieved + thread WatchT5Flag( "dmf", "t5_cd_ec_9", false ); // step 10 — Final knife / death-machine fire +} + +///////////////////////////////////////////////////////// +// Shared helpers used by multiple T5 maps. +///////////////////////////////////////////////////////// + +// Shared `level.meteor_counter` poll-to-3 song-EE watcher. Used by maps that +// follow the stock Treyarch pattern: 3 `mus_easteregg` STRUCTS, each spawning +// a runtime trigger_radius (so we can't waittill on the struct), with the +// counter incremented on each touch+use. Pattern: Coast (zombie_coast), +// Shangri-La (zombie_temple). Ascension uses level.teddybear_counter instead +// (different name) so it has its own watcher. +WatchT5MeteorCounterSong( stepKeyPrefix ) +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: " + stepKeyPrefix + " (polling level.meteor_counter)\n" ); + + // Same loop shape as HookAscensionTeddyBears / T4 Der Riese flytrap. + // 0.5s cadence covers human reaction time between fragment interactions. + prev = 0; + while ( !IsDefined( level.meteor_counter ) || level.meteor_counter < 3 ) + { + if ( IsDefined( level.meteor_counter ) && level.meteor_counter > prev ) + { + for ( i = prev + 1; i <= level.meteor_counter && i <= 3; i++ ) + { + stepKey = stepKeyPrefix + "_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } + prev = level.meteor_counter; + } + wait ( 0.5 ); + } + + // 2→3 tick exits early — flush remainder. + for ( i = prev + 1; i <= 3; i++ ) + { + stepKey = stepKeyPrefix + "_" + i; + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + } +} + +// Generic level-notify watcher. Used for stock notify-driven step events +// (e.g. _zombiemode_sidequests::stage_completed_internal which fires +// `__completed`). isCanonical mirrors WatchT5Flag's contract. +WatchT5LevelNotify( notifyName, stepKey, isCanonical ) +{ + level endon( "end_game" ); + + level waittill( notifyName ); + + logprint( "[ZM-EE] Step fired: " + stepKey + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); + + if ( !isCanonical ) + { + return; + } + + if ( IsDefined( level.iw4m_ee_fired ) && level.iw4m_ee_fired ) + { + logprint( "[ZM-EE] Suppressed re-emit on map=" + level.script + " (already fired)\n" ); + return; + } + level.iw4m_ee_fired = true; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] EE complete fired for map=" + level.script + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;complete;" + level.script + "\n" ); +} + +///////////////////////////////////////////////////////// +// Shangri-La (zombie_temple) — two parallel quests: +// +// Song EE — three Element 115 fragments, same poll-counter pattern as +// Coast (level.meteor_counter). Uses shared WatchT5MeteorCounterSong. +// +// Time Travel Will Tell — uses the stock _zombiemode_sidequests +// framework: 8 declared stages each emitting `sq__completed` +// level notify on completion, plus 2 inter-stage flags +// (gongs_resonating, meteorite_shrunk) that gate stage transitions. +// The terminal stage (BaG) completion triggers the existing +// `temple_sidequest_achieved` notify (already wired in +// WaitForEasterEggComplete), so none of our watchers mark canonical +// here — emitting from both paths would double-fire. +// +// Premium config sets MinPlayers=4 because step 2 (OaFC, floor tiles) has +// its solo auto-pass branch wrapped in /# #/ debug blocks (stripped in +// Pluto T5 production builds), and step 3 (DgCWf, water slide) requires +// `level._on_plate >= 3` simultaneously with someone going down — physically +// impossible with <4 players. Steps still fire regardless; UI hides quest +// for sub-4P matches. +///////////////////////////////////////////////////////// +HookShangriLa() +{ + level endon( "end_game" ); + + thread WatchT5MeteorCounterSong( "t5_sl_fragment" ); + thread HookShangriLaSidequest(); +} + +HookShangriLaSidequest() +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: t5_sl_sq (10 Time Travel Will Tell steps)\n" ); + + // Stages 2-8 use the sidequest framework's __completed + // notify. Stage names are the case-sensitive identifiers from + // declare_sidequest_stage() in zombie_temple_sq_*.gsc. + thread WatchT5LevelNotify( "sq_OaFC_completed", "t5_sl_sq_1", false ); // step 2 — Floor Tiles + thread WatchT5LevelNotify( "sq_DgCWf_completed", "t5_sl_sq_2", false ); // step 3 — Water Slide + thread WatchT5LevelNotify( "sq_LGS_completed", "t5_sl_sq_3", false ); // step 4 — Water Slide Crystal + thread WatchT5LevelNotify( "sq_PtT_completed", "t5_sl_sq_4", false ); // step 5 — Gas Pipes + thread WatchT5LevelNotify( "sq_StD_completed", "t5_sl_sq_5", false ); // step 6 — Spikemores + thread WatchT5LevelNotify( "sq_bttp_completed", "t5_sl_sq_6", false ); // step 7 — Wall Panels + Snare + thread WatchT5LevelNotify( "sq_bttp2_completed", "t5_sl_sq_7", false ); // step 8 — Mud Room Dials + + // Step 9 (gongs + dynamite catch) terminates with the gongs_resonating + // flag set, NOT a sidequest stage — gongs are handled by gong_watcher + // outside the framework. Step 10 (Fractilizer + crystal bounce) sets + // meteorite_shrunk flag. + thread WatchT5Flag( "gongs_resonating", "t5_sl_sq_8", false ); // step 9 — Gongs + dynamite catch + thread WatchT5Flag( "meteorite_shrunk", "t5_sl_sq_9", false ); // step 10 — Meteorite shrink (Fractilizer) + + // Step 11 — final altar / reward. BaG stage completion is what triggers + // the existing temple_sidequest_achieved notify, so canonical fire is + // handled by WaitForEasterEggComplete. + thread WatchT5LevelNotify( "sq_BaG_completed", "t5_sl_sq_10", false ); // step 11 — Final reward +} + +///////////////////////////////////////////////////////// +// Moon (zombie_moon) — two parallel quests: +// +// Song EE — three Element 115 fragments (community-known as bears for +// "Coming Home"). Same poll-counter pattern as Coast / Shangri-La. +// Reuses shared WatchT5MeteorCounterSong helper. +// +// Richtofen's Grand Scheme — uses the stock _zombiemode_sidequests +// framework: 8 stage-completion notifies across 3 sidequests: +// - `sq` main quest (ss1, osc, sc, sc2, ss2) +// - `be` sub-sidequest (Bouncing Egg — Vril Sphere & Big Bang) +// - `ctvg` sub-quest (Charge The Vril Generator — supercharged step) +// The `tanks` sub-sidequest (Charge The Tanks) is a script-level +// prereq for ctvg but isn't surfaced as a discrete community step. +// +// Canonical terminal is the existing `moon_sidequest_big_bang_achieved` +// notify in WaitForEasterEggComplete (fired from sq.gsc::do_launch +// after be_stage_two_completed). All step watchers here are +// isCanonical: false — emitting from both paths would double-fire. +// +// Premium config sets MinPlayers=4 because the script hard-walls steps +// 6+ (per community walkthrough: "Any step after this will require +// 4 players to complete"). Step events still fire regardless; UI hides +// quest for sub-4P matches. +///////////////////////////////////////////////////////// +HookMoon() +{ + level endon( "end_game" ); + + thread WatchT5MeteorCounterSong( "t5_mn_fragment" ); + thread HookMoonRichtofen(); +} + +HookMoonRichtofen() +{ + level endon( "end_game" ); + + logprint( "[ZM-EE] Step watcher armed: t5_mn_rgs (8 Richtofen's Grand Scheme stages)\n" ); + + // Stage notifies follow the framework convention `__completed`. + thread WatchT5LevelNotify( "sq_ss1_completed", "t5_mn_rgs_1", false ); // step 2 — Samantha Says (1st) + thread WatchT5LevelNotify( "sq_osc_completed", "t5_mn_rgs_2", false ); // step 3 — Lab Hacking (Open Source Code) + thread WatchT5LevelNotify( "be_stage_one_completed", "t5_mn_rgs_3", false ); // step 4 — Vril Sphere (Bouncing Egg 1) + thread WatchT5LevelNotify( "sq_sc_completed", "t5_mn_rgs_4", false ); // step 5 — Cryogenic Slumber Party (Soul Catch) + thread WatchT5LevelNotify( "ctvg_charge_completed", "t5_mn_rgs_5", false ); // step 6 — Supercharged Vril Device + thread WatchT5LevelNotify( "sq_sc2_completed", "t5_mn_rgs_6", false ); // step 7 — Richtofen's Betrayal (Soul Swap) + thread WatchT5LevelNotify( "sq_ss2_completed", "t5_mn_rgs_7", false ); // step 8 — Samantha Says (2nd) + thread WatchT5LevelNotify( "be_stage_two_completed", "t5_mn_rgs_8", false ); // step 9 — Big Bang Theory +} + +///////////////////////////////////////////////////////// +// PWR — Map power state monitoring +///////////////////////////////////////////////////////// +// +// All T5 zombie maps set the unified "power_on" flag from _zombiemode.gsc +// after their per-map switch handler runs. Watching the flag is sufficient +// to detect activation across the entire game. +// +// Player attribution is best-effort: a parallel thread watches common +// use-trigger entity names ("use_power_switch", etc.) and records the +// activator's identity on level._iw4m_power_activator with a timestamp. +// When the flag fires, we check if a recent (<5s) activation was recorded +// and emit player-attributed if so, world-attributed otherwise. +// +// T5 has no power_off mechanic in stock maps — flag is never cleared. +// The state-change loop still polls for flag clear in case custom maps +// implement it (and so the same code drops cleanly into T6 TranZit). +///////////////////////////////////////////////////////// + +WatchPowerSwitches() +{ + level endon( "end_game" ); + + // Wait briefly for map entities to be available. + wait ( 2 ); + + candidates = []; + candidates[0] = "use_power_switch"; + candidates[1] = "power_switch_trig"; + candidates[2] = "power_button"; + + triggers = []; + for ( c = 0; c < candidates.size; c++ ) + { + ents = getentarray( candidates[c], "targetname" ); + for ( i = 0; i < ents.size; i++ ) + { + triggers[triggers.size] = ents[i]; + } + } + + for ( t = 0; t < triggers.size; t++ ) + { + triggers[t] thread WatchSinglePowerSwitch(); + } +} + +WatchSinglePowerSwitch() +{ + level endon( "end_game" ); + self endon( "death" ); + + for ( ;; ) + { + self waittill( "trigger", who ); + if ( IsPlayer( who ) ) + { + level._iw4m_power_activator = who; + level._iw4m_power_activator_time = gettime(); + } + } +} + +WatchPowerStateChanges() +{ + level endon( "end_game" ); + + while ( true ) + { + flag_wait( "power_on" ); + EmitPowerOn(); + + // Spin while flag remains set. T5 stock maps never clear it + // (loop exits only on end_game endon). T6 TranZit clears it + // on bus power loss / pylon disconnect — same code reused there. + while ( flag( "power_on" ) ) + { + wait ( 0.5 ); + } + EmitPowerOff(); + } +} + +EmitPowerOn() +{ + activator = undefined; + if ( IsDefined( level._iw4m_power_activator ) && IsDefined( level._iw4m_power_activator_time ) ) + { + // Within last 5 seconds = caller of the switch is responsible + // for this flag fire. Beyond that, attribution is suspect (e.g., + // devgui-set, scripted activation) — fall through to world. + if ( gettime() - level._iw4m_power_activator_time < 5000 ) + { + activator = level._iw4m_power_activator; + } + } + + if ( IsDefined( activator ) ) + { + logPrint( "GSE;ZW;power;on;player;" + BuildPlayerInfoString( activator ) + "\n" ); + } + else + { + logPrint( "GSE;ZW;power;on;world\n" ); + } +} + +EmitPowerOff() +{ + // Power-off is rare and not player-attributed in stock maps (TranZit + // bus power loss is the only stock case). Always emit as world. + logPrint( "GSE;ZW;power;off;world\n" ); +} + +// Periodic emission of (round, zombies-remaining-to-spawn, currently-alive). Lets +// the live modal compute true "zombies cleared" = budget - remaining - alive, +// which captures trap kills / environmental kills / friendly grenade splash — +// all things that don't credit to a player's kill count but still reduce the +// round's spawn pool. Without this, live SPH would systematically over-estimate +// pace on trap-heavy strategies. +// +// Throttle: every 5s, only emits when remaining or alive changed since last +// emission. Matches the live-modal poll cadence — no point emitting faster +// than the UI refreshes. ~12 lines/min on an active round, none during +// intermission. +WatchZombiesRemaining() +{ + level endon( "end_game" ); + last_remaining = -1; + last_alive = -1; + while ( true ) + { + wait( 5 ); + if ( !IsDefined( level.zombie_total ) || !IsDefined( level.round_number ) ) + { + continue; + } + remaining = level.zombie_total; + alive = get_enemy_count(); + if ( remaining == last_remaining && alive == last_alive ) + { + continue; + } + logPrint( "GSE;ZW;zombies;" + level.round_number + ";" + remaining + ";" + alive + "\n" ); + last_remaining = remaining; + last_alive = alive; + } +} diff --git a/GameFiles/ZombieStats/_zm_stats_t6.gsc b/GameFiles/ZombieStats/_zm_stats_t6.gsc new file mode 100644 index 000000000..6cdbb4a57 --- /dev/null +++ b/GameFiles/ZombieStats/_zm_stats_t6.gsc @@ -0,0 +1,2314 @@ +#include maps\mp\_utility; +#include common_scripts\utility; +#include maps\mp\zombies\_zm_utility; + +// ───────────────────────────────────────────────────────────────── +// T6 Zombie Stats — Game Log Event Emitter +// ───────────────────────────────────────────────────────────────── +// +// This script is the T6 (Black Ops 2) port of _zm_stats_t4.gsc. +// While the T4 version hooks callbacks once during init and they +// remain stable for the lifetime of the match, T6 maps frequently +// override level.callback* variables AFTER init completes: +// +// - Transit (zm_transit): bussetup() replaces callbackactordamage +// with transit_actor_damage_override_wrapper for bus zombie +// death animations. Affects Transit and all offshoots (Town, +// Diner, Farm, Bus Depot). +// +// - Origins (zm_tomb): replaces callbackactordamage with +// tomb_actor_damage_override_wrapper for capture zone and +// tank zombie handling. +// +// - Die Rise (zm_highrise): achievement system replaces +// level.perk_bought_func with its own tracker, silently +// discarding any prior hook. +// +// Because of this, the T4 approach of "hook once, trust it stays" +// is unreliable on T6. This script uses two mitigations: +// +// 1. WATCHDOG THREAD: Periodically checks if our callback hooks +// have been overwritten. If so, captures the new map-specific +// function as the "original" to chain through, and re-installs +// our hook on top. This is agnostic — works with any map +// (including custom maps) that overrides callbacks post-init. +// +// 2. PLAYER NOTIFY LISTENERS: For perks, instead of hooking +// level.perk_bought_func (which maps can replace), we listen +// for the "perk_bought" notify that _zm_perks::give_perk() +// fires on the player entity. This fires for every perk +// acquisition on every map regardless of func overrides. +// +// ───────────────────────────────────────────────────────────────── + +init() +{ + // Seed the bootstrap dvar so IW4MAdmin can recover the current round + // when it starts (or reconnects to RCon) mid-match. Updated after every + // RC event in PrintPlayerRoundData. Defaults to round 1 here so a + // bootstrap during the very first round still resolves correctly. + setdvar( "sv_iw4m_zm_round", 1 ); + + // Stable per-match ID so IW4MAdmin can stitch a restarted/reconnected + // process back onto the existing EFZombieMatch row instead of creating + // a new orphaned match. Two randomints give ~10^12 collision space — + // overkill for the "at most a few open matches per server" lookup. + // gettime() returns 0 at init time (engine clock not yet running), so + // we don't use it here. The lookup index is (ServerId, GameMatchId) + // so cross-server collisions are harmless either way. + // Set once per init (= once per map load). + setdvar( "sv_iw4m_zm_matchid", "" + randomint( 1000000 ) + "_" + randomint( 1000000 ) ); + + thread WaitForRoundChange(); + thread WaitForPlayerConnect(); + thread WaitForPowerupSpawned(); + thread WaitForWeaponPurchases(); + thread WaitForPackAPunch(); + thread WaitForDoorPurchases(); + // T6 box detection — notify-driven on self.zbarrier (T6's equivalent + // of T5's chest_origin) with 3-tier user resolution + scoped teddy + // suppression. See header comment above WaitForMysteryBox. + thread WaitForMysteryBox(); + thread WaitForBoxTeddySuppression(); + thread WaitForTrapActivations(); + thread WaitForBuildables(); + thread WaitForCraftables(); + thread WaitForEasterEggComplete(); + thread WaitForT6EasterEggSteps(); + thread WatchPowerSwitches(); + thread WatchPowerStateChanges(); + thread WatchZombiesRemaining(); + + // --- Zombie Event Log Format --- // + // Combat events (legacy format): + // AK, AD, K, D = kills/damage (unchanged) + // RD, RC = round data/complete (unchanged) + // + // Unified ZE format: + // ZP;{player};down = player downed + // ZP;{player};revive;{reviver} = player revived + // ZP;{player};perk;buy;{perkName};{cost} = perk purchased + // ZP;{player};powerup;grab;{powerupName} = powerup grabbed + // ZP;{player};weapon;buy;{weaponName};{cost} = wall weapon purchase + // ZP;{player};weapon;upgrade;{old};{new};{cost} = pack-a-punch + // ZP;{player};box;take;{weaponName};{cost} = box weapon taken + // ZP;{player};box;pass;{weaponName};{cost} = box weapon passed + // ZP;{player};box;teddy;{cost} = teddy bear (box moves) + // ZP;{player};door;buy;{cost} = door/debris opened + // ZP;{player};trap;activate;{trapType};{cost} = trap activated + // ZP;{player};build;complete;{buildableName} = buildable completed + + SetupCallbacks(); +} + +SetupCallbacks() +{ + waittillframeend; + + // zombie damage events + level.callbackActorDamageOriginal = level.callbackactordamage; + level.callbackActorKilledOriginal = level.callbackactorkilled; + level.callbackactordamage = ::OnActorDamage; + level.callbackactorkilled = ::OnActorKilled; + + // player damage events + level.callbackPlayerDamageOriginal = level.callbackplayerdamage; + level.callbackplayerdamage = ::OnPlayerDamaged; + + // down/revive events + level.callbackPlayerLastStandOriginal = level.callbackplayerlaststand; + level.callbackplayerlaststand = ::OnPlayerDowned; + + // Some maps override level.callback* variables after init + // (e.g. Transit's bussetup, Origins' tomb wrapper). + // This watchdog detects when our hooks have been replaced and re-applies them, + // capturing the new map-specific functions as the originals to chain through. + thread WatchdogCallbacks(); +} + +//-----------------// +//---- Waiters ----// +//-----------------// + +///////////////////////////////////////////////////////// +// Waits until a player connects and spawns the +// monitoring threads +///////////////////////////////////////////////////////// +WaitForPlayerConnect() +{ + for ( ;; ) + { + level waittill( "connecting", player ); + + // T6 passes the reviver as a parameter to the player_revived notify + // so we no longer need weapon switch monitoring to identify the reviver + player thread WaitForPlayerRevive(); + + // zm mode does not actually kill a player after down timer expires + // they get put into spectator without a kill callback + player thread WaitForPlayerZombified(); + + // _zm_perks::give_perk() fires "perk_bought" on the player for every + // perk acquisition on every map. This is more reliable than + // level.perk_bought_func which can be overridden by map-specific + // scripts (e.g. Die Rise's achievement system) + player thread WaitForPerkBought(); + + // Tranzit / Die Rise / Buried — bank deposit/withdraw and weapon + // locker store/retrieve. Polling-based: T6's _zm_banking.gsc only + // emits "bank_withdrawal" on level (deposits silent) and the locker + // only emits "weapon_locker_grab" (store silent), so we sample the + // player's account_value + stored_weapon_data each tick instead of + // chasing two half-instrumented signals. + player thread WatchBankAccountValue(); + player thread WatchWeaponLockerSlot(); + + // PaP moved to trigger-based polling in init() — see WatchPackAPunch + + ///# + // todo: remove — debug helpers + // Uncomment lines as needed for testing. + // + // Give max points (must wait for spawn or game overwrites with default): + //player thread DebugGiveScore(); + // + // Disable out-of-bounds kill monitor. The monitor thread uses + // self endon("stop_player_out_of_playable_area_monitor"), so + // notifying it kills the thread. Also set the flag to 0 to + // prevent it restarting on respawn. + //level.player_out_of_playable_area_monitor = 0; + //player notify( "stop_player_out_of_playable_area_monitor" ); + //#/ + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player revive timer expires +// Prints a "Kill" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerZombified() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // zombified notify occurs when a player is moved to spectator + // after downed timer expires + self waittill( "zombified" ); + playerInfo = BuildPlayerInfoString( self ); + + logprint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } +} + +///////////////////////////////////////////////////////// +// Waits until a downed player is revived +// Prints a "Player Revived" event to the game log +///////////////////////////////////////////////////////// +WaitForPlayerRevive() +{ + self endon ( "disconnect" ); + + for ( ;; ) + { + // T6 always passes the reviver as a parameter to the notify + self waittill( "player_revived", reviver ); + + // Self-revive (solo Quick Revive auto / Who's Who): reviver==self. Emit + // distinct subtype so downstream classification doesn't need guid compare. + if ( IsDefined( reviver ) && IsPlayer( reviver ) && reviver == self ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;self\n" ); + } + else + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;" + BuildPlayerInfoString( reviver ) + "\n" ); + } + } +} + +///////////////////////////////////////////////////////// +// Waits for the "perk_bought" notify fired by +// _zm_perks::give_perk() whenever a perk is acquired. +// This works on all maps regardless of perk_bought_func +///////////////////////////////////////////////////////// +WaitForPerkBought() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + self waittill( "perk_bought", perk ); + + // Perk cost is not available from the perk_bought notify. + // The game's cost lookup is hardcoded in _zm_perks per-perk switch + // statements — not exposed to external scripts. + // Free perks (from free_perk powerup) don't fire perk_bought at all. + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";perk;buy;" + perk + ";0\n" ); + } +} + +///////////////////////////////////////////////////////// +// Waits for "pap_taken" player notify fired by +// _zm_perks::vending_weapon_upgrade() when a player +// grabs their upgraded weapon from the PaP machine +///////////////////////////////////////////////////////// +// T6 Pack-a-Punch — POLLING APPROACH +// +// pap_taken fires on the player, but by the time our thread resumes, +// getCurrentWeapon() returns "none" (weapon not given yet). +// Same approach as T4/T5: poll the PaP trigger's .current_weapon +// property which is set when the player places their weapon. +///////////////////////////////////////////////////////// +// T6 Pack-a-Punch — DEBUG INSTRUMENTED BUILD +// +// Mirrors the T4 redesign (notify-driven, lock-first buyer attribution, +// distinct outcomes via pap_taken/pap_timeout). T6 adds a third notify: +// pap_player_disconnected (engine fires when buyer disconnects mid-iter). +// We track it via WatchPapDisconnectFlag and skip emission on disconnect +// (no player to credit). +// +// T6 engine reference (`_zm_perks.gsc::vending_weapon_upgrade`): +// - Trigger discovery: targetname "zombie_vending" + script_noteworthy +// "specialty_weapupgrade", OR legacy targetname "zombie_vending_upgrade" +// - Field: self.current_weapon (~L630 set, ~L645 clear) +// - Take notify: self notify("pap_taken") ~L742 +// - Timeout notify: self notify("pap_timeout") ~L793 +// - Disconnect notify: self notify("pap_player_disconnected") ~L816 +// - Cost: 5000 base +// - Timeout: level.packapunch_timeout = 15s +///////////////////////////////////////////////////////// +WaitForPackAPunch() +{ + wait ( 2 ); + + // T6 PaP triggers come from two sources: + // 1. targetname "zombie_vending" with script_noteworthy "specialty_weapupgrade" + // 2. targetname "zombie_vending_upgrade" (legacy fallback) + // Both get threaded with vending_weapon_upgrade() by the game. + papTriggers = []; + + vendingTriggers = getEntArray( "zombie_vending", "targetname" ); + if ( IsDefined( vendingTriggers ) ) + { + for ( i = 0; i < vendingTriggers.size; i++ ) + { + if ( IsDefined( vendingTriggers[i].script_noteworthy ) ) + { + if ( vendingTriggers[i].script_noteworthy == "specialty_weapupgrade" ) + { + papTriggers[papTriggers.size] = vendingTriggers[i]; + } + } + } + } + + oldPacks = getEntArray( "zombie_vending_upgrade", "targetname" ); + if ( IsDefined( oldPacks ) ) + { + for ( i = 0; i < oldPacks.size; i++ ) + { + papTriggers[papTriggers.size] = oldPacks[i]; + } + } + + if ( papTriggers.size == 0 ) + { + return; + } + + for ( i = 0; i < papTriggers.size; i++ ) + { + papTriggers[i].iw4m_pap_buyer = undefined; + papTriggers[i].iw4m_pap_buyer_weapon = undefined; + papTriggers[i].iw4m_pap_taken_flag = false; + papTriggers[i].iw4m_pap_timeout_flag = false; + papTriggers[i].iw4m_pap_disconnect_flag = false; + + papTriggers[i] thread WatchPapOutcome(); + papTriggers[i] thread WatchPapTriggerForBuyer(); + papTriggers[i] thread WatchPapTakenFlag(); + papTriggers[i] thread WatchPapTimeoutFlag(); + papTriggers[i] thread WatchPapDisconnectFlag(); + } +} + +// First-notify-wins. Engine has stale per-iter threads (wait_for_player_to_take, +// wait_for_timeout) that can outlive their iter and fire pap_taken/pap_timeout +// AFTER another notify has already resolved the iter. Ignoring later notifies +// prevents misclassification (e.g., abandon emitted as upgrade when a stale +// take notify fires after timeout cleared the iter). +WatchPapTakenFlag() +{ + for ( ;; ) + { + self waittill( "pap_taken" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_taken_flag = true; + } +} + +WatchPapTimeoutFlag() +{ + for ( ;; ) + { + self waittill( "pap_timeout" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_timeout_flag = true; + } +} + +WatchPapDisconnectFlag() +{ + for ( ;; ) + { + self waittill( "pap_player_disconnected" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) + { + continue; + } + self.iw4m_pap_disconnect_flag = true; + } +} + +WatchPapTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + // Phase2 path: engine already accepted a buy (current_weapon set). + // The buyer is the player whose weapon engine just took — their + // GetCurrentWeapon() is now empty/none. Late F-pressers in phase2 + // still hold their own weapon, so this discriminates cleanly. + // Lock immediately with engine's current_weapon as authority; skip + // verify (engine already accepted). Without this path, scheduler + // ordering that runs the engine handler before ours causes legit + // buyers to be rejected as phase2 late-pressers, dropping the emit. + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + buyerWeapon = who GetCurrentWeapon(); + if ( IsDefined( buyerWeapon ) && buyerWeapon != "" && buyerWeapon != "none" ) + { + continue; + } + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = self.current_weapon; + continue; + } + + // Phase1 path: engine hasn't accepted yet. Replicate engine gates + // (score, upgradeable weapon) so we don't lock on rejected presses. + if ( !IsDefined( who.score ) || who.score < 5000 ) + { + continue; + } + + // T6 weapons have _zm suffix; upgrade variant is at + // level.zombie_weapons[weapon].upgrade_name (NOT weapon + "_upgraded" + // — that produces "weapon_zm_upgraded" vs real "weapon_upgraded_zm"). + weapon = who GetCurrentWeapon(); + if ( weapon == "" || weapon == "none" ) + { + continue; + } + if ( !IsDefined( level.zombie_weapons ) || !IsDefined( level.zombie_weapons[weapon] ) ) + { + continue; + } + if ( !IsDefined( level.zombie_weapons[weapon].upgrade_name ) ) + { + continue; + } + + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = weapon; + + // Verify engine actually accepted; unlock otherwise. Handles engine- + // side gates we don't replicate (laststand, throwing grenade, + // switching weapons). + self thread VerifyPapBuyerLock(); + } +} + +VerifyPapBuyerLock() +{ + // Poll for engine acceptance up to 2s. Single 0.25s wait was too short on + // T6 (engine sometimes delays setting self.current_weapon past 0.25s, + // unlocking legit buyer → later stale F-press re-locks wrong weapon → + // false mismatch → skipped emit). Polling fix is identical across + // T4/T5/T6 even though only T6 was observed failing — defensive + // consistency. + timeoutMs = 2000; + pollMs = 50; + elapsedMs = 0; + while ( elapsedMs < timeoutMs ) + { + if ( IsDefined( self.current_weapon ) && self.current_weapon != "" ) + { + return; + } + wait ( 0.05 ); + elapsedMs = elapsedMs + pollMs; + } + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + } +} + +WatchPapOutcome() +{ + for ( ;; ) + { + // Reset per-iter state + self.iw4m_pap_taken_flag = false; + self.iw4m_pap_timeout_flag = false; + self.iw4m_pap_disconnect_flag = false; + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + + // Phase 1: wait for engine to accept a buy (current_weapon set). + while ( !IsDefined( self.current_weapon ) || self.current_weapon == "" ) + { + wait ( 0.05 ); + } + + oldWeapon = self.current_weapon; + + // Phase 2: wait for iter boundary. Boundary = current_weapon changes + // (clears OR engine immediately starts a new iter with a different + // weapon in the same frame our poll would otherwise miss). Detecting + // weapon-change as an exit prevents losing back-to-back iters when + // engine clears + re-sets within one 50ms poll window. + while ( IsDefined( self.current_weapon ) && self.current_weapon == oldWeapon ) + { + wait ( 0.05 ); + } + + // Disconnect short-circuits emission (no player to credit). + if ( self.iw4m_pap_disconnect_flag ) + { + continue; + } + + // Emit policy: engine_weapon (oldWeapon) is authoritative for what got + // upgraded/abandoned. Locked buyer is best-effort attribution. Lock-vs- + // engine weapon mismatch happens when player switches weapons between + // F-presses or when scheduler ordering causes our lock to fire after + // engine commits — engine's choice wins. Skip emission only when no + // buyer was ever locked. + if ( !IsDefined( self.iw4m_pap_buyer ) || !IsPlayer( self.iw4m_pap_buyer ) ) + { + continue; + } + + // Resolve actual cost engine charged. Engine sets self.cost (5000 + // base, 1000 during bonfire sale) and self.attachment_cost (2000 + // base, 1000 sale) on the trigger via vending_weapon_upgrade_cost. + // Attachment-only upgrade fires on already-upgraded weapons on + // re-PaP-enabled maps (BO2 attachment-perk maps); engine charges + // attachment_cost in that case. Substring check on _upgraded is a + // proxy for engine's will_upgrade_weapon_as_attachment gate (engine + // also requires zombiemode_reusing_pack_a_punch + supports_attachments + // — but those gates already passed if we're at phase1_enter with an + // upgraded weapon). Misses pers_upgrade double_points modifier + // (per-player premium feature, rare). + cost = 5000; + if ( IsDefined( self.cost ) ) { cost = self.cost; } + if ( IsDefined( self.attachment_cost ) && IsSubStr( oldWeapon, "_upgraded" ) ) { cost = self.attachment_cost; } + + if ( self.iw4m_pap_taken_flag ) + { + newWeapon = oldWeapon + "_upgraded"; + if ( IsDefined( level.zombie_weapons ) && IsDefined( level.zombie_weapons[oldWeapon] ) && IsDefined( level.zombie_weapons[oldWeapon].upgrade_name ) ) + { + newWeapon = level.zombie_weapons[oldWeapon].upgrade_name; + } + logprint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;upgrade;" + oldWeapon + ";" + newWeapon + ";" + cost + "\n" ); + } + else if ( self.iw4m_pap_timeout_flag ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;abandon;" + oldWeapon + ";" + cost + "\n" ); + } + } +} + +///////////////////////////////////////////////////////// +// Monitors all four level.callback* hooks to detect when +// a map script overwrites them post-init. Re-captures the +// new map function as the original (to chain through) and +// re-installs our hook on top. Covers callbackactordamage, +// callbackactorkilled, callbackplayerdamage, and +// callbackplayerlaststand. +///////////////////////////////////////////////////////// +WatchdogCallbacks() +{ + // give map scripts time to finish their init + // most overrides happen during map setup within the first second + wait ( 1 ); + + for ( ;; ) + { + if ( level.callbackactordamage != ::OnActorDamage ) + { + level.callbackActorDamageOriginal = level.callbackactordamage; + level.callbackactordamage = ::OnActorDamage; + } + + if ( level.callbackactorkilled != ::OnActorKilled ) + { + level.callbackActorKilledOriginal = level.callbackactorkilled; + level.callbackactorkilled = ::OnActorKilled; + } + + if ( level.callbackplayerdamage != ::OnPlayerDamaged ) + { + level.callbackPlayerDamageOriginal = level.callbackplayerdamage; + level.callbackplayerdamage = ::OnPlayerDamaged; + } + + if ( level.callbackplayerlaststand != ::OnPlayerDowned ) + { + level.callbackPlayerLastStandOriginal = level.callbackplayerlaststand; + level.callbackplayerlaststand = ::OnPlayerDowned; + } + + // check periodically; once stable this is essentially free + wait ( 5 ); + } +} + +///////////////////////////////////////////////////////// +// Periodically checks active script_models to see if any +// have a defined powerup_name. If so monitors for pickup +///////////////////////////////////////////////////////// +WaitForPowerupSpawned() +{ + powerupEntCount = 0; + + for ( ;; ) + { + // the powerup ent is not named and there are + // no events to tell us when one is spawned + // so we need to periodically check for changes + // and wait for a player to get in range + // additionally, overriding the level.zombie_powerup_grab_func + // prevents original powerup code from running + models = GetEntArray( "script_model", "classname" ); + powerupEnts = []; + + for ( i = 0; i < models.size; i++ ) + { + if( IsDefined( models[i].powerup_name ) && !IsDefined( models[i].isWaiting ) ) + { + powerupEnts[powerupEnts.size] = models[i]; + } + } + + if ( powerupEnts.size != 0 && powerupEnts.size != powerupEntCount ) + { + // we only want to start a new thread if the size increases + // if it's decreased that means a powerup despawned + if ( powerupEnts.size >= powerupEntCount ) + { + array_thread( powerupEnts, ::WaitForPowerupGrab ); + } + } + + powerupEntCount = powerupEnts.size; + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until a player gets within proximity of a powerup +// or the powerup despawns. Write powerup to game log +///////////////////////////////////////////////////////// +WaitForPowerupGrab() +{ + self.isWaiting = true; + + self endon( "powerup_timedout" ); + self endon( "powerup_grabbed" ); + + while ( IsDefined( self ) ) + { + players = get_players(); + + for ( i = 0; i < players.size; i++ ) + { + // this is not ideal, but this is the only way + // to properly replicate how the powerup grab + // is determined in the original code + if ( Distance( players[i].origin, self.origin ) < 64 ) + { + powerup = "unknown"; + + if ( IsDefined( self.powerup_name ) ) + { + powerup = self.powerup_name; + } + + self.isWaiting = false; + + logprint( "GSE;ZP;" + BuildPlayerInfoString( players[i] ) + ";powerup;grab;" + powerup + "\n" ); + + return; + } + } + + wait ( 0.05 ); + } +} + +///////////////////////////////////////////////////////// +// Waits until the game is over or new round is initalized +// Writes round data to game log +///////////////////////////////////////////////////////// +WaitForRoundChange() +{ + for ( ;; ) + { + // intermission occurs when "game over" screen appears + // between_round_over occurs when the next round setup has completed + result = level waittill_any_return( "intermission", "between_round_over" ); + + /# + println( "WaitForRoundStart TRIGGERED" ); + #/ + + players = get_players(); + + for ( i = 0; i < players.size; i++ ) + { + // they were downed and not revived, so we already printed the event + if ( ( IsDefined( players[i].is_zombie ) && players[i].is_zombie ) ) + { + continue; + } + + // if there are no zombies alive, then the game is not over + if ( get_current_zombie_count() == 0 ) + { + continue; + } + + // game is over so we print out their death + playerInfo = BuildPlayerInfoString( players[i] ); + logprint( "GSE;K;" + playerInfo + ";-1;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n"); + } + + // IW4MAdmin reads the game log and processes events concurrently. + // When K (death) and RD (round data) events are emitted in the same + // server frame, they arrive simultaneously and IW4MAdmin may process + // the RD event's stat rollup before the K event's death increment, + // causing Deaths to be missing from match/aggregate totals. + // This wait ensures the K events are written to the log and processed + // before RD/RC events arrive. + wait ( 0.1 ); + + isGameOver = IsDefined( result ) && result == "intermission"; + PrintPlayerRoundData( isGameOver ); + + if ( isGameOver ) + { + break; + } + + // Detect special-round type for the round about to begin and emit a + // GSE;ZW;round_special;; line so IW4MAdmin can flag the round in the + // breakdown UI and skip Seconds-Per-Horde for round types where the + // static budget formula doesn't apply (dogs/leapers replace the regular + // zombie spawn budget; SPH would render visibly wrong otherwise). + // T6 maps where these flags exist: dog_round (universal), leaper_round + // (Die Rise only). flag_exists guards keep us safe on maps that never + // initialised the flag. + EmitSpecialRoundIfAny(); + } +} + +EmitSpecialRoundIfAny() +{ + // Use IsDefined-based flag checks for cross-game consistency with T4 (which + // doesn't expose flag_exists()) and to avoid asserting on maps that never + // initialised the flag (e.g. leaper_round only exists on Die Rise). + specialType = ""; + if ( IsDefined( level.flag ) && IsDefined( level.flag[ "dog_round" ] ) && level.flag[ "dog_round" ] ) + { + specialType = "dog"; + } + else if ( IsDefined( level.flag ) && IsDefined( level.flag[ "leaper_round" ] ) && level.flag[ "leaper_round" ] ) + { + specialType = "leaper"; + } + + if ( specialType != "" ) + { + logprint( "GSE;ZW;round_special;" + level.round_number + ";" + specialType + "\n" ); + } +} + +//-------------------// +//---- Callbacks ----// +//-------------------// + +// T6 actor damage signature: (inflictor, attacker, damage, flags, meansofdeath, weapon, vpoint, vdir, shitloc, psoffsettime, boneindex) +// T4 used (eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, iModelIndex, iTimeOffset) +OnActorDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // we only want to log damage if they aren't going to die + // T6/Plutonium reduces self.health before the callback fires, + // so we check if the zombie is still alive after the hit + if ( IsDefined( self.health ) && self.health > 0 ) + { + // Cap reported damage at the victim's max HP — the engine can pass + // iDamage values far in excess of what the zombie could actually absorb + // (seen in Die Rise at round 30: MOD_PROJECTILE_SPLASH reporting ~5.5M/hit). + reportedDamage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && reportedDamage > self.maxhealth ) + { + reportedDamage = self.maxhealth; + } + + logprint( "GSE;AD;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + reportedDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + } + + [[ level.callbackActorDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex ); +} + +// T6 actor killed signature: (einflictor, attacker, idamage, smeansofdeath, sweapon, vdir, shitloc, psoffsettime) +OnActorKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + // Cap kill damage at the victim's max HP so the final blow doesn't + // include overkill / engine-inflated iDamage. + damage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && damage > self.maxhealth ) + { + damage = self.maxhealth; + } + + logprint( "GSE;AK;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + damage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[ level.callbackActorKilledOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime ); +} + +// T6 player damage signature: (einflictor, eattacker, idamage, idflags, smeansofdeath, sweapon, vpoint, vdir, shitloc, psoffsettime, boneindex) +// T4 had (iModelIndex, iTimeOffset) as last two params; T6 uses (psoffsettime, boneindex) +OnPlayerDamaged( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + + if ( IsPlayer( eInflictor ) ) + { + attackerInfo = BuildPlayerInfoString( eInflictor ); + } + + logprint( "GSE;D;" + victimInfo + ";" + attackerInfo + ";" + sWeapon + ";" + iDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + [[ level.callbackPlayerDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex ); +} + +// T6 laststand signature: (einflictor, eattacker, idamage, smeansofdeath, sweapon, vdir, shitloc, psoffsettime, deathanimduration) +OnPlayerDowned( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ) +{ + // sometimes this callback can be executed multiple times while the player is still downed + // this struct is set to undefined when they die or get revived + if ( IsDefined( self.revivetrigger ) ) + { + return; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";down\n" ); + + [[ level.callbackPlayerLastStandOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ); +} + +//-----------------// +//---- Helpers ----// +//-----------------// + +PrintPlayerRoundData( isGameOver ) +{ + // Skip emission entirely if level.round_number is undefined — fires during + // post-game-over shutdown / exit_level cleanup with no round context. Without + // this guard, currentRound defaults to 1 and emits a spurious GSE;RC;1 between + // the legit final RC and ExitLevel (see match 1614). + if ( !IsDefined( level.round_number ) ) + { + return; + } + + players = get_players(); + currentRound = level.round_number; + + for( i = 0; i < players.size; i++ ) + { + // Skip players who never spawned (e.g. joined mid-game into spectator) + // to avoid crediting them with starting points they never earned + if ( IsDefined( players[i].sessionstate ) && players[i].sessionstate == "spectator" ) + { + continue; + } + + totalScore = 0; + currentScore = 0; + + if ( IsDefined ( players[i].score_total ) ) + { + totalScore = players[i].score_total; + } + + if ( IsDefined ( players[i].score ) ) + { + currentScore = players[i].score; + } + + logprint( "GSE;RD;" + BuildPlayerInfoString( players[i] ) + ";" + totalScore + ";" + currentScore + ";" + currentRound + ";" + isGameOver + "\n" ); + } + + // Ensure all RD events are processed before RC triggers StartNextRound + // which clears round states. Without this wait, RC can race ahead of + // late-arriving RD events due to IW4MAdmin's concurrent event processing. + wait ( 0.1 ); + + setdvar( "sv_iw4m_zm_round", currentRound ); + logprint( "GSE;RC;" + currentRound + "\n" ); +} + +///////////////////////////////////////////////////////// +// Economy event hooks — wall buys, box, PaP, doors, +// traps, and buildables. +// T6 fires level-scoped notifies for most of these. +///////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////// +// Listens for "weapon_bought" level notify fired by +// _zm_weapons::vending_weapon_upgrade() on wall buys +///////////////////////////////////////////////////////// +WaitForWeaponPurchases() +{ + for ( ;; ) + { + level waittill( "weapon_bought", player, weaponName ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + // Cost stored in level.zombie_weapons table + cost = 0; + if ( IsDefined( level.zombie_weapons ) && IsDefined( level.zombie_weapons[weaponName] ) && IsDefined( level.zombie_weapons[weaponName].cost ) ) + { + cost = level.zombie_weapons[weaponName].cost; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";weapon;buy;" + weaponName + ";" + cost + "\n" ); + } +} + +///////////////////////////////////////////////////////// +// Listens for "door_opened" level notify fired by +// _zm_blockers when any door/debris is purchased +///////////////////////////////////////////////////////// +WaitForDoorPurchases() +{ + // Wait for doors to be initialized + wait ( 2 ); + + doors = getEntArray( "zombie_door", "targetname" ); + debris = getEntArray( "zombie_debris", "targetname" ); + + for ( i = 0; i < doors.size; i++ ) + { + doors[i] thread WatchDoorPurchase(); + } + + for ( i = 0; i < debris.size; i++ ) + { + debris[i] thread WatchDoorPurchase(); + } +} + +// Door triggers fire on ANY interaction, even if the player can't afford it. +// Check score before logging to avoid false positives. +WatchDoorPurchase() +{ + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + return; + } + + if ( !IsDefined( player.score ) || player.score < cost ) + { + return; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";door;buy;" + cost + "\n" ); +} + +///////////////////////////////////////////////////////// +// T6 Mystery Box Detection (vanilla T6 + Tranzit/Buried/Origins/etc). +// +// Notify-driven on self.zbarrier — each engine box iteration emits +// exactly one randomization_done + one box_spin_done, giving 1:1 +// mapping between our handler iters and engine pulls. +// +// Engine reference: `_zm_magicbox.gsc` from t6-scripts-main ZM/Core. +// Key entities/state: +// - level.chests[]: array of all magic box trigger entities. (No +// "treasure_chest_use" targetname lookup like T4/T5 — T6 publishes +// the array directly.) +// - self.zbarrier: per-chest, holds .weapon_string (cycles during +// randomization, final value persists until cleared at L2227) and +// emits randomization_done (L1204) / box_spin_done (L1291) / +// weapon_grabbed notifies. +// - self.chest_user: assigned BEFORE randomization (L454/461/468), +// cleared at L628. Reliably observable at randomization_done resume +// on the normal grab path. +// - self.timedout (lowercase, matches engine L510/583). GSC field +// access is case-insensitive but match the engine for clarity. +// - self.grab_weapon_name: set at L525 from self.zbarrier.weapon_string +// after randomization_done. Persists across iters until next pull. +// - level "weapon_fly_away_start" notify (L1213): teddy path, ~0.5s +// after randomization_done. +// +// 3-tier user resolution (same shape as T4/T5): +// 1. capturedUser snapshotted at randomization_done resume +// 2. live self.chest_user at box_spin_done resume +// 3. self.iw4m_box_last_trigger (parallel waittill ground truth) +// — gated on IsDefined(self.timedout) +// Tier 3 is required for teddy attribution: treasure_chest_move +// (self.chest_user) is threaded at L521, then engine drops to L628 +// (chest_user = undefined) without yielding — both tier 1 and tier 2 +// miss. The trigger waittill captured the buyer earlier. +// +// Per-chest state: +// - self.iw4m_box_teddy_marker — set by suppression, consumed +// at box_spin_done +// - self.iw4m_box_in_late_phase — true between rand_done and +// iter end; suppression only marks chests with this flag +// - self.iw4m_box_last_trigger — parallel trigger capture, +// cleared at iter end +// +// Note on Origins (zm_tomb): the custom `_zm_magicbox_tomb.gsc` only +// overrides visual/zbarrier-state machinery. Once a zone is captured, +// chests in that zone publish the same notifies as vanilla — no +// Origins-specific code needed here. +// +// Note on Buried: candy-lady mechanic dynamically prunes level.chests +// after init. Suppression iterating live level.chests handles this +// correctly — only chests in active rotation get marked. +///////////////////////////////////////////////////////// +WaitForMysteryBox() +{ + wait ( 5 ); + + if ( !IsDefined( level.chests ) ) + { + return; + } + + for ( i = 0; i < level.chests.size; i++ ) + { + level.chests[i].iw4m_box_teddy_marker = false; + level.chests[i].iw4m_box_in_late_phase = false; + + level.chests[i] thread WatchBoxOutcome(); + level.chests[i] thread WatchBoxTriggerForBuyer(); + } +} + +///////////////////////////////////////////////////////// +// Teddy bear suppression — same scoping rule as T4/T5: only mark +// chests flagged as iw4m_box_in_late_phase, otherwise level-scoped +// marking bleeds across iterations. +///////////////////////////////////////////////////////// +WaitForBoxTeddySuppression() +{ + wait ( 5 ); + for ( ;; ) + { + level waittill( "weapon_fly_away_start" ); + + if ( !IsDefined( level.chests ) ) + { + continue; + } + + for ( k = 0; k < level.chests.size; k++ ) + { + if ( IsDefined( level.chests[k].iw4m_box_in_late_phase ) ) + { + if ( level.chests[k].iw4m_box_in_late_phase ) + { + level.chests[k].iw4m_box_teddy_marker = true; + } + } + } + } +} + +///////////////////////////////////////////////////////// +// Parallel ground-truth capture of who pressed USE on the chest. +// Used as tier-3 fallback for teddy attribution (chest_user is cleared +// in same frame as L521/L628 on teddy path) and instant-grab cases. +// +// Lock-on-first-valid-press: record only the FIRST trigger of an +// iteration that comes from a player who could afford the buy, then +// ignore every subsequent press until iter end clears the lock. This +// is the buyer because the engine enforces buyer-only-can-grab +// (treasure_chest_think rejects "trigger" from non-buyers). Without +// the lock, in multi-player F-spam, another player's noise press +// would overwrite the real buyer. +// +// Affordability gate: replicates the engine's own score check so a +// player pressing F with insufficient funds doesn't lock attribution. +///////////////////////////////////////////////////////// +WatchBoxTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_box_last_trigger ) ) + { + continue; + } + if ( !IsDefined( who ) || !IsPlayer( who ) ) + { + continue; + } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) + { + cost = level.zombie_treasure_chest_cost; + } + else if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + if ( !IsDefined( who.score ) || who.score < cost ) + { + continue; + } + + self.iw4m_box_last_trigger = who; + } +} + +WatchBoxOutcome() +{ + if ( !IsDefined( self.zbarrier ) ) + { + return; + } + + for ( ;; ) + { + // Iter starts when engine signals randomization done. + self.zbarrier waittill( "randomization_done" ); + + // Mark for teddy suppression scoping. Cleared at iter end. + self.iw4m_box_in_late_phase = true; + + // Snapshot weapon + buyer immediately. weapon_string is + // undefined on the teddy path. chest_user may also be + // undefined on teddy because the engine doesn't yield + // between L1204 and L628 — that's why tier-3 trigger + // fallback is required for teddy attribution. + weaponName = "undef"; + if ( IsDefined( self.zbarrier.weapon_string ) ) + { + weaponName = self.zbarrier.weapon_string; + } + + capturedUser = undefined; + if ( IsDefined( self.chest_user ) ) + { + if ( IsPlayer( self.chest_user ) ) + { + capturedUser = self.chest_user; + } + } + + // Wait for outcome. + self.zbarrier waittill( "box_spin_done" ); + + // Safe to read final state — engine sleeps post-grab before + // resetting timedout for the next iter. + timedOutDefined = 0; + timedOutValue = false; + if ( IsDefined( self.timedout ) ) + { + timedOutDefined = 1; + if ( self.timedout ) + { + timedOutValue = true; + } + } + + isTeddy = false; + if ( IsDefined( self.iw4m_box_teddy_marker ) ) + { + if ( self.iw4m_box_teddy_marker ) + { + isTeddy = true; + } + } + self.iw4m_box_teddy_marker = false; + + // 3-tier user resolution. + user = capturedUser; + if ( !IsDefined( user ) ) + { + if ( IsDefined( self.chest_user ) ) + { + if ( IsPlayer( self.chest_user ) ) + { + user = self.chest_user; + } + } + } + if ( !IsDefined( user ) ) + { + if ( timedOutDefined == 1 ) + { + if ( IsDefined( self.iw4m_box_last_trigger ) ) + { + if ( IsPlayer( self.iw4m_box_last_trigger ) ) + { + user = self.iw4m_box_last_trigger; + } + } + } + } + + cost = 950; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + if ( IsDefined( user ) ) + { + if ( isTeddy ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;teddy;" + cost + "\n" ); + } + else if ( timedOutValue ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;pass;" + weaponName + ";" + cost + "\n" ); + } + else + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;take;" + weaponName + ";" + cost + "\n" ); + } + } + + // Per-iter cleanup. + self.iw4m_box_last_trigger = undefined; + self.iw4m_box_in_late_phase = false; + } +} + +///////////////////////////////////////////////////////// +// T6 Trap Detection — POLLING _trap_in_use +// +// Same approach as T5. T6's _zm_traps only fires "trap_activate" +// notify from trap_activate_electric(). Fire, rotating, and flipper +// traps don't fire it. _trap_in_use is set to 1 on purchase for +// ALL trap types by the shared trap_think() function. +// +// Mob of the Dead's custom traps (fan/acid/tower) use their own +// system entirely — no zombie_trap targetname, no _trap_in_use. +// The IsSubStr("trap") scan catches their trigger entities but +// _trap_in_use won't be set. Those traps are not supported. +///////////////////////////////////////////////////////// +WaitForTrapActivations() +{ + // Wait for traps to be initialized + wait ( 2 ); + + // Standard T6 traps use "zombie_trap" targetname + traps = getEntArray( "zombie_trap", "targetname" ); + + for ( i = 0; i < traps.size; i++ ) + { + traps[i] thread WatchTrapActivation(); + } + + // Also scan for non-standard trap triggers (e.g. Mob of the Dead: + // fan_trap_use_trigger, acid_trap_trigger, tower_trap_activate_trigger) + allTriggers = getEntArray( "trigger_use", "classname" ); + + for ( i = 0; i < allTriggers.size; i++ ) + { + if ( !IsDefined( allTriggers[i].targetname ) ) + { + continue; + } + + name = allTriggers[i].targetname; + + if ( name == "zombie_trap" ) + { + continue; + } + + if ( IsSubStr( name, "trap" ) ) + { + allTriggers[i] thread WatchTrapActivation(); + } + } +} + +// Polls _trap_in_use instead of waittill("trap_activate"). +// T6 (like T5) only fires "trap_activate" from trap_activate_electric(). +// Fire, rotating, flipper traps don't fire it. _trap_in_use is set to 1 +// on purchase for ALL trap types. +WatchTrapActivation() +{ + trapType = "trap"; + if ( IsDefined( self.script_noteworthy ) ) + { + trapType = self.script_noteworthy; + } + + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) + { + cost = self.zombie_cost; + } + + for ( ;; ) + { + // Wait for trap to be activated (purchased) + while ( true ) + { + if ( IsDefined( self._trap_in_use ) ) + { + if ( self._trap_in_use == 1 ) + { + break; + } + } + wait ( 0.2 ); + } + + // Find who activated it from the connected players + // The game stores the activator context but doesn't pass it with the notify, + // so we check who most recently triggered the use trigger + players = getPlayers(); + closest = undefined; + closestDist = 99999; + + for ( i = 0; i < players.size; i++ ) + { + if ( !IsAlive( players[i] ) ) + { + continue; + } + + dist = distance( players[i].origin, self.origin ); + if ( dist < closestDist ) + { + closestDist = dist; + closest = players[i]; + } + } + + if ( IsDefined( closest ) ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( closest ) + ";trap;activate;" + trapType + ";" + cost + "\n" ); + } + + // Wait for trap to finish and cool down before re-polling + while ( true ) + { + if ( IsDefined( self._trap_in_use ) ) + { + if ( self._trap_in_use != 1 ) + { + break; + } + } + wait ( 1 ); + } + } +} + +///////////////////////////////////////////////////////// +// Monitors buildable completion via level notifies +// T6-specific: buildables fire "_built" on level +///////////////////////////////////////////////////////// +WaitForBuildables() +{ + // Wait for buildable system to initialize + wait ( 3 ); + + if ( !IsDefined( level.zombie_buildables ) ) + { + return; + } + + names = getArrayKeys( level.zombie_buildables ); + + for ( i = 0; i < names.size; i++ ) + { + thread WatchBuildableComplete( names[i] ); + } +} + +WatchBuildableComplete( buildableName ) +{ + for ( ;; ) + { + level waittill( buildableName + "_built", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";build;complete;" + buildableName + "\n" ); + } +} + +///////////////////////////////////////////////////////// +// Monitors craftable completion via level notifies. +// +// Origins (zm_tomb) and Mob of the Dead (zm_prison) use the parallel +// _zm_craftables system instead of _zm_buildables, registering items +// in level.zombie_craftablestubs and emitting "_crafted" on level +// (see _zm_craftables.gsc:1581 — `level notify( name + "_crafted", player )`). +// +// Origins items: 4 elemental staffs, zombie shield, dieseldrone (G-Strike), +// gramophone. MotD items: riot shield, packasplat (Acid Gat Kit), plane, +// refuelable_plane, quest_key1. +// +// We emit identical `GSE;ZP;...;build;complete;` events so the +// downstream pipeline (BuildComplete event log, MapBuildableConfig) treats +// them uniformly. DISTINCT-by-name aggregation in the leaderboard service +// collapses any duplicate notifies (e.g. MotD's repeated refuelable_plane). +///////////////////////////////////////////////////////// +WaitForCraftables() +{ + wait ( 3 ); + + if ( !IsDefined( level.zombie_craftablestubs ) ) + { + return; + } + + names = getArrayKeys( level.zombie_craftablestubs ); + + for ( i = 0; i < names.size; i++ ) + { + thread WatchCraftableComplete( names[i] ); + } + + // Origins-special: gramophone "fully crafted" requires all 6 vinyls placed + // (player + master + 4 elemental records), which most matches never reach + // even when the EE side is fully exercised. The community + EE-completion + // semantics treat the gramophone as "built" the moment it's physically + // placed on the music stand, which fires the gramophone_placed level flag + // (zm_tomb_main_quest.gsc:286 — flag_set fires `level notify(flagname)`). + // Fire BuildComplete on that signal in addition to the standard handler so + // the buildables card actually reflects the player's progress. + if ( IsDefined( level.script ) && level.script == "zm_tomb" ) + { + thread WatchGramophonePlacement(); + } +} + +WatchCraftableComplete( craftableName ) +{ + for ( ;; ) + { + level waittill( craftableName + "_crafted", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) + { + continue; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";build;complete;" + craftableName + "\n" ); + } +} + +// Origins-only. The gramophone_placed flag fires whenever the player +// physically places the gramophone on a music stand (first or subsequent — +// it toggles on pickup/replace). We only emit on the FIRST set per match; +// downstream DISTINCT-by-name aggregation in the leaderboard service would +// dedupe duplicates anyway, but exiting after the first fire saves the +// per-cycle log noise. Player attribution falls to the first connected +// player since the flag notify carries no player arg — buildable lists are +// match-scoped (no per-player credit), so any valid player is fine. +WatchGramophonePlacement() +{ + level waittill( "gramophone_placed" ); + + players = getPlayers(); + if ( !IsDefined( players ) || players.size == 0 ) + { + return; + } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( players[0] ) + ";build;complete;gramophone\n" ); +} + +//-----------------------// +//---- Utility/Infra ----// +//-----------------------// + + +///# +// todo: remove +DebugGiveScore() +{ + self endon( "disconnect" ); + self waittill( "spawned_player" ); + wait ( 0.5 ); + self.score = 1000000; +} + + +BuildPlayerInfoString( entity ) +{ + if ( IsPlayer( entity ) ) + { + guid = entity getGuid(); + clientNumber = entity getEntityNumber(); + team = entity.team; + name = entity.name; + + if ( !IsDefined( name ) ) + { + name = "null"; + } + + return guid + ";" + clientNumber + ";" + team + ";" + name; + } + + return "-1;-1;axis;Zombie"; +} + +///////////////////////////////////////////////////////// +// Easter Egg main quest detection. +// +// Each T6 stock map has a single notify fired on `level` when the EE main +// quest reaches its terminal state (cinematic kickoff / final showdown / etc). +// We arm a per-map watcher based on level.script and emit a one-shot +// "GSE;ZW;easter_egg;complete;" log line. Re-emit is guarded by level.iw4m_ee_fired so even +// if the engine fires the notify twice (rare but possible) we only count once. +// +// Reference: pulled from t6-scripts-main per-map *_sq.gsc / *_achievement.gsc. +// Custom maps: no notify match → silently no-op (debug log records "no +// watcher configured" so you can spot it in server console). +///////////////////////////////////////////////////////// +WaitForEasterEggComplete() +{ + level endon( "end_game" ); + + notifyName = ""; + switch ( level.script ) + { + // zm_transit's "Tower of Babble" is BRANCHING (Maxis vs Richtofen). + // The shared transit_sidequest_achieved notify carries no path identity + // — both paths fire it. The C# side derives per-variant completion from + // the terminal step flags emitted by WaitForT6EasterEggSteps below + // (level.sq_progress["maxis"|"rich"]["FINISHED"] == 1). Don't hook the + // shared notify here or we'd double-count + lose path identity. + case "zm_transit": + case "zm_highrise": + case "zm_buried": + // Branching map — variants drive completion via per-step terminal + // flags emitted by WaitForT6EasterEggSteps. The shared notify + // (transit_sidequest_achieved / highrise_sidequest_achieved / + // buried_sidequest_achieved) carries no path identity, so don't + // hook here. + logprint( "[ZM-EE] Canonical notify intentionally not hooked on " + level.script + " (branching — handled by per-step watchers)\n" ); + return; + case "zm_prison": notifyName = "pop_goes_the_weasel_achieved"; break; + case "zm_tomb": notifyName = "tomb_sidequest_complete"; break; + default: + logprint( "[ZM-EE] No canonical EE watcher configured for map=" + level.script + "\n" ); + return; + } + + logprint( "[ZM-EE] Watcher armed: map=" + level.script + " notify=" + notifyName + "\n" ); + + level waittill( notifyName ); + + if ( IsDefined( level.iw4m_ee_fired ) && level.iw4m_ee_fired ) + { + logprint( "[ZM-EE] Suppressed re-emit on map=" + level.script + " (already fired)\n" ); + return; + } + level.iw4m_ee_fired = true; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] EE complete fired for map=" + level.script + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;complete;" + level.script + "\n" ); +} + +///////////////////////////////////////////////////////// +// Easter Egg per-step detection (T6 — branching + multi-stage quests). +// +// Distinct from WaitForEasterEggComplete above: +// • That hook handles single-quest maps with one terminal notify +// (Highrise / Buried / Prison / Origins). +// • This one handles per-step granularity for branching/multi-stage +// quests (currently zm_transit's Tower of Babble — Maxis vs Richtofen +// paths plus the song bears). +// +// Emits the standard pair when a step fires: +// [ZM-EE] Step fired: +// GSE;ZW;easter_egg;step; +// +// Step keys match _PRIVATE/ZombieStatsPremium/Configuration/MapEasterEggConfig.cs. +// Unknown keys are ignored downstream — adding a watcher here without +// adding the step to the C# config is silent (warning at WRN level). +///////////////////////////////////////////////////////// +WaitForT6EasterEggSteps() +{ + level endon( "end_game" ); + + switch ( level.script ) + { + case "zm_transit": + level thread WatchT6MeteorCounterSong( "t6_tr_bear" ); + level thread WatchT6TransitMaxisPath(); + level thread WatchT6TransitRichPath(); + break; + case "zm_nuked": + // Nuketown has 2 song EEs: a population-trigger one ("Won't Back + // Down" at zombie_pop==15) and the 3-bear meteor-counter one + // ("Samantha's Lullaby"). We track the bears only — the population + // trigger isn't player-driven (it auto-fires by standing still). + level thread WatchT6MeteorCounterSong( "t6_nk_bear" ); + break; + case "zm_highrise": + // Die Rise: song bears + branching "High Maintenance". Pre-branch + // shared stages (atd, slb) deliberately not hooked — see config + // doc-comment for rationale. + level thread WatchT6MeteorCounterSong( "t6_dr_bear" ); + level thread WatchT6HighriseMaxisPath(); + level thread WatchT6HighriseRichPath(); + level thread WatchT6HighriseTerminal(); + break; + case "zm_buried": + // Buried: song bears + branching "Mined Games". Both paths share + // the same stage notifies (bt/mta/gl/ftl/ll/ctw/ip/ows) — path + // identity is determined at emit-time by inspecting the + // sq_is_max_tower_built / sq_is_ric_tower_built flags set during + // the bt (Build Tower) stage when the player commits to a buildable. + level thread WatchT6MeteorCounterSong( "t6_br_bear" ); + level thread WatchT6BuriedStages(); + level thread WatchT6BuriedTerminals(); + break; + case "zm_prison": + // Mob of the Dead: 3 quests. + // • Rusty Cage (3 bottles) — meteor_counter pattern. + // • Where Are We Going (115 then 935 nixie) — 2 stage notifies. + // • Pop Goes the Weasel — 6 progress steps + canonical + // (pop_goes_the_weasel_achieved already in WaitForEasterEggComplete). + level thread WatchT6CounterSong( "t6_md_bottle", ::GetMeteorCounter, 3 ); + level thread WatchT6LevelNotify( "nixie_115", "t6_md_nixie_1" ); + level thread WatchT6LevelNotify( "nixie_935", "t6_md_nixie_2" ); + level thread WatchT6PrisonPgw(); + break; + case "zm_tomb": + // Origins: 3 song EEs (Archangel meteorites / Shepherd of Fire + // radios / Aether generator numbers) + 8-step "Little Lost Girl" + // main quest. Main quest is single-path — canonical hook + // (tomb_sidequest_complete) already in WaitForEasterEggComplete + // above; per-step watchers add granular progress markers. + level thread WatchT6CounterSong( "t6_or_meteor", ::GetMeteorCounter, 3 ); + level thread WatchT6CounterSong( "t6_or_radio", ::GetRadioCounter, 3 ); + level thread WatchT6CounterSong( "t6_or_115", ::Get115Counter, 3 ); + level thread WatchT6OriginsLittleGirlLost(); + break; + default: + // No per-step watcher configured for this map. Silent — the + // canonical hook above still fires for single-quest maps. + return; + } + + logprint( "[ZM-EE] Per-step watchers armed for map=" + level.script + "\n" ); +} + +EmitEeStep( stepKey ) +{ + // Idempotency: in-process dedup so a watcher that polls a flag which + // gets reset and re-flipped doesn't double-emit. C# event processor + // also dedups (HashSet add), so this is belt-and-braces — but cheaper + // to suppress here than to log+drop on the C# side. + if ( !IsDefined( level.iw4m_ee_steps_fired ) ) + { + level.iw4m_ee_steps_fired = []; + } + if ( IsDefined( level.iw4m_ee_steps_fired[ stepKey ] ) ) + { + return; + } + level.iw4m_ee_steps_fired[ stepKey ] = 1; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] Step fired: " + stepKey + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); +} + +// Polls level.sq_progress[group][key] until it flips to 1, then emits the +// step. Won't re-arm — first transition wins (stage flags can reset to 0 +// on rollback paths in stock script; we want "did the player reach this +// stage at least once" semantics). +WatchT6SqProgress( group, key, stepKey ) +{ + level endon( "end_game" ); + + // Guard against init order: sq_progress is built inside sidequest_init_tracker + // which runs after a "start_zombie_round_logic" flag_wait. Poll until ready. + while ( !IsDefined( level.sq_progress ) + || !IsDefined( level.sq_progress[ group ] ) + || !IsDefined( level.sq_progress[ group ][ key ] ) ) + { + wait ( 1.0 ); + } + + while ( level.sq_progress[ group ][ key ] != 1 ) + { + wait ( 0.5 ); + } + + EmitEeStep( stepKey ); +} + +// One-shot notify watcher — waits for a level notify and emits a step. Used +// for stock _zombiemode_sidequests stage transitions which fire predictable +// "__over" notifies on `level` when each stage completes. +WatchT6LevelNotify( notifyName, stepKey ) +{ + level endon( "end_game" ); + level waittill( notifyName ); + EmitEeStep( stepKey ); +} + +WatchT6HighriseMaxisPath() +{ + level endon( "end_game" ); + + // Stock: sidequest_logic_2() fires sq_2_ssp_2_over and sq_2_pts_2_over. + // Terminal handled separately by WatchT6HighriseTerminal so we don't + // double-emit when both paths converge on sq_tower_active. + level thread WatchT6LevelNotify( "sq_2_ssp_2_over", "t6_dr_maxis_a" ); + level thread WatchT6LevelNotify( "sq_2_pts_2_over", "t6_dr_maxis_b" ); +} + +WatchT6HighriseRichPath() +{ + level endon( "end_game" ); + + level thread WatchT6LevelNotify( "sq_1_ssp_1_over", "t6_dr_rich_a" ); + level thread WatchT6LevelNotify( "sq_1_pts_1_over", "t6_dr_rich_b" ); +} + +// Die Rise terminal: stock fires sq_tower_active when the mahjong sequence +// is solved. Path identity is inferred from sq__tower_complete +// flags which were set BEFORE the mahjong phase (in sidequest_logic_ +// after pts stage). Either-or — one of the two flags will be set when we +// reach this point. +WatchT6HighriseTerminal() +{ + level endon( "end_game" ); + + // sq_tower_active is initialized in zm_highrise_sq.gsc init (flag_init); + // safe to flag_wait without an IsDefined guard. + flag_wait( "sq_tower_active" ); + + if ( flag( "sq_ric_tower_complete" ) ) + { + EmitEeStep( "t6_dr_rich_complete" ); + } + else if ( flag( "sq_max_tower_complete" ) ) + { + EmitEeStep( "t6_dr_maxis_complete" ); + } + else + { + // Defensive — sq_tower_active should never fire without one of the + // path-claim flags set. Log so we notice if stock script changes. + logprint( "[ZM-EE] Die Rise sq_tower_active fired but neither tower-complete flag set\n" ); + } +} + +WatchT6TransitMaxisPath() +{ + level endon( "end_game" ); + + level thread WatchT6SqProgress( "maxis", "A_complete", "t6_tr_maxis_a" ); + level thread WatchT6SqProgress( "maxis", "B_complete", "t6_tr_maxis_b" ); + level thread WatchT6SqProgress( "maxis", "C_complete", "t6_tr_maxis_c" ); + level thread WatchT6SqProgress( "maxis", "FINISHED", "t6_tr_maxis_complete" ); +} + +WatchT6TransitRichPath() +{ + level endon( "end_game" ); + + level thread WatchT6SqProgress( "rich", "A_complete", "t6_tr_rich_a" ); + level thread WatchT6SqProgress( "rich", "B_complete", "t6_tr_rich_b" ); + level thread WatchT6SqProgress( "rich", "C_complete", "t6_tr_rich_c" ); + level thread WatchT6SqProgress( "rich", "FINISHED", "t6_tr_rich_complete" ); +} + +// Generic flag-wait watcher — analog of WatchT6LevelNotify for code paths +// driven by flag_set rather than a notify. Defends against the flag not being +// initialized yet (init order race). +WatchT6Flag( flagName, stepKey ) +{ + level endon( "end_game" ); + + while ( !IsDefined( level.flag ) || !IsDefined( level.flag[ flagName ] ) ) + { + wait ( 1.0 ); + } + + flag_wait( flagName ); + EmitEeStep( stepKey ); +} + +// Mob of the Dead — Pop Goes the Weasel main quest. 6 progress steps in stock- +// script execution order (zm_prison_sq_final.gsc:34-36 chain prerequisites, +// then nixie codes / audio logs / plane). Canonical terminal +// (pop_goes_the_weasel_achieved) is hooked separately via WaitForEasterEggComplete. +WatchT6PrisonPgw() +{ + level endon( "end_game" ); + + level thread WatchT6Flag( "quest_completed_thrice", "t6_md_pgw_cycle" ); + level thread WatchT6Flag( "warden_blundergat_obtained","t6_md_pgw_blundergat" ); + level thread WatchT6Flag( "spoon_obtained", "t6_md_pgw_spoon" ); + level thread WatchT6PrisonCodes(); + level thread WatchT6PrisonAudioLogs(); + level thread WatchT6Flag( "plane_boarded", "t6_md_pgw_plane" ); +} + +// 4 mobster prison numbers (101, 481, 386, 872). Stock fires per-code notify +// "nixie_final_" then waittill_multiple in stage_one. Order is player- +// arbitrary so we mirror the multi-wait — emit only when all 4 land. +WatchT6PrisonCodes() +{ + level endon( "end_game" ); + level waittill_multiple( "nixie_final_386", "nixie_final_481", "nixie_final_101", "nixie_final_872" ); + EmitEeStep( "t6_md_pgw_codes" ); +} + +// 6 audio log drops (vox_guar_tour_vo_1 through _10 grouped into 6 plays in +// stage_two). Stock spawns level.m_headphones at first drop and deletes it at +// stage_two:258 after the loop completes. Watching the IsDefined transition +// is cleaner than chaining individual sound-done notifies. +WatchT6PrisonAudioLogs() +{ + level endon( "end_game" ); + + while ( !IsDefined( level.m_headphones ) ) + { + wait ( 1.0 ); + } + while ( IsDefined( level.m_headphones ) ) + { + wait ( 1.0 ); + } + EmitEeStep( "t6_md_pgw_logs" ); +} + +// Origins "Little Lost Girl" main quest — 8 sequential stages, each fires +// little_girl_lost_step__over notify (zm_tomb_ee_main.gsc:83-104). Single +// path — canonical (tomb_sidequest_complete) hooked separately. Step 8's _over +// fires at functionally identical time to the canonical, so no double-emit +// concern (EmitEeStep dedups by step key anyway). +WatchT6OriginsLittleGirlLost() +{ + level endon( "end_game" ); + + level thread WatchT6LevelNotify( "little_girl_lost_step_1_over", "t6_or_llg_1" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_2_over", "t6_or_llg_2" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_3_over", "t6_or_llg_3" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_4_over", "t6_or_llg_4" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_5_over", "t6_or_llg_5" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_6_over", "t6_or_llg_6" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_7_over", "t6_or_llg_7" ); + level thread WatchT6LevelNotify( "little_girl_lost_step_8_over", "t6_or_llg_8" ); +} + +// Buried path-aware step emit. Both Mined Games variants share stage notifies +// in stock; the path identity comes from the per-side flag set during the bt +// (Build Tower) stage when the player commits to a buildable. Resolves the +// active variant by inspecting both flags and emits the appropriate variant's +// step key. Skips emit if neither flag is set (bt hasn't completed yet) — the +// step will fire when the relevant stage notifies. +EmitT6BuriedStep( stepSuffix ) +{ + if ( IsDefined( level.flag ) && IsDefined( level.flag[ "sq_is_max_tower_built" ] ) && level.flag[ "sq_is_max_tower_built" ] ) + { + EmitEeStep( "t6_br_maxis_" + stepSuffix ); + } + else if ( IsDefined( level.flag ) && IsDefined( level.flag[ "sq_is_ric_tower_built" ] ) && level.flag[ "sq_is_ric_tower_built" ] ) + { + EmitEeStep( "t6_br_rich_" + stepSuffix ); + } + else + { + // bt stage hasn't set a path flag yet — drop the step. Should never + // happen in practice (path is determined by the bt completion which + // is itself the first hooked stage), but if stock script ever changes + // the flag-set order, log so we notice. + logprint( "[ZM-EE] Buried stage suffix '" + stepSuffix + "' fired but no path flag set\n" ); + } +} + +// Hook each shared stage notify and route to the active variant's step key. +// Stages map to walkthrough steps (a-h). tpo (Time Bomb placement) is a +// preparation phase — not its own walkthrough step — so we hook ip (the +// switches/bells phase) instead and treat them as combined "Step 7". +WatchT6BuriedStages() +{ + level endon( "end_game" ); + + level thread WatchT6BuriedSimpleStage( "sq_bt_over", "a" ); // build tower + level thread WatchT6BuriedSimpleStage( "sq_mta_over", "b" ); // orbs + level thread WatchT6BuriedSimpleStage( "sq_gl_over", "c" ); // lantern grab + level thread WatchT6BuriedSimpleStage( "sq_ftl_over", "d" ); // power lantern + level thread WatchT6BuriedSimpleStage( "sq_ll_over", "e" ); // lantern placed + level thread WatchT6BuriedWispStage(); // f — ts/ctw loop with success guard + level thread WatchT6BuriedSimpleStage( "sq_ip_over", "g" ); // bells / maze switches + level thread WatchT6BuriedSimpleStage( "sq_ows_over", "h" ); // make a wish +} + +WatchT6BuriedSimpleStage( notifyName, stepSuffix ) +{ + level endon( "end_game" ); + level waittill( notifyName ); + EmitT6BuriedStep( stepSuffix ); +} + +// The decipher/wisp stages (ts then ctw) are inside a while(!flag("sq_wisp_success")) +// retry loop in stock script — players can fail the wisp follow and have to +// re-decipher. Only emit step f on a SUCCESSFUL completion. Loops on each +// ctw_over until sq_wisp_success is set. +WatchT6BuriedWispStage() +{ + level endon( "end_game" ); + + while ( true ) + { + level waittill( "sq_ctw_over" ); + if ( IsDefined( level.flag ) && IsDefined( level.flag[ "sq_wisp_success" ] ) && level.flag[ "sq_wisp_success" ] ) + { + EmitT6BuriedStep( "f" ); + return; + } + // Failed iteration — wait for next ctw_over (player retries the loop). + } +} + +// Per-side terminals — fired explicitly by stock after the path-determination +// flag check at zm_buried_sq.gsc:380-393. Cleaner than inferring from the +// shared buried_sidequest_achieved notify because path identity is unambiguous. +WatchT6BuriedTerminals() +{ + level endon( "end_game" ); + + level thread WatchT6LevelNotify( "sq_maxis_complete", "t6_br_maxis_complete" ); + level thread WatchT6LevelNotify( "sq_richtofen_complete", "t6_br_rich_complete" ); +} + +// Generic counter-based song watcher — used by every T6 map whose song EE +// follows the "N hardcoded origins, counter increments per hit, song fires at +// target" pattern (most T6 song EEs). Polls the value returned by the getter +// function pointer and emits stepKeyPrefix + "_1" / "_2" / "_n" on each +// transition. First-transition-wins per step (EmitEeStep dedups). Returns +// once counter hits target. +// +// Function-pointer indirection because GSC can't dynamically read level[] +// — each counter has its own dedicated reader (GetMeteorCounter, GetRadioCounter, +// Get115Counter) below. Adding a new counter = add a new getter + pass it here. +WatchT6CounterSong( stepKeyPrefix, counterGetter, target ) +{ + level endon( "end_game" ); + + // Stock map-init runs the counter setup before WaitForT6EasterEggSteps + // is armed — but the counter var may not be set yet on race. Defend. + while ( !IsDefined( [[ counterGetter ]]() ) ) + { + wait ( 1.0 ); + } + + lastSeen = 0; + while ( true ) + { + cur = [[ counterGetter ]](); + if ( cur > lastSeen ) + { + // Walk every value we crossed (in case multiple ticks happen + // between polls), capped at target. + for ( i = lastSeen + 1; i <= cur && i <= target; i++ ) + { + EmitEeStep( stepKeyPrefix + "_" + i ); + } + lastSeen = cur; + if ( cur >= target ) + { + return; + } + } + wait ( 0.5 ); + } +} + +GetMeteorCounter() { if ( !IsDefined( level.meteor_counter ) ) return undefined; return level.meteor_counter; } +GetRadioCounter() { if ( !IsDefined( level.found_ee_radio_count ) ) return undefined; return level.found_ee_radio_count; } +Get115Counter() { if ( !IsDefined( level.snd115count ) ) return undefined; return level.snd115count; } + +// Backwards-compatible wrapper for the legacy meteor-counter callers +// (TranZit / Nuketown / Die Rise / Buried / Mob Rusty). Routes to the +// generic watcher with the meteor_counter getter and target=3 (every map +// that uses meteor_counter has a 3-bear EE). +WatchT6MeteorCounterSong( stepKeyPrefix ) +{ + level thread WatchT6CounterSong( stepKeyPrefix, ::GetMeteorCounter, 3 ); +} + +///////////////////////////////////////////////////////// +// PWR — Map power state monitoring +///////////////////////////////////////////////////////// +// +// Mirrors the T5 implementation. T6 _zm.gsc unifies "power_on" flag the +// same way; per-map switch handlers all flag_set after their use trigger. +// +// TranZit additionally clears the flag (zm_transit_power.gsc) — bus power +// loss / pylon disconnect — so the state-change loop emits power_off when +// the flag transitions back to false. Other T6 maps never clear the flag. +// +// Player attribution: same best-effort approach as T5 — watch use-trigger +// entities by common targetname, record activator on level, attribute if +// recent enough when the flag fires. +///////////////////////////////////////////////////////// + +WatchPowerSwitches() +{ + level endon( "end_game" ); + + wait ( 2 ); + + candidates = []; + candidates[0] = "use_power_switch"; + candidates[1] = "power_switch_trig"; + candidates[2] = "power_button"; + + triggers = []; + for ( c = 0; c < candidates.size; c++ ) + { + ents = getentarray( candidates[c], "targetname" ); + for ( i = 0; i < ents.size; i++ ) + { + triggers[triggers.size] = ents[i]; + } + } + + for ( t = 0; t < triggers.size; t++ ) + { + triggers[t] thread WatchSinglePowerSwitch(); + } +} + +WatchSinglePowerSwitch() +{ + level endon( "end_game" ); + self endon( "death" ); + + for ( ;; ) + { + self waittill( "trigger", who ); + if ( IsPlayer( who ) ) + { + level._iw4m_power_activator = who; + level._iw4m_power_activator_time = gettime(); + } + } +} + +WatchPowerStateChanges() +{ + level endon( "end_game" ); + + while ( true ) + { + flag_wait( "power_on" ); + EmitPowerOn(); + + // Spin while flag remains set. Most T6 maps never clear it; TranZit + // does on bus power loss, in which case we fall through and emit off. + while ( flag( "power_on" ) ) + { + wait ( 0.5 ); + } + EmitPowerOff(); + } +} + +EmitPowerOn() +{ + activator = undefined; + if ( IsDefined( level._iw4m_power_activator ) && IsDefined( level._iw4m_power_activator_time ) ) + { + if ( gettime() - level._iw4m_power_activator_time < 5000 ) + { + activator = level._iw4m_power_activator; + } + } + + if ( IsDefined( activator ) ) + { + logPrint( "GSE;ZW;power;on;player;" + BuildPlayerInfoString( activator ) + "\n" ); + } + else + { + logPrint( "GSE;ZW;power;on;world\n" ); + } +} + +EmitPowerOff() +{ + logPrint( "GSE;ZW;power;off;world\n" ); +} + +// Periodic emission of (round, zombies-remaining-to-spawn, currently-alive). Lets +// the live modal compute true "zombies cleared" = budget - remaining - alive, +// which captures trap kills / environmental kills / friendly grenade splash — +// all things that don't credit to a player's kill count but still reduce the +// round's spawn pool. Without this, live SPH would systematically over-estimate +// pace on trap-heavy strategies (Verruckt electric trap, Origins generators). +// +// Throttle: every 5s, only emits when remaining or alive changed since last +// emission. Matches the live-modal poll cadence — no point emitting faster +// than the UI refreshes. ~12 lines/min on an active round, none during +// intermission. +WatchZombiesRemaining() +{ + level endon( "end_game" ); + last_remaining = -1; + last_alive = -1; + while ( true ) + { + wait( 5 ); + if ( !IsDefined( level.zombie_total ) || !IsDefined( level.round_number ) ) + { + continue; + } + remaining = level.zombie_total; + alive = get_current_zombie_count(); + if ( remaining == last_remaining && alive == last_alive ) + { + continue; + } + logPrint( "GSE;ZW;zombies;" + level.round_number + ";" + remaining + ";" + alive + "\n" ); + last_remaining = remaining; + last_alive = alive; + } +} + +///////////////////////////////////////////////////////// +// Bank — Tranzit / Die Rise / Buried only. _zm_banking.gsc +// stores per-player balance in self.account_value (units of +// $1000). Engine emits level "bank_withdrawal" but no +// equivalent on deposit, so we poll the value and emit on +// every transition. +// +// Amount per transition is always $1000 (one increment). +// Fee structure: $1000 paid to deposit → +1 increment; +// withdrawal pays $1000 net back to player but charges a +// $100 fee on top (level.ta_vaultfee). We emit the +/-1000 +// principal — fee accounting belongs server-side if needed. +// +// GSE format: +// ZP;{player};bank;deposit;1000 +// ZP;{player};bank;withdraw;1000 +///////////////////////////////////////////////////////// +WatchBankAccountValue() +{ + self endon( "disconnect" ); + + // _zm_banking.gsc only initialises account_value on maps that load it. + // Wait briefly for first-spawn setup; bail if undefined after 5s (map + // doesn't have a bank — Origins, Nuketown, Mob, all stock T5 maps). + waited = 0; + while ( !IsDefined( self.account_value ) && waited < 5 ) + { + wait ( 0.5 ); + waited = waited + 0.5; + } + + if ( !IsDefined( self.account_value ) ) + { + return; + } + + last = self.account_value; + for ( ;; ) + { + wait ( 0.5 ); + if ( !IsDefined( self.account_value ) ) + { + continue; + } + + current = self.account_value; + if ( current == last ) + { + continue; + } + + delta = current - last; + last = current; + + // One transaction = one increment. Multiple increments in a single + // 0.5s tick would collapse — but the engine gates each trigger with + // an animation lockout (~2s), so this is effectively impossible. + if ( delta > 0 ) + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";bank;deposit;1000\n" ); + } + else + { + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";bank;withdraw;1000\n" ); + } + } +} + +///////////////////////////////////////////////////////// +// Weapon Locker — Tranzit / Die Rise / Buried only. +// _zm_weapon_locker.gsc stores weapondata via the wrapper +// set_stored_weapondata(); we poll has_stored_weapondata() +// per tick and emit on transition. +// +// Weapon name resolution: +// Store: take the player's previousweapon (one tick ago) +// since the engine has already taken the current +// one when we detect the transition. +// Retrieve: the player's current weapon post-grab is the +// weapon they just pulled from the locker. +// +// GSE format: +// ZP;{player};locker;store;{weapon} +// ZP;{player};locker;retrieve;{weapon} +///////////////////////////////////////////////////////// +WatchWeaponLockerSlot() +{ + self endon( "disconnect" ); + + // _zm_weapon_locker.gsc lives in Tranzit, Die Rise, Buried only. + // We deliberately do NOT call wl_has_stored_weapondata() — namespacing + // maps\mp\zombies\_zm_weapon_locker:: would fail to resolve on maps + // that don't load that script (Origins/Nuketown/Mob). Polling the + // offline-path field self.stored_weapon_data directly works on every + // map: it stays undefined on no-locker maps (loop never fires) and + // tracks 1:1 with the locker state on Tranzit/Die Rise/Buried. + last = IsDefined( self.stored_weapon_data ); + + previousWeapon = self getCurrentWeapon(); + for ( ;; ) + { + wait ( 0.2 ); + + currentWeapon = self getCurrentWeapon(); + currentHas = IsDefined( self.stored_weapon_data ); + + if ( currentHas != last ) + { + if ( currentHas ) + { + // Store transition: weapon was taken from the player one tick ago. + weaponName = previousWeapon; + if ( !IsDefined( weaponName ) || weaponName == "" || weaponName == "none" ) + { + weaponName = "unknown"; + } + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";locker;store;" + weaponName + "\n" ); + } + else + { + // Retrieve transition: player's current weapon is the retrieved one. + weaponName = currentWeapon; + if ( !IsDefined( weaponName ) || weaponName == "" || weaponName == "none" ) + { + weaponName = "unknown"; + } + logPrint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";locker;retrieve;" + weaponName + "\n" ); + } + last = currentHas; + } + + previousWeapon = currentWeapon; + } +} + diff --git a/GameFiles/ZombieStats/_zm_stats_t7.compiled.gsc b/GameFiles/ZombieStats/_zm_stats_t7.compiled.gsc new file mode 100644 index 000000000..c0def9b98 Binary files /dev/null and b/GameFiles/ZombieStats/_zm_stats_t7.compiled.gsc differ diff --git a/GameFiles/ZombieStats/_zm_stats_t7.gsc b/GameFiles/ZombieStats/_zm_stats_t7.gsc new file mode 100644 index 000000000..26af1344f --- /dev/null +++ b/GameFiles/ZombieStats/_zm_stats_t7.gsc @@ -0,0 +1,1771 @@ +#using scripts\codescripts\struct; +#using scripts\shared\array_shared; +#using scripts\shared\callbacks_shared; +#using scripts\shared\flag_shared; +#using scripts\shared\system_shared; +#using scripts\shared\util_shared; +#using scripts\shared\ai\zombie_utility; +#using scripts\zm\_zm_utility; +#using scripts\zm\_zm_weapons; + +#insert scripts\shared\shared.gsh; + +// ───────────────────────────────────────────────────────────────── +// T7 Zombie Stats — Game Log Event Emitter +// Port of _zm_stats_t6.gsc to T7 (Black Ops 3 / T7x AlterWare). +// +// Key T7 differences vs T6: +// 1. ZM bypasses the T7-native callback::callback() dispatcher +// (_zm.gsc:1504-1511 sets level.callback* to ZM-specific fns +// which never chain into the dispatcher). So combat hooks use +// the legacy level.callback* override + chain-to-Original +// pattern, same as T6. Watchdog re-installs over map overrides +// (zm_tomb.gsc:244 sets &tomb_actor_damage_override_wrapper). +// 2. Engine signatures differ from T6: actordamage 15 args (vs 11), +// playerdamage 13 args (vs 11). Killed/laststand unchanged. +// 3. _zm_buildables system replaced — buildables coverage dropped +// for v1. Craftables (zm_craftables.gsc) still present and +// hooked. +// 4. Easter Egg coverage is placeholder-only for v1 — per-map +// structure preserved with empty bodies so the wiring is +// visible in code. +// +// Event log format (matches T6 — IW4MAdmin parser is shared): +// GSE;AD;;;;;; = actor damage +// GSE;AK;;;;;; = actor kill +// GSE;D;;;;;; = player damage +// GSE;K;;;... = player kill / zombified +// GSE;RD;;;;; = round data per player +// GSE;RC; = round complete +// GSE;ZP;;;... = player action (perk/box/etc) +// GSE;ZW;... = world-scope event +// ───────────────────────────────────────────────────────────────── + +#namespace zombie_stats; + +REGISTER_SYSTEM( "zombie_stats", &__init__, undefined ) + +function __init__() +{ + callback::on_start_gametype( &init ); +} + +function init() +{ + // Bootstrap dvars for IW4MAdmin recovery on RCon reconnect mid-match. + setdvar( "sv_iw4m_zm_round", 1 ); + setdvar( "sv_iw4m_zm_matchid", "" + randomint( 1000000 ) + "_" + randomint( 1000000 ) ); + + // T7x engine emits "InitGame: map zm_zod; gametype zclassic;" — the + // semicolons short-circuit BaseEventParser.GetEventTypeFromLine through + // the `;`-split path before the .*InitGame.* regex can match, so MapChange + // never fires and MatchStartEvent is never dispatched. Plutonium/CoD-stock + // format uses `\key\val\key\val` (no `;`) which falls through to the regex. + // Emit a synthetic CoD-stock-shaped InitGame so the parser fires MapChange. + // The engine line still lands in the log as Unknown noise but no longer + // gates match start. + logprint( "InitGame: \\mapname\\" + getdvarstring( "mapname" ) + "\\g_gametype\\" + getdvarstring( "g_gametype" ) + "\n" ); + + // Engine-event callbacks (T7-native append-mode). + SetupCallbacks(); + + // Periodic / level-scoped watchers. + thread WaitForRoundChange(); + thread WaitForPlayerConnect(); + thread WaitForPowerupSpawned(); + thread WaitForWeaponPurchases(); + thread WaitForPackAPunch(); + thread WaitForDoorPurchases(); + thread WaitForMysteryBox(); + thread WaitForBoxTeddySuppression(); + thread WaitForTrapActivations(); + thread WaitForCraftables(); + thread WaitForEasterEggComplete(); // stub for v1 — wiring only + thread WaitForT7EasterEggSteps(); // stub for v1 — wiring only + thread WatchPowerSwitches(); + thread WatchPowerStateChanges(); + thread WatchZombiesRemaining(); + thread WaitForGobbleGumMachines(); +} + +function SetupCallbacks() +{ + // ZM bypasses callback::callback() dispatcher (set in _zm.gsc:1504-1511). + // Must override level.callback* legacy hooks and chain to original. + // Engine signatures (cp/_globallogic_actor.gsc: callback_X dispatch): + // playerdamage = 13 args + // actordamage = 15 args (T7 adds vdamageorigin, modelindex, surfacetype, vsurfacenormal vs T6's 11) + // actorkilled = 8 args (same as T6) + // playerlaststand = 9 args (same as T6) + waittillframeend; + + level.callbackActorDamageOriginal = level.callbackactordamage; + level.callbackActorKilledOriginal = level.callbackactorkilled; + level.callbackactordamage = &OnActorDamage; + level.callbackactorkilled = &OnActorKilled; + + level.callbackPlayerDamageOriginal = level.callbackplayerdamage; + level.callbackplayerdamage = &OnPlayerDamaged; + + level.callbackPlayerLastStandOriginal = level.callbackplayerlaststand; + level.callbackplayerlaststand = &OnPlayerDowned; + + // Maps that overwrite level.callback* post-init (e.g. zm_tomb.gsc:244 + // sets &tomb_actor_damage_override_wrapper). Periodic re-install captures + // map fn as new Original to chain through. + thread WatchdogCallbacks(); +} + +function WatchdogCallbacks() +{ + // 1s grace for map setup to finish. + wait ( 1 ); + + for ( ;; ) + { + if ( level.callbackactordamage != &OnActorDamage ) + { + level.callbackActorDamageOriginal = level.callbackactordamage; + level.callbackactordamage = &OnActorDamage; + } + + if ( level.callbackactorkilled != &OnActorKilled ) + { + level.callbackActorKilledOriginal = level.callbackactorkilled; + level.callbackactorkilled = &OnActorKilled; + } + + if ( level.callbackplayerdamage != &OnPlayerDamaged ) + { + level.callbackPlayerDamageOriginal = level.callbackplayerdamage; + level.callbackplayerdamage = &OnPlayerDamaged; + } + + if ( level.callbackplayerlaststand != &OnPlayerDowned ) + { + level.callbackPlayerLastStandOriginal = level.callbackplayerlaststand; + level.callbackplayerlaststand = &OnPlayerDowned; + } + + wait ( 5 ); + } +} + +//-----------------// +//---- Waiters ----// +//-----------------// + +function WaitForPlayerConnect() +{ + for ( ;; ) + { + level waittill( "connecting", player ); + + player thread WaitForPlayerRevive(); + player thread WaitForPlayerZombified(); + player thread WaitForPerkBought(); + player thread WaitForGobbleGumActivate(); + } +} + +function WaitForGobbleGumActivate() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // _zm_bgb.gsc:905 fires `self notify(#"bgb_activation", self.bgb)` when + // a player consumes an "activated" limit_type Gobble Gum (Perkaholic, + // Anywhere But Here, etc.). Auto-trigger types (time/rounds/event) start + // silently — not covered here. + self waittill( "bgb_activation", bgbName ); + if ( !IsDefined( bgbName ) ) { bgbName = "undef"; } + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";gum;activate;" + bgbName + "\n" ); + } +} + +function WaitForGobbleGumMachines() +{ + // Settle window — _zm_bgb_machine.gsc:281 sets up the array via getentarray + // during init. 3s mirrors WaitForCraftables and is safe across maps. + wait ( 3 ); + + if ( !IsDefined( level.bgb_machines ) ) { return; } + + for ( i = 0; i < level.bgb_machines.size; i++ ) + { + level.bgb_machines[i] thread WatchGobbleGumTake(); + } +} + +function WatchGobbleGumTake() +{ + for ( ;; ) + { + // _zm_bgb_machine.gsc:736 — gumball_available fires once selected_bgb + + // current_cost are locked AND cost was deducted at line 699. Snapshot + // here so we still have the data if the machine resets state quickly. + self waittill( "gumball_available" ); + + offeredUser = self.bgb_machine_user; + offeredGum = "undef"; + if ( IsDefined( self.selected_bgb ) ) { offeredGum = self.selected_bgb; } + offeredCost = 0; + if ( IsDefined( self.current_cost ) ) { offeredCost = self.current_cost; } + ghostBall = IsDefined( self.b_bgb_is_available ) && !self.b_bgb_is_available; + + // Race the two terminal notifies of the machine cycle. + // user_grabbed_bgb (line 771) → take path; bgb_machine_accessed will + // trail moments later — drain to keep + // next iteration aligned. + // bgb_machine_accessed (line 831) → end-of-cycle without a take. If + // b_bgb_is_available was false this + // is a refunded ghost-ball, NOT a + // player-driven leave. + outcome = self util::waittill_any_return( "user_grabbed_bgb", "bgb_machine_accessed" ); + + if ( !IsDefined( offeredUser ) || !IsPlayer( offeredUser ) ) { continue; } + + if ( outcome == "user_grabbed_bgb" ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( offeredUser ) + ";gum;take;" + offeredGum + ";" + offeredCost + "\n" ); + self waittill( "bgb_machine_accessed" ); + } + else if ( !ghostBall ) + { + // Cost was deducted upfront and not refunded — player paid for nothing. + logprint( "GSE;ZP;" + BuildPlayerInfoString( offeredUser ) + ";gum;leave;" + offeredGum + ";" + offeredCost + "\n" ); + } + } +} + +function WaitForPlayerZombified() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // T7: _zm.gsc fires self notify(#"zombified") when downed player + // is moved to spectator after revive timer expires. + self waittill( "zombified" ); + playerInfo = BuildPlayerInfoString( self ); + logprint( "GSE;K;" + playerInfo + ";ffffffff;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n" ); + } +} + +function WaitForPlayerRevive() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // Same notify as T6. T7 _zm_laststand fires this on the revivee + // with the reviver as the second arg. + self waittill( "player_revived", reviver ); + + // Self-revive (solo Quick Revive auto / Self Revive gobblegum): + // reviver==self. Emit distinct subtype so downstream classification + // doesn't need guid compare. + if ( IsDefined( reviver ) && IsPlayer( reviver ) && reviver == self ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;self\n" ); + } + else + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";revive;" + BuildPlayerInfoString( reviver ) + "\n" ); + } + } +} + +function WaitForPerkBought() +{ + self endon( "disconnect" ); + + for ( ;; ) + { + // T7 _zm_perks fires self notify(#"perk_bought", perk) — confirmed + // by zm_moon_achievement.gsc waiting on it. + self waittill( "perk_bought", perk ); + // Cost not exposed by the notify (engine hardcodes per-perk). + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";perk;buy;" + perk + ";0\n" ); + } +} + +function WaitForRoundChange() +{ + for ( ;; ) + { + result = level util::waittill_any_return( "intermission", "between_round_over" ); + + players = getplayers(); + + for ( i = 0; i < players.size; i++ ) + { + // Already-zombified players are emitted by WaitForPlayerZombified. + if ( IsDefined( players[i].is_zombie ) && players[i].is_zombie ) + { + continue; + } + if ( zombie_utility::get_current_zombie_count() == 0 ) + { + continue; + } + playerInfo = BuildPlayerInfoString( players[i] ); + logprint( "GSE;K;" + playerInfo + ";ffffffff;-1;axis;Zombie;default_weapon;0;MOD_MELEE;none\n" ); + } + + // Ensure K events drain before RD/RC. IW4MAdmin event processing + // is concurrent; same-frame K+RD can race and drop Deaths stat. + wait ( 0.1 ); + + isGameOver = IsDefined( result ) && result == "intermission"; + PrintPlayerRoundData( isGameOver ); + + if ( isGameOver ) + { + // Synthetic ExitLevel — T7x doesn't write the native engine line + // on game-over, so we fabricate one that matches IW4MAdmin's + // MapEnd regex (BaseEventParser.cs:104 — .*(?:ExitLevel|ShutdownGame).*). + // Without this, MatchEndEvent never fires on T7, OnMatchEnded + // never runs, premium cleanup never happens, ZombieRoundNumber + // stays stale, and EFZombieMatch.Completed never sets. Must NOT + // have a "GSE;" prefix — that branch routes to GameScriptEvent + // BEFORE the MapEnd regex would match. + logprint( "ExitLevel: zombie match ended\n" ); + break; + } + + // Detect special-round type for the round about to begin so IW4MAdmin can + // flag it in the breakdown UI and skip SPH (special spawn budgets don't + // match the regular zombie formula). Single-fire per round transition. + EmitSpecialRoundIfAny(); + } +} + +// T7 special-round flags. Each map-set's _zm_ai_.gsc sets its flag when +// the round starts (and "special_round" alongside as a generic gate). Direct +// level.flag[name] dict access is portable across maps — flag::get would +// assert on un-initialised flags on maps that don't load that AI script. +function EmitSpecialRoundIfAny() +{ + if ( !IsDefined( level.flag ) ) { return; } + + specialType = ""; + if ( IsDefined( level.flag[ "dog_round" ] ) && level.flag[ "dog_round" ] ) { specialType = "dog"; } + else if ( IsDefined( level.flag[ "monkey_round" ] ) && level.flag[ "monkey_round" ] ) { specialType = "monkey"; } + else if ( IsDefined( level.flag[ "wasp_round" ] ) && level.flag[ "wasp_round" ] ) { specialType = "wasp"; } + else if ( IsDefined( level.flag[ "spiders_from_mars_round" ] ) && level.flag[ "spiders_from_mars_round" ] ) { specialType = "spider"; } + else if ( IsDefined( level.flag[ "three_robot_round" ] ) && level.flag[ "three_robot_round" ] ) { specialType = "robot"; } + else if ( IsDefined( level.flag[ "special_quad_round" ] ) && level.flag[ "special_quad_round" ] ) { specialType = "quad"; } + else if ( IsDefined( level.flag[ "boss_round" ] ) && level.flag[ "boss_round" ] ) { specialType = "boss"; } + else if ( IsDefined( level.flag[ "ee_round" ] ) && level.flag[ "ee_round" ] ) { specialType = "ee"; } + + if ( specialType != "" ) + { + logprint( "GSE;ZW;round_special;" + level.round_number + ";" + specialType + "\n" ); + } +} + +//-------------------// +//---- Callbacks ----// +//-------------------// + +// T7 ZM actor-damage signature (15 args — _zm.gsc:6083 actor_damage_override_wrapper). +// Adds vDamageOrigin, modelIndex, surfaceType, vSurfaceNormal vs T6's 11. +// T7 weapon param is a struct (WeaponObject) — use .name for string. Engine's own +// logprint at _globallogic_actor.gsc:212 does the same. +function OnActorDamage( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, boneIndex, modelIndex, surfaceType, vSurfaceNormal ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + if ( IsPlayer( eInflictor ) ) { attackerInfo = BuildPlayerInfoString( eInflictor ); } + weaponName = WeaponName( sWeapon ); + + // Skip on lethal hit — kill callback covers it. + if ( IsDefined( self.health ) && self.health > 0 ) + { + reportedDamage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && reportedDamage > self.maxhealth ) + { + reportedDamage = self.maxhealth; + } + logprint( "GSE;AD;" + victimInfo + ";" + attackerInfo + ";" + weaponName + ";" + reportedDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + } + + self [[ level.callbackActorDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, boneIndex, modelIndex, surfaceType, vSurfaceNormal ); +} + +// T7 ZM actor-killed signature (8 args — _zm.gsc:6136 actor_killed_override). Same as T6. +function OnActorKilled( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + if ( IsPlayer( eInflictor ) ) { attackerInfo = BuildPlayerInfoString( eInflictor ); } + weaponName = WeaponName( sWeapon ); + + damage = iDamage; + if ( IsDefined( self.maxhealth ) && self.maxhealth > 0 && damage > self.maxhealth ) + { + damage = self.maxhealth; + } + logprint( "GSE;AK;" + victimInfo + ";" + attackerInfo + ";" + weaponName + ";" + damage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + self [[ level.callbackActorKilledOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime ); +} + +// T7 ZM player-damage signature (13 args — _zm.gsc:1599 callback_playerdamage). +// Adds vDamageOrigin, vSurfaceNormal vs T6's 11. +function OnPlayerDamaged( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, boneIndex, vSurfaceNormal ) +{ + if ( IsPlayer( eInflictor ) || IsPlayer( eAttacker ) || IsPlayer( self ) ) + { + victimInfo = BuildPlayerInfoString( self ); + attackerInfo = BuildPlayerInfoString( eAttacker ); + if ( IsPlayer( eInflictor ) ) { attackerInfo = BuildPlayerInfoString( eInflictor ); } + weaponName = WeaponName( sWeapon ); + + logprint( "GSE;D;" + victimInfo + ";" + attackerInfo + ";" + weaponName + ";" + iDamage + ";" + sMeansOfDeath + ";" + sHitLoc + "\n" ); + } + + self [[ level.callbackPlayerDamageOriginal ]]( eInflictor, eAttacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, vDamageOrigin, psOffsetTime, boneIndex, vSurfaceNormal ); +} + +// T7 ZM laststand signature (9 args — _zm.gsc:1536 callback_playerlaststand). Same as T6. +function OnPlayerDowned( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ) +{ + // De-dupe re-fires while player is still downed. + if ( !IsDefined( self.revivetrigger ) ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self ) + ";down\n" ); + } + + self [[ level.callbackPlayerLastStandOriginal ]]( eInflictor, eAttacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration ); +} + +//-----------------// +//---- Helpers ----// +//-----------------// + +function PrintPlayerRoundData( isGameOver ) +{ + if ( !IsDefined( level.round_number ) ) + { + return; + } + + players = getplayers(); + currentRound = level.round_number; + + for ( i = 0; i < players.size; i++ ) + { + if ( IsDefined( players[i].sessionstate ) && players[i].sessionstate == "spectator" ) + { + continue; + } + + totalScore = 0; + currentScore = 0; + if ( IsDefined( players[i].score_total ) ) { totalScore = players[i].score_total; } + if ( IsDefined( players[i].score ) ) { currentScore = players[i].score; } + + logprint( "GSE;RD;" + BuildPlayerInfoString( players[i] ) + ";" + totalScore + ";" + currentScore + ";" + currentRound + ";" + isGameOver + "\n" ); + } + + wait ( 0.1 ); + setdvar( "sv_iw4m_zm_round", currentRound ); + logprint( "GSE;RC;" + currentRound + "\n" ); +} + +function BuildPlayerInfoString( entity ) +{ + if ( IsPlayer( entity ) ) + { + // T7x getGuid() returns 0 in offline-style sessions; getxuid() returns + // the Steam XUID hex string that matches RCon status + EFClients.NetworkId. + // Parser default GuidNumberStyle=HexNumber throws FormatException on "0". + guid = entity getxuid(); + clientNumber = entity getEntityNumber(); + team = entity.team; + name = entity.name; + if ( !IsDefined( name ) ) { name = "null"; } + return guid + ";" + clientNumber + ";" + team + ";" + name; + } + // "ffffffff" not "-1" — HexNumber parser throws on signed "-1". + return "ffffffff;-1;axis;Zombie"; +} + +// T7 weapons are structs; string-coerce safely. Accepts a WeaponObject, a string, +// or undefined. Returns the engine name ("ar_modern_zm" etc.) or "none"/"undef" +// fallbacks so log lines never include an entity pointer. +function WeaponName( w ) +{ + if ( !IsDefined( w ) ) { return "undef"; } + if ( IsString( w ) ) { return w; } + if ( IsDefined( w.name ) ) { return w.name; } + return "undef"; +} + +//----------------// +//---- Powerups ----// +//----------------// + +// Event-driven powerup detection. Engine fires: +// level notify("powerup_dropped", powerup) — on every drop (zombie kill, gum, scripted) +// self notify("powerup_grabbed") — on the entity at engine-grab time +// +// **STRING-FORM notify ONLY**. Hashed `#"powerup_dropped"` / `#"powerup_grabbed"` do NOT +// fire in T7x's runtime — verified empirically 2026-05-14 via three-way detection race. +// Hash mismatch between our linker and the engine; shiversoftdev decompile shows +// these notifies as `#"..."` but the names are reverse-lookup guesses and the actual +// engine hashes differ. Use STRING form for all engine-emitted notifies on T7x. +// +// Polling at script-init catches one-shot pre-existing entities (powerups spawned by +// init scripts before our notify listeners attached). After init, the notify path +// handles everything. iw4m_pwr_seen sentinel deduplicates poll vs notify pickups. +function WaitForPowerupSpawned() +{ + level thread WatchPowerupSpawnNotify(); + level thread WatchPowerupSpawnInitScan(); +} + +function WatchPowerupSpawnNotify() +{ + for ( ;; ) + { + level waittill( "powerup_dropped", powerup ); + if ( !IsDefined( powerup ) ) { continue; } + if ( IsDefined( powerup.iw4m_pwr_seen ) ) { continue; } + powerup.iw4m_pwr_seen = true; + powerup thread WaitForPowerupGrab(); + } +} + +// One-shot scan to catch powerups already in-world when our threads started +// (e.g. spawned during map init before our notify listener attached). +function WatchPowerupSpawnInitScan() +{ + wait ( 0.05 ); + models = getentarray( "script_model", "classname" ); + for ( i = 0; i < models.size; i++ ) + { + if ( !IsDefined( models[i].powerup_name ) ) { continue; } + if ( IsDefined( models[i].iw4m_pwr_seen ) ) { continue; } + models[i].iw4m_pwr_seen = true; + models[i] thread WaitForPowerupGrab(); + } +} + +function WaitForPowerupGrab() +{ + self endon( "powerup_timedout" ); + + self waittill( "powerup_grabbed" ); + + if ( !IsDefined( self ) ) { return; } + + powerup = "unknown"; + if ( IsDefined( self.powerup_name ) ) { powerup = self.powerup_name; } + + // Engine's grab notify carries no player arg — resolve grabbing player by + // nearest-distance snapshot. The engine's own grab logic uses ~64-unit + // proximity, so nearest-player at notify-fire time matches the engine's + // pick with very high accuracy (solo: 100%; co-op: ~99%). + grabber = undefined; + bestDist = 999999; + players = getplayers(); + for ( i = 0; i < players.size; i++ ) + { + if ( !IsDefined( players[i] ) || !IsPlayer( players[i] ) ) { continue; } + if ( !IsDefined( players[i].origin ) ) { continue; } + d = distance( players[i].origin, self.origin ); + if ( d < bestDist ) + { + bestDist = d; + grabber = players[i]; + } + } + + if ( !IsDefined( grabber ) ) { return; } + logprint( "GSE;ZP;" + BuildPlayerInfoString( grabber ) + ";powerup;grab;" + powerup + "\n" ); +} + +//----------------// +//---- Economy ----// +//----------------// + +function WaitForWeaponPurchases() +{ + for ( ;; ) + { + // T7 _zm_weapons.gsc:2473 fires `level notify(#"weapon_bought", player, self.weapon)`. + // Third arg is a WeaponObject struct, NOT a string. Use zm_weapons::get_weapon_cost + // which the engine itself uses (line 2452) — does the level.zombie_weapons[struct].cost + // lookup safely for us. + level waittill( "weapon_bought", player, weaponObj ); + if ( !IsDefined( player ) || !IsPlayer( player ) ) { continue; } + + cost = 0; + if ( IsDefined( weaponObj ) ) + { + cost = zm_weapons::get_weapon_cost( weaponObj ); + if ( !IsDefined( cost ) ) { cost = 0; } + } + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";weapon;buy;" + WeaponName( weaponObj ) + ";" + cost + "\n" ); + } +} + +function WaitForDoorPurchases() +{ + wait ( 2 ); + + doors = getentarray( "zombie_door", "targetname" ); + debris = getentarray( "zombie_debris", "targetname" ); + + for ( i = 0; i < doors.size; i++ ) { doors[i] thread WatchDoorPurchase(); } + for ( i = 0; i < debris.size; i++ ) { debris[i] thread WatchDoorPurchase(); } +} + +function WatchDoorPurchase() +{ + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) { cost = self.zombie_cost; } + + self waittill( "trigger", player ); + + if ( !IsDefined( player ) || !IsPlayer( player ) ) { return; } + if ( !IsDefined( player.score ) || player.score < cost ) { return; } + + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";door;buy;" + cost + "\n" ); +} + +//----------------// +//---- Pack-a-Punch ----// +//----------------// + +function WaitForPackAPunch() +{ + wait ( 2 ); + + // T7: PaP triggers are published in level.pack_a_punch.triggers by + // _zm_pack_a_punch.gsc init. + if ( !IsDefined( level.pack_a_punch ) || !IsDefined( level.pack_a_punch.triggers ) ) + { + return; + } + + papTriggers = level.pack_a_punch.triggers; + if ( papTriggers.size == 0 ) { return; } + + for ( i = 0; i < papTriggers.size; i++ ) + { + papTriggers[i].iw4m_pap_buyer = undefined; + papTriggers[i].iw4m_pap_buyer_weapon = undefined; + papTriggers[i].iw4m_pap_taken_flag = false; + papTriggers[i].iw4m_pap_timeout_flag = false; + papTriggers[i].iw4m_pap_disconnect_flag = false; + + papTriggers[i] thread WatchPapOutcome(); + papTriggers[i] thread WatchPapTriggerForBuyer(); + papTriggers[i] thread WatchPapTakenFlag(); + papTriggers[i] thread WatchPapTimeoutFlag(); + papTriggers[i] thread WatchPapDisconnectFlag(); + } +} + +// First-notify-wins. Engine has stale per-iter threads (wait_for_player_to_take, +// wait_for_timeout) that can outlive their iter and fire pap_taken/pap_timeout +// AFTER another notify resolved the iter. Ignoring later notifies prevents +// misclassification (abandon emitted as upgrade when stale take notify fires +// after timeout cleared the iter). +function WatchPapTakenFlag() +{ + for ( ;; ) + { + self waittill( "pap_taken" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) { continue; } + self.iw4m_pap_taken_flag = true; + } +} + +function WatchPapTimeoutFlag() +{ + for ( ;; ) + { + self waittill( "pap_timeout" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) { continue; } + self.iw4m_pap_timeout_flag = true; + } +} + +// T7 _zm_pack_a_punch.gsc:772 fires pap_player_disconnected when buyer leaves +// mid-PaP. Short-circuits emission — no player to credit. +function WatchPapDisconnectFlag() +{ + for ( ;; ) + { + self waittill( "pap_player_disconnected" ); + if ( self.iw4m_pap_taken_flag || self.iw4m_pap_timeout_flag || self.iw4m_pap_disconnect_flag ) { continue; } + self.iw4m_pap_disconnect_flag = true; + } +} + +function WatchPapTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_pap_buyer ) ) { continue; } + if ( !IsDefined( who ) || !IsPlayer( who ) ) { continue; } + + // Phase 2: engine accepted (self.current_weapon set to a real weapon + // struct, not level.weaponnone). Buyer is the player whose weapon engine + // just took — their getcurrentweapon() now returns level.weaponnone. + // Late F-pressers in phase2 still hold their own weapon → discriminates. + if ( IsDefined( self.current_weapon ) && self.current_weapon !== level.weaponnone ) + { + buyerWeapon = who getcurrentweapon(); + if ( IsDefined( buyerWeapon ) && buyerWeapon !== level.weaponnone ) + { + continue; + } + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = self.current_weapon; + continue; + } + + // Phase 1: pre-engine-accept. Replicate engine score + upgradeable gates + // so we don't lock on rejected presses. + if ( !IsDefined( who.score ) || who.score < 5000 ) { continue; } + + weapon = who getcurrentweapon(); + if ( !IsDefined( weapon ) || weapon === level.weaponnone ) { continue; } + if ( !zm_weapons::can_upgrade_weapon( weapon ) ) { continue; } + + self.iw4m_pap_buyer = who; + self.iw4m_pap_buyer_weapon = weapon; + + // Verify engine actually accepted; unlock otherwise. Handles engine-side + // gates we don't replicate (laststand, throwing grenade, weapon switch). + self thread VerifyPapBuyerLock(); + } +} + +function VerifyPapBuyerLock() +{ + // Poll for engine acceptance up to 2s. T6 lesson: 0.25s wasn't long enough; + // engine sometimes delays setting current_weapon past that → unlocks legit + // buyer → later stale F-press re-locks wrong weapon → false mismatch → + // skipped emit. Defensive port even though only T6 was observed failing. + timeoutMs = 2000; + pollMs = 50; + elapsedMs = 0; + while ( elapsedMs < timeoutMs ) + { + if ( IsDefined( self.current_weapon ) && self.current_weapon !== level.weaponnone ) + { + return; + } + wait ( 0.05 ); + elapsedMs = elapsedMs + pollMs; + } + + if ( IsDefined( self.iw4m_pap_buyer ) ) + { + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + } +} + +function WatchPapOutcome() +{ + for ( ;; ) + { + self.iw4m_pap_taken_flag = false; + self.iw4m_pap_timeout_flag = false; + self.iw4m_pap_disconnect_flag = false; + self.iw4m_pap_buyer = undefined; + self.iw4m_pap_buyer_weapon = undefined; + + // Phase 1: wait for engine to accept a buy. T7 _zm_pack_a_punch.gsc:507 + // sets self.current_weapon = current_weapon (struct); cleared back to + // level.weaponnone at line 527. + while ( !IsDefined( self.current_weapon ) || self.current_weapon === level.weaponnone ) + { + wait ( 0.05 ); + } + + oldWeapon = self.current_weapon; + + // Phase 2: wait for iter boundary (current_weapon clears OR engine + // immediately starts new iter with different weapon). + while ( IsDefined( self.current_weapon ) && self.current_weapon === oldWeapon ) + { + wait ( 0.05 ); + } + + // Disconnect short-circuits emission (no player to credit). + if ( self.iw4m_pap_disconnect_flag ) + { + continue; + } + + if ( !IsDefined( self.iw4m_pap_buyer ) || !IsPlayer( self.iw4m_pap_buyer ) ) + { + continue; + } + + // T7 PaP cost: self.cost = 5000 base, 1000 during sale. T7 replaces T6's + // attachment-only re-PaP (self.attachment_cost) with the AAT system — + // separate self.aat_cost, separate flow. Skip AAT for now; just emit + // base/upgrade cost. + cost = 5000; + if ( IsDefined( self.cost ) ) { cost = self.cost; } + + if ( self.iw4m_pap_taken_flag ) + { + // T7-native upgrade lookup via zm_weapons helper. Fallback to "_upgraded" + // string if helper returns undef on some edge case. + upgradedWeapon = undefined; + if ( IsDefined( oldWeapon ) && oldWeapon !== level.weaponnone ) + { + upgradedWeapon = zm_weapons::get_upgrade_weapon( oldWeapon ); + } + oldName = WeaponName( oldWeapon ); + newName = WeaponName( upgradedWeapon ); + if ( newName == "undef" ) { newName = oldName + "_upgraded"; } + logprint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;upgrade;" + oldName + ";" + newName + ";" + cost + "\n" ); + } + else if ( self.iw4m_pap_timeout_flag ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( self.iw4m_pap_buyer ) + ";weapon;abandon;" + WeaponName( oldWeapon ) + ";" + cost + "\n" ); + } + } +} + +//----------------// +//---- Mystery Box ----// +//----------------// + +function WaitForMysteryBox() +{ + wait ( 5 ); + + if ( !IsDefined( level.chests ) ) { return; } + + for ( i = 0; i < level.chests.size; i++ ) + { + level.chests[i].iw4m_box_teddy_marker = false; + level.chests[i].iw4m_box_in_late_phase = false; + + level.chests[i] thread WatchBoxOutcome(); + level.chests[i] thread WatchBoxTriggerForBuyer(); + } +} + +function WaitForBoxTeddySuppression() +{ + wait ( 5 ); + for ( ;; ) + { + level waittill( "weapon_fly_away_start" ); + if ( !IsDefined( level.chests ) ) { continue; } + + for ( k = 0; k < level.chests.size; k++ ) + { + if ( IsDefined( level.chests[k].iw4m_box_in_late_phase ) && level.chests[k].iw4m_box_in_late_phase ) + { + level.chests[k].iw4m_box_teddy_marker = true; + } + } + } +} + +function WatchBoxTriggerForBuyer() +{ + for ( ;; ) + { + self waittill( "trigger", who ); + + if ( IsDefined( self.iw4m_box_last_trigger ) ) { continue; } + if ( !IsDefined( who ) || !IsPlayer( who ) ) { continue; } + + cost = 950; + if ( IsDefined( level.zombie_treasure_chest_cost ) ) { cost = level.zombie_treasure_chest_cost; } + else if ( IsDefined( self.zombie_cost ) ) { cost = self.zombie_cost; } + + if ( !IsDefined( who.score ) || who.score < cost ) { continue; } + self.iw4m_box_last_trigger = who; + } +} + +function WatchBoxOutcome() +{ + if ( !IsDefined( self.zbarrier ) ) { return; } + + for ( ;; ) + { + self.zbarrier waittill( "randomization_done" ); + self.iw4m_box_in_late_phase = true; + + // T7: zbarrier.weapon is a struct (set by treasure_chest_weapon_locking). + // T6 stored a string on .weapon_string — that property doesn't exist in T7. + weaponName = "undef"; + if ( IsDefined( self.zbarrier.weapon ) ) { weaponName = WeaponName( self.zbarrier.weapon ); } + + capturedUser = undefined; + if ( IsDefined( self.chest_user ) && IsPlayer( self.chest_user ) ) + { + capturedUser = self.chest_user; + } + + self.zbarrier waittill( "box_spin_done" ); + + timedOutValue = false; + timedOutDefined = 0; + if ( IsDefined( self.timedout ) ) + { + timedOutDefined = 1; + if ( self.timedout ) { timedOutValue = true; } + } + + isTeddy = false; + if ( IsDefined( self.iw4m_box_teddy_marker ) && self.iw4m_box_teddy_marker ) { isTeddy = true; } + self.iw4m_box_teddy_marker = false; + + // 3-tier user resolution (snapshot → live → trigger-capture). + user = capturedUser; + if ( !IsDefined( user ) && IsDefined( self.chest_user ) && IsPlayer( self.chest_user ) ) + { + user = self.chest_user; + } + if ( !IsDefined( user ) && timedOutDefined == 1 && IsDefined( self.iw4m_box_last_trigger ) && IsPlayer( self.iw4m_box_last_trigger ) ) + { + user = self.iw4m_box_last_trigger; + } + + cost = 950; + if ( IsDefined( self.zombie_cost ) ) { cost = self.zombie_cost; } + + if ( IsDefined( user ) ) + { + if ( isTeddy ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;teddy;" + cost + "\n" ); + } + else if ( timedOutValue ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;pass;" + weaponName + ";" + cost + "\n" ); + } + else + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( user ) + ";box;take;" + weaponName + ";" + cost + "\n" ); + } + } + + self.iw4m_box_last_trigger = undefined; + self.iw4m_box_in_late_phase = false; + } +} + +//----------------// +//---- Traps ----// +//----------------// + +function WaitForTrapActivations() +{ + wait ( 2 ); + + traps = getentarray( "zombie_trap", "targetname" ); + for ( i = 0; i < traps.size; i++ ) { traps[i] thread WatchTrapActivation(); } +} + +function WatchTrapActivation() +{ + trapType = "trap"; + if ( IsDefined( self.script_noteworthy ) ) { trapType = self.script_noteworthy; } + + cost = 1000; + if ( IsDefined( self.zombie_cost ) ) { cost = self.zombie_cost; } + + for ( ;; ) + { + while ( true ) + { + if ( IsDefined( self._trap_in_use ) && self._trap_in_use == 1 ) { break; } + wait ( 0.2 ); + } + + players = getplayers(); + closest = undefined; + closestDist = 99999; + + for ( i = 0; i < players.size; i++ ) + { + if ( !IsAlive( players[i] ) ) { continue; } + dist = distance( players[i].origin, self.origin ); + if ( dist < closestDist ) + { + closestDist = dist; + closest = players[i]; + } + } + + if ( IsDefined( closest ) ) + { + logprint( "GSE;ZP;" + BuildPlayerInfoString( closest ) + ";trap;activate;" + trapType + ";" + cost + "\n" ); + } + + while ( true ) + { + if ( IsDefined( self._trap_in_use ) && self._trap_in_use != 1 ) { break; } + wait ( 1 ); + } + } +} + +//----------------// +//---- Craftables ----// +//----------------// + +function WaitForCraftables() +{ + wait ( 3 ); + + if ( !IsDefined( level.zombie_craftablestubs ) ) { return; } + + names = getarraykeys( level.zombie_craftablestubs ); + for ( i = 0; i < names.size; i++ ) + { + thread WatchCraftableComplete( names[i] ); + } +} + +function WatchCraftableComplete( craftableName ) +{ + for ( ;; ) + { + // T7 _zm_craftables fires `level notify(name + "_crafted", player)`. + level waittill( craftableName + "_crafted", player ); + if ( !IsDefined( player ) || !IsPlayer( player ) ) { continue; } + logprint( "GSE;ZP;" + BuildPlayerInfoString( player ) + ";build;complete;" + craftableName + "\n" ); + } +} + +//----------------// +//---- Power ----// +//----------------// + +function WatchPowerSwitches() +{ + level endon( "end_game" ); + wait ( 2 ); + + candidates = []; + candidates[0] = "use_power_switch"; + candidates[1] = "power_switch_trig"; + candidates[2] = "power_button"; + + triggers = []; + for ( c = 0; c < candidates.size; c++ ) + { + ents = getentarray( candidates[c], "targetname" ); + for ( i = 0; i < ents.size; i++ ) { triggers[ triggers.size ] = ents[i]; } + } + + for ( t = 0; t < triggers.size; t++ ) { triggers[t] thread WatchSinglePowerSwitch(); } +} + +function WatchSinglePowerSwitch() +{ + level endon( "end_game" ); + self endon( "death" ); + + for ( ;; ) + { + self waittill( "trigger", who ); + if ( IsPlayer( who ) ) + { + level._iw4m_power_activator = who; + level._iw4m_power_activator_time = gettime(); + } + } +} + +function WatchPowerStateChanges() +{ + level endon( "end_game" ); + + while ( true ) + { + level flag::wait_till( "power_on" ); + EmitPowerOn(); + + while ( level flag::get( "power_on" ) ) + { + wait ( 0.5 ); + } + EmitPowerOff(); + } +} + +function EmitPowerOn() +{ + activator = undefined; + if ( IsDefined( level._iw4m_power_activator ) && IsDefined( level._iw4m_power_activator_time ) ) + { + if ( gettime() - level._iw4m_power_activator_time < 5000 ) + { + activator = level._iw4m_power_activator; + } + } + + if ( IsDefined( activator ) ) + { + logprint( "GSE;ZW;power;on;player;" + BuildPlayerInfoString( activator ) + "\n" ); + } + else + { + logprint( "GSE;ZW;power;on;world\n" ); + } +} + +function EmitPowerOff() +{ + logprint( "GSE;ZW;power;off;world\n" ); +} + +//----------------// +//---- Zombies Remaining ----// +//----------------// + +function WatchZombiesRemaining() +{ + level endon( "end_game" ); + last_remaining = -1; + last_alive = -1; + + while ( true ) + { + wait ( 5 ); + if ( !IsDefined( level.zombie_total ) || !IsDefined( level.round_number ) ) { continue; } + + remaining = level.zombie_total; + alive = zombie_utility::get_current_zombie_count(); + if ( remaining == last_remaining && alive == last_alive ) { continue; } + + logprint( "GSE;ZW;zombies;" + level.round_number + ";" + remaining + ";" + alive + "\n" ); + last_remaining = remaining; + last_alive = alive; + } +} + +//----------------// +//---- Easter Eggs (V1 STUBS — wiring only) ----// +//----------------// + +// Per-map switch preserved as a hookpoint. **Intentionally empty bodies — +// not a stub.** T7 EE completion is derived server-side from terminal-step +// emissions in WaitForT7EasterEggSteps below; the premium plugin sets +// quest.HasCanonicalNotify = false on every T7 quest, so the "all steps +// logged → mark complete" derivation path is authoritative. Same outcome +// as T4/T5/T6's canonical emission, just on a different signal channel. +// +// Why the divergence: most T7 main-quest terminal flags are hashed in the +// shiversoftdev dump (no source name), so a clean named-notify wait isn't +// always available. Step-based derivation lets us cover all 14 maps using +// the named per-step flags we DO have, without needing a canonical +// terminal flag per map. Cases where T7 DOES have a clean terminal flag +// (e.g. zm_zod's "ee_complete") are still surfaced — they appear as the +// final step in the quest's step list (t7_soe_complete), which fires the +// derivation when logged. +function WaitForEasterEggComplete() +{ + level endon( "end_game" ); + + if ( !IsDefined( level.script ) ) + { + return; + } + + switch ( level.script ) + { + case "zm_zod": break; // Shadows of Evil — Apocalypse Averted + case "zm_factory": break; // The Giant + case "zm_castle": break; // Der Eisendrache — My Brother's Keeper + case "zm_island": break; // Zetsubou No Shima — A Better Tomorrow + case "zm_stalingrad": break; // Gorod Krovi — Love and War + case "zm_genesis": break; // Revelations — For The Good of All + case "zm_prototype": break; // Nacht der Untoten + case "zm_asylum": break; // Verruckt + case "zm_sumpf": break; // Shi No Numa + case "zm_cosmodrome": break; // Ascension — Casimir Mechanism + case "zm_theater": break; // Kino der Toten — no main quest + case "zm_moon": break; // Moon — Richtofen's Grand Scheme + case "zm_temple": break; // Shangri-La — Time Travel Will Tell + case "zm_tomb": break; // Origins — Little Lost Girl + default: + logprint( "[ZM-EE] No canonical EE watcher configured for map=" + level.script + "\n" ); + return; + } +} + +function WaitForT7EasterEggSteps() +{ + level endon( "end_game" ); + + if ( !IsDefined( level.script ) ) + { + return; + } + + switch ( level.script ) + { + case "zm_zod": + // Shadows of Evil — THREE quests: + // Apocalypse Averted (main, requires 4P for full ending; C# gates + // UI with MinPlayers=4). 8 step flags exposed by zm_zod_ee.gsc: + // ee_book, totem_placed, 4 × ee_keeper__resurrected, + // ee_boss_defeated (phase 1 = "Apocalypse Ascendant"), + // ee_complete (terminal = "Apocalypse Averted"). + // Song "Snake Skin Boots" — 3 radios → music state + // "snakeskinboots" or "_instr" (radio-order-dependent). + // Song "Cold Hard Cash" — 3 mic parts assembled → "coldhardcash". + level thread WatchT7FlagStep( "ee_book", "t7_soe_book" ); + level thread WatchT7FlagStep( "totem_placed", "t7_soe_totem" ); + level thread WatchT7FlagStep( "ee_keeper_boxer_resurrected", "t7_soe_keeper_boxer" ); + level thread WatchT7FlagStep( "ee_keeper_detective_resurrected", "t7_soe_keeper_detective" ); + level thread WatchT7FlagStep( "ee_keeper_femme_resurrected", "t7_soe_keeper_femme" ); + level thread WatchT7FlagStep( "ee_keeper_magician_resurrected", "t7_soe_keeper_magician" ); + level thread WatchT7FlagStep( "ee_boss_defeated", "t7_soe_boss_1" ); + level thread WatchT7FlagStep( "ee_complete", "t7_soe_complete" ); + level thread WatchT7MusicStateStep( "snakeskinboots", "t7_soe_snakeskin" ); + level thread WatchT7MusicStateStep( "snakeskinboots_instr", "t7_soe_snakeskin" ); + level thread WatchT7MusicStateStep( "coldhardcash", "t7_soe_cash" ); + // Equipment-upgrade side quests — wonder-weapon-equivalent for SoE: + // Upgraded Riot Shield — weapon name "zod_riotshield_upgraded" + // bought via zm_equipment::buy at zm_zod_ee_side.gsc:1635. + // Upgraded Bouncing Bettys (Trip Mines) — "bouncingbetty_holly" + // mine type registered at zm_zod_ee_side.gsc:65. + // Upgraded Li'l Arnies — hash-literal terminal notify + // #"hash_21edb6b6" fired at zm_zod_ee_side.gsc:1481 after the + // stage-dance cutscene. BO3 EOL = hash frozen, safe to wait. + level thread WatchWeaponSubstringUpgrade( "zod_riotshield_upgraded", "t7_soe_shield_upgrade" ); + level thread WatchWeaponSubstringUpgrade( "bouncingbetty_holly", "t7_soe_betty_upgrade" ); + level thread WatchSoEArnieUpgrade(); + break; + case "zm_factory": + // The Giant — three EEs: song (Beauty of Annihilation Remix), + // flytrap (Hide and Go Seek main quest), secret perk (Sixth Perk). + // All three use HasCanonicalNotify: false on the C# side — completion + // derives from "all steps logged" because no single notify cleanly + // signals end-state per quest (flytrap is a 3-flag composite, secret + // perk's snow_ee_completed flag is itself the final step we emit). + level thread WatchFactoryMusic(); + level thread WatchFactoryFlytrap(); + level thread WatchFactorySecretPerk(); + break; + case "zm_castle": + // Der Eisendrache — SIX quests: + // My Brother's Keeper (main, MinPlayers=1 — solo canonical): + // 8 step flags from zm_castle_ee.gsc covering pyramid → + // fuse → safe → key → simon → MPD canister → moon rockets → outro. + // Song "Dead Again" — 3 teddy bears → music state "dead_again". + // Song "Requiem Aeternam" (Gramophone) — 3 gramophones → "requiem". + // Music Box — single activation in Samantha's bedroom. + // Disco Inferno — moon globe shot at planetary alignment. + // Elemental Bow Upgrades — 4 bows (Storm/Wolf/Fire/Void). + // Engine flag names are hashed in the dump; detect via per-player + // weapon inventory poll for each elemental_bow_* substring. + level thread WatchT7FlagStep( "ee_start_done", "t7_de_pyramid" ); + level thread WatchT7FlagStep( "ee_fuse_placed", "t7_de_fuse" ); + level thread WatchT7FlagStep( "ee_safe_open", "t7_de_safe" ); + level thread WatchT7FlagStep( "ee_golden_key", "t7_de_key" ); + level thread WatchT7FlagStep( "end_simon", "t7_de_simon" ); + level thread WatchT7FlagStep( "mpd_canister_replacement", "t7_de_canister" ); + level thread WatchT7FlagStep( "sent_rockets_to_the_moon", "t7_de_rockets" ); + level thread WatchT7FlagStep( "ee_outro", "t7_de_complete" ); + level thread WatchT7MusicStateStep( "dead_again", "t7_de_song_deadagain" ); + level thread WatchT7MusicStateStep( "requiem", "t7_de_song_requiem" ); + level thread WatchT7FlagStep( "ee_music_box_turning", "t7_de_musicbox" ); + level thread WatchT7FlagStep( "ee_disco_inferno", "t7_de_disco" ); + level thread WatchWeaponSubstringUpgrade( "elemental_bow_storm", "t7_de_bow_storm" ); + level thread WatchWeaponSubstringUpgrade( "elemental_bow_wolf_howl", "t7_de_bow_wolf" ); + level thread WatchWeaponSubstringUpgrade( "elemental_bow_rune_prison", "t7_de_bow_fire" ); + level thread WatchWeaponSubstringUpgrade( "elemental_bow_demongate", "t7_de_bow_void" ); + break; + case "zm_island": + // Zetsubou No Shima — FIVE quests: + // Seeds of Doubt (main, solo-canonical): + // trilogy_released → 3 elevator gears → AA gun → outro. + // KT-4 Wonder Weapon: base ww_obtained + Masamune upgrade. + // Skull of Nan Sapwe: 4 altar rituals + skull obtained. + // Spider EE: cage charge → mom trapped → quest complete. + // Song "Dead Flowers": music state poll, single step. + level thread WatchT7FlagStep( "trilogy_released", "t7_zn_trilogy" ); + level thread WatchT7FlagStep( "elevator_part_gear1_placed", "t7_zn_gear_1" ); + level thread WatchT7FlagStep( "elevator_part_gear2_placed", "t7_zn_gear_2" ); + level thread WatchT7FlagStep( "elevator_part_gear3_placed", "t7_zn_gear_3" ); + level thread WatchT7FlagStep( "aa_gun_ee_complete", "t7_zn_aagun" ); + level thread WatchT7FlagStep( "flag_outro_cutscene_done", "t7_zn_complete" ); + level thread WatchT7FlagStep( "ww_obtained", "t7_zn_kt4_base" ); + level thread WatchT7FlagStep( "wwup_ready", "t7_zn_kt4_upgrade" ); + level thread WatchT7FlagStep( "skullquest_ritual_complete1", "t7_zn_skull_ritual_1" ); + level thread WatchT7FlagStep( "skullquest_ritual_complete2", "t7_zn_skull_ritual_2" ); + level thread WatchT7FlagStep( "skullquest_ritual_complete3", "t7_zn_skull_ritual_3" ); + level thread WatchT7FlagStep( "skullquest_ritual_complete4", "t7_zn_skull_ritual_4" ); + level thread WatchT7FlagStep( "skull_quest_complete", "t7_zn_skull_obtained" ); + level thread WatchT7FlagStep( "charged_spider_cage_powerup", "t7_zn_spider_charge" ); + level thread WatchT7FlagStep( "spider_from_mars_trapped_in_raised_cage", "t7_zn_spider_trap" ); + level thread WatchT7FlagStep( "spider_ee_quest_complete", "t7_zn_spider_complete" ); + level thread WatchT7MusicStateStep( "dead_flowers", "t7_zn_song" ); + break; + case "zm_stalingrad": + // Gorod Krovi — FOUR quests, all solo-canonical: + // Love and War (main): 8 steps from zm_stalingrad_ee_main.gsc + + // zm_stalingrad_gauntlet.gsc. Terminal is a hashed notify + // (#"hash_6460283a") fired inside ee_outro() — no named flag + // exists for "EE complete" on this map. Real call site: + // zm_stalingrad_nikolai.gsc:114 post-boss-defeat. + // Song "Dead Ended" — 3 vodka bottles → "dead_ended" state. + // Song "Ace of Spades" — playing cards → "ace_of_spades" state. + // Song "Samantha's Lullaby" — monkey bombs on dragon fires → + // "sam" state (NOT the snd_zhdegg_activate flag used by the + // Chronicles maps — Gorod Krovi uses music-state instead). + level thread WatchT7FlagStep( "generator_on", "t7_gk_power" ); + level thread WatchT7FlagStep( "keys_placed", "t7_gk_keys" ); + level thread WatchT7FlagStep( "dragon_egg_acquired", "t7_gk_egg" ); + // (gauntlet step moved to standalone "Dragon Gauntlet" quest below) + level thread WatchT7FlagStep( "weapon_cores_delivered", "t7_gk_cores" ); + level thread WatchT7FlagStep( "sophia_escaped", "t7_gk_sophia" ); + level thread WatchT7FlagStep( "players_in_arena", "t7_gk_arena" ); + level thread WatchGorodKroviComplete(); + // Siegfried's Gauntlet upgrade chain — standalone quest. Replaces + // the prior single t7_gk_gauntlet step in Love and War (acquisition + // → 4 upgrade steps → quest complete). Same convention as + // zm_island KT-4/Skull being tracked separately from Seeds of Doubt. + level thread WatchT7FlagStep( "dragon_gauntlet_acquired", "t7_gk_gauntlet_acquired" ); + level thread WatchT7FlagStep( "gauntlet_step_2_complete", "t7_gk_gauntlet_step_2" ); + level thread WatchT7FlagStep( "gauntlet_step_3_complete", "t7_gk_gauntlet_step_3" ); + level thread WatchT7FlagStep( "gauntlet_step_4_complete", "t7_gk_gauntlet_step_4" ); + level thread WatchT7FlagStep( "gauntlet_quest_complete", "t7_gk_gauntlet_complete" ); + level thread WatchT7MusicStateStep( "dead_ended", "t7_gk_song_deadended" ); + level thread WatchT7MusicStateStep( "ace_of_spades", "t7_gk_song_ace" ); + level thread WatchT7MusicStateStep( "sam", "t7_gk_song_sam" ); + break; + case "zm_genesis": + // Revelations — TWO quests, solo-canonical: + // For The Good of All (main): 13 step flags spanning the + // character-graves opener through the Shadowman outro. + // Terminal "ending_room" is set by zm_genesis_arena.gsc:5489 + // after the Shadowman defeat cutscene begins — distinct from + // the level's #"end_game" notify (fired at ee_quest:2414 a + // few seconds later, which is also our level endon). + // Song "The Gift": 3 teddy bears across the map → music state + // "the_gift" via zm_genesis_sound.gsc:517. Overlaps the main + // quest's grand_tour/toys_collected steps (the same teddies + // drive both); tracked separately via music-state poll. + level thread WatchT7FlagStep( "character_stones_done", "t7_rv_stones" ); + level thread WatchT7FlagStep( "placed_audio3", "t7_rv_audio" ); + level thread WatchT7FlagStep( "b_targets_collected", "t7_rv_targets" ); + level thread WatchT7FlagStep( "acm_done", "t7_rv_corruption" ); + level thread WatchT7FlagStep( "shards_done", "t7_rv_shards" ); + level thread WatchT7FlagStep( "sophia_activated", "t7_rv_sophia" ); + level thread WatchT7FlagStep( "sophia_at_teleporter", "t7_rv_sophia_teleporter" ); + level thread WatchT7FlagStep( "book_picked_up", "t7_rv_book" ); + level thread WatchT7FlagStep( "book_runes_success", "t7_rv_runes" ); + level thread WatchT7FlagStep( "grand_tour", "t7_rv_grand_tour" ); + level thread WatchT7FlagStep( "toys_collected", "t7_rv_toys" ); + level thread WatchT7FlagStep( "boss_fight", "t7_rv_boss" ); + level thread WatchT7FlagStep( "ending_room", "t7_rv_complete" ); + level thread WatchT7MusicStateStep( "the_gift", "t7_rv_song" ); + level thread WatchT7FlagStep( "lil_arnie_prereq_done", "t7_rv_arnie_prereq" ); + level thread WatchT7FlagStep( "lil_arnie_done", "t7_rv_arnie_done" ); + break; + case "zm_prototype": + // Nacht der Untoten — two EEs: + // Song "Undone" (returning W@W) — destroy all explodable barrels. + // Engine fires sndmusicsystem_playstate("undone") at the end of + // zm_prototype_barrels.gsc:289 after counting `hash_83cc4809` + // notifies once per barrel destroyed. + // Hide-and-Seek "Samantha's Lullaby" (NEW Chronicles) — 4 hidden + // buttons → snd_zhdegg_activate flag set by zm_prototype.gsc:659. + // The post-flag doll-hunt phase has no server-visible state we + // can hook; flag completion is our terminal. + level thread WatchT7MusicStateStep( "undone", "t7_nu_song" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_nu_hns" ); + break; + case "zm_asylum": + // Verruckt — two EEs: + // Song "Lullaby for a Dead Man" (returning W@W) — flush the + // leftmost upstairs toilet 3 times. Engine plays state + // "lullaby_for_a_dead_man" at zm_asylum.gsc:650. + // Hide-and-Seek "Samantha's Sorrow" (NEW Chronicles) — 3 toilets + // flushed in 935 pattern (right 9 / middle 3 / left 5) → + // snd_zhdegg_activate set by zm_asylum.gsc:1512. + level thread WatchT7MusicStateStep( "lullaby_for_a_dead_man", "t7_vr_song" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_vr_hns" ); + break; + case "zm_sumpf": + // Shi No Numa — two EEs: + // Song "The One" (returning W@W) — phone in Comm Room interacted + // 4 times (community guide says "3" but engine counts 4 — final + // iteration plays the answer audio). State "the_one" at + // zm_sumpf.gsc:732. + // Hide-and-Seek "Samantha's Sorrow" (NEW Chronicles) — 4 metal + // pans in Fishing Hut shot with starting pistol → flag set by + // zm_sumpf.gsc:1268. + level thread WatchT7MusicStateStep( "the_one", "t7_sh_song" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_sh_hns" ); + break; + case "zm_cosmodrome": + // Ascension — FOUR quests: + // Casimir Mechanism (main, MinPlayers=4): 7 named flag steps + // from zm_cosmodrome_eggs.gsc. Terminal "weapons_combined" + // fires when Thundergun + Ray Gun + Nesting Dolls combined + // damage exceeds threshold → triggers soul_release cutscene. + // thundergun_hit (line 1095) is a precondition gate inside + // step 7, not its own step. + // Song "Abracadavre" — 3 teddy bears → snd_song_completed → + // music state "abracadavre" (amb.gsc:226). + // Song "Not Ready to Die" (A7X) — all egg_phone targets meleed + // → music state "not_ready_to_die" (amb.gsc:252). + // Samantha's HnS (Chronicles addition) — snd_zhdegg_activate + // set by zm_cosmodrome_amb.gsc:563. + level thread WatchT7FlagStep( "target_teleported", "t7_as_gersh" ); + level thread WatchT7FlagStep( "rerouted_power", "t7_as_monkey" ); + level thread WatchT7FlagStep( "switches_synced", "t7_as_clock" ); + level thread WatchT7FlagStep( "pressure_sustained", "t7_as_luna" ); + level thread WatchT7FlagStep( "letter_acquired", "t7_as_letter" ); + level thread WatchT7FlagStep( "passkey_confirmed", "t7_as_passkey" ); + level thread WatchT7FlagStep( "weapons_combined", "t7_as_complete" ); + level thread WatchT7MusicStateStep( "abracadavre", "t7_as_song_abracadavre" ); + level thread WatchT7MusicStateStep( "not_ready_to_die", "t7_as_song_nrtd" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_as_hns" ); + break; + case "zm_theater": + // Kino der Toten — TWO quests, no main EE (W@W/BO1 never had one, + // Chronicles didn't add one): + // Song "115" — 3 meteor rocks shot → music state "115" at + // zm_theater_amb.gsc:224. + // Samantha Doll HnS (Chronicles addition) — knocking door + // pattern echoed → first doll → 5 hidden dolls shot → + // return to original. snd_zhdegg_activate flag set by + // zm_theater_amb.gsc:483 on terminal step. + level thread WatchT7MusicStateStep( "115", "t7_kn_song" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_kn_hns" ); + break; + case "zm_moon": + // Moon — FOUR quests: + // Richtofen's Grand Scheme (main): combines Cryogenic Slumber + // Party (CSP, steps 1-4) + Big Bang Theory (BBT, steps 5-7). + // Chronicles solo-canonical (BO1 BBT was co-op-only; + // Chronicles unlocked solo). + // Terminal "complete_be_1" set by sq_be.gsc:501 inside the + // final BBT validation switch. + // Song "Coming Home" — 3 PES-helmet teddy bears, music state + // "cominghome" (amb.gsc:534). + // Song "Nightmare" — excavator-suicide co-op trigger, music + // state "nightmare" (achievement.gsc:156). Solo-impossible. + // Samantha's Journey HnS (Chronicles addition) — spinning + // Samantha doll on 115 box → snd_zhdegg_activate (amb.gsc:839). + // 8-Bit Coming Home / 8-Bit Pareidolia minor variants skipped — + // dynamic state names via self.script_string not enumerable. + level thread WatchT7FlagStep( "first_tanks_charged", "t7_mn_tanks" ); + level thread WatchT7FlagStep( "c_built", "t7_mn_capsule" ); + level thread WatchT7FlagStep( "vg_charged", "t7_mn_vril" ); + level thread WatchT7FlagStep( "soul_swap_done", "t7_mn_csp" ); + level thread WatchT7FlagStep( "sam_switch_thrown", "t7_mn_samswitch" ); + level thread WatchT7FlagStep( "be2", "t7_mn_bbt_mid" ); + level thread WatchT7FlagStep( "complete_be_1", "t7_mn_complete" ); + level thread WatchT7MusicStateStep( "cominghome", "t7_mn_song_cominghome" ); + level thread WatchT7MusicStateStep( "nightmare", "t7_mn_song_nightmare" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_mn_hns" ); + break; + case "zm_temple": + // Shangri-La — THREE quests: + // Time Travel Will Tell (main, MinPlayers=4): 5 named flag + // steps drawn from zm_temple_sq*.gsc files. Eclipse activation + // step requires 4 simultaneous button presses at Quick Revive + // — same 4P gate as Casimir. Terminal "meteorite_shrunk" + // fires when shrink-ray + dynamite chain rewards Focus Stone. + // Song "Pareidolia" — 3 element-115 rocks → music state + // "pareidolia" (amb.gsc:137). + // Samantha's HnS (Chronicles addition) — snd_zhdegg_activate + // set by zm_temple_amb.gsc:244. + // Intermediate flags pap_override / radio_7 / radio_9 are sub- + // steps covered by neighboring main steps — not tracked. + level thread WatchT7FlagStep( "trap_destroyed", "t7_sl_trap" ); + level thread WatchT7FlagStep( "radio_4_played", "t7_sl_explorers" ); + level thread WatchT7FlagStep( "gongs_resonating", "t7_sl_gongs" ); + level thread WatchT7FlagStep( "given_dynamite", "t7_sl_dynamite" ); + level thread WatchT7FlagStep( "meteorite_shrunk", "t7_sl_complete" ); + level thread WatchT7MusicStateStep( "pareidolia", "t7_sl_song" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_sl_hns" ); + break; + case "zm_tomb": + // Origins — NINE quests, solo-canonical: + // Little Lost Girl (main): 11 named flag steps spanning + // staff-craft → upgrade → place → quadrotor → mech fight → + // Maxis Drone → One Inch Punch → souls absorbed → portal → + // Samantha released (terminal). + // Note: ee_quadrotor_disabled is set at step_4 (first Panzer + // down), cleared at step_5, re-set at step_8. Our wait_till + // fires once on first set = step_4 timing. The final boss + // completion is covered by ee_samantha_released. + // 4 Elemental Staff Upgrades — Kagutsuchi's Blood (fire), + // Ull's Arrow (water/ice), Boreas' Fury (air/wind), Kimat's + // Bite (lightning). Per-staff upgrade-unlock flags are + // hashed in the dump (same as zm_castle bows); detect via + // weapon-inventory poll on staff__upgraded. + // 4 Song EEs — Archangel (3 records → "archangel"), + // Aether (3 mus115 prone-triggers → "aether"), + // Shepherd of Fire (3 radios → "shepherd_of_fire"), + // Samantha's Lullaby (Chronicles HnS — snd_zhdegg_activate + // set by zm_tomb_amb.gsc:635 after 4 elemental targets shot). + level thread WatchT7FlagStep( "ee_all_staffs_crafted", "t7_or_staffs_crafted" ); + level thread WatchT7FlagStep( "ee_all_staffs_upgraded", "t7_or_staffs_upgraded" ); + level thread WatchT7FlagStep( "ee_all_staffs_placed", "t7_or_staffs_placed" ); + level thread WatchT7FlagStep( "ee_mech_zombie_hole_opened", "t7_or_mech_hole" ); + level thread WatchT7FlagStep( "ee_quadrotor_disabled", "t7_or_quadrotor" ); + level thread WatchT7FlagStep( "ee_mech_zombie_fight_completed", "t7_or_mech_fight" ); + level thread WatchT7FlagStep( "ee_maxis_drone_retrieved", "t7_or_drone" ); + level thread WatchT7FlagStep( "ee_all_players_upgraded_punch", "t7_or_punch" ); + level thread WatchT7FlagStep( "ee_souls_absorbed", "t7_or_souls" ); + level thread WatchT7FlagStep( "ee_sam_portal_active", "t7_or_portal" ); + level thread WatchT7FlagStep( "ee_samantha_released", "t7_or_complete" ); + level thread WatchWeaponSubstringUpgrade( "staff_fire_upgraded", "t7_or_staff_fire" ); + level thread WatchWeaponSubstringUpgrade( "staff_water_upgraded", "t7_or_staff_ice" ); + level thread WatchWeaponSubstringUpgrade( "staff_air_upgraded", "t7_or_staff_wind" ); + level thread WatchWeaponSubstringUpgrade( "staff_lightning_upgraded", "t7_or_staff_lightning" ); + level thread WatchT7MusicStateStep( "archangel", "t7_or_song_archangel" ); + level thread WatchT7MusicStateStep( "aether", "t7_or_song_aether" ); + level thread WatchT7MusicStateStep( "shepherd_of_fire", "t7_or_song_shepherd" ); + level thread WatchT7FlagStep( "snd_zhdegg_activate", "t7_or_hns" ); + break; + default: return; + } + + logprint( "[ZM-EE] Per-step watchers armed for map=" + level.script + "\n" ); +} + +// Generic helper: wait for a level flag to fire, emit a step. Used across the +// T7 EE watchers — most steps are flag::set fires. +// +// flag::wait_till crashes ("cannot cast undefined to bool") on flags that the +// map script hasn't called flag::init() on yet. Our EE threads start at map +// init time, before the map's per-EE code arms its flags. Poll flag::exists +// until the flag is created, then wait_till as normal. +function WatchT7FlagStep( flagName, stepKey ) +{ + level endon( "end_game" ); + WaitForT7FlagInit( flagName ); + level flag::wait_till( flagName ); + EmitEeStep( stepKey ); +} + +function WaitForT7FlagInit( flagName ) +{ + level endon( "end_game" ); + while ( !( level flag::exists( flagName ) ) ) + { + wait ( 0.5 ); + } +} + +// Poll the shared zm_audio music state. Emits stepKey on the first observation +// of level.musicsystem.currentstate matching stateName. Used for song EEs where +// the only clean terminal signal is the engine calling sndmusicsystem_playstate +// (_zm_audio.gsc:1342 — sets m.currentstate to the requested state). +function WatchT7MusicStateStep( stateName, stepKey ) +{ + level endon( "end_game" ); + for ( ;; ) + { + if ( IsDefined( level.musicsystem ) + && IsDefined( level.musicsystem.currentstate ) + && level.musicsystem.currentstate == stateName ) + { + EmitEeStep( stepKey ); + return; + } + wait ( 0.5 ); + } +} + +// Generic per-player weapon-inventory poller. Fires stepKey when any live +// player's inventory contains a weapon name matching weaponSubstr. +// +// Used for any wonder-weapon-upgrade quest whose upgrade-complete flag is +// hashed in the decompiled dump (no source name available). Substring match +// tolerates engine variant suffixes / attachments. Current consumers: +// - Der Eisendrache elemental bows (storm/wolf/fire/void) +// - Origins elemental staffs (fire/ice/wind/lightning) +// - SoE Shield + Bouncing Betty upgrades +function WatchWeaponSubstringUpgrade( weaponSubstr, stepKey ) +{ + level endon( "end_game" ); + for ( ;; ) + { + players = getplayers(); + for ( i = 0; i < players.size; i++ ) + { + if ( !IsAlive( players[i] ) ) { continue; } + if ( PlayerHasWeaponSubstr( players[i], weaponSubstr ) ) + { + EmitEeStep( stepKey ); + return; + } + } + wait ( 2 ); + } +} + +function PlayerHasWeaponSubstr( player, weaponSubstr ) +{ + weapons = player getweaponslist(); + if ( !IsDefined( weapons ) ) { return false; } + for ( w = 0; w < weapons.size; w++ ) + { + if ( IsDefined( weapons[w].name ) && IsSubStr( weapons[w].name, weaponSubstr ) ) + { + return true; + } + } + return false; +} + +// Gorod Krovi "Love and War" terminal. zm_stalingrad_ee_main.gsc has no named +// "EE complete" flag; the only deterministic signal is the hashed level notify +// #"hash_6460283a" fired at the very top of ee_outro() (line 4413). The real +// production call site for ee_outro is zm_stalingrad_nikolai.gsc:114 — fired +// from function_a21082e5() which runs after the Mother Dragon is killed. +// BO3 is end-of-life so the compile-time hash is frozen; hash-literal waittill +// is robust. +function WatchGorodKroviComplete() +{ + level endon( "end_game" ); + level waittill( #"hash_6460283a" ); + EmitEeStep( "t7_gk_complete" ); +} + +// Shadows of Evil Upgraded Li'l Arnies terminal. zm_zod_ee_side.gsc:1481 fires +// level notify(#"hash_21edb6b6") at the end of function_c4842cb1, the stage- +// dance cutscene that runs after the player completes the placement chain +// (top hat / cane / bow tie + Black Lace stage). No named flag exists for +// this EE's terminal; hash-literal waittill is the only deterministic signal. +function WatchSoEArnieUpgrade() +{ + level endon( "end_game" ); + level waittill( #"hash_21edb6b6" ); + EmitEeStep( "t7_soe_arnie_upgrade" ); +} + +//---- Factory (The Giant) EE watchers ----// + +// Music EE — "Beauty of Annihilation Remix". 3 brain jars; engine wires them +// via zm_audio::sndmusicsystem_eesetup (zm_factory.gsc:2220 → _zm_audio.gsc:1597). +// Each "use" hit increments level.sndeecount; song fires at == sndeemax. +// No per-jar notify is reachable from outside zm_audio's temp_ent scope, so +// poll the counter and emit step transitions. +function WatchFactoryMusic() +{ + level endon( "end_game" ); + + while ( !IsDefined( level.sndeemax ) || level.sndeemax == 0 ) + { + wait ( 0.5 ); + } + + lastCount = 0; + for ( ;; ) + { + if ( IsDefined( level.sndeecount ) && level.sndeecount > lastCount ) + { + for ( i = lastCount + 1; i <= level.sndeecount; i++ ) + { + EmitEeStep( "t7_fa_song_" + i ); + } + lastCount = level.sndeecount; + if ( lastCount >= level.sndeemax ) + { + return; + } + } + wait ( 0.25 ); + } +} + +// Flytrap / Hide and Go Seek — main EE giving the Annihilator wonder weapon. +// 4 steps: panel hit with PaP'd weapon → 3 hidden targets (order undetermined, +// emit by first-fire index). +// References: zm_factory.gsc:1316 ("flytrap" flag), :1321-1323 (target flags). +function WatchFactoryFlytrap() +{ + level endon( "end_game" ); + + level thread WatchT7FlagStep( "flytrap", "t7_fa_flytrap_panel" ); + + level.iw4m_flytrap_target_idx = 0; + level thread WatchFactoryFlytrapTarget( "ee_exp_monkey" ); + level thread WatchFactoryFlytrapTarget( "ee_bowie_bear" ); + level thread WatchFactoryFlytrapTarget( "ee_perk_bear" ); +} + +function WatchFactoryFlytrapTarget( flagName ) +{ + level endon( "end_game" ); + WaitForT7FlagInit( flagName ); + level flag::wait_till( flagName ); + // GSC cooperative scheduling means inc+read is atomic between waits — no + // tearing even if two targets fire in the same frame. + level.iw4m_flytrap_target_idx = level.iw4m_flytrap_target_idx + 1; + EmitEeStep( "t7_fa_flytrap_target_" + level.iw4m_flytrap_target_idx ); +} + +// Secret Perk (6th Perk-A-Cola). 3 cymbal-monkey-on-pad steps + the snow-melt +// reveal. References: zm_factory.gsc:2819/2837/2855 (console_*_completed), +// :2700 (snow_ee_completed). +function WatchFactorySecretPerk() +{ + level endon( "end_game" ); + + level.iw4m_perk_pad_idx = 0; + level thread WatchFactorySecretPerkPad( "console_one_completed" ); + level thread WatchFactorySecretPerkPad( "console_two_completed" ); + level thread WatchFactorySecretPerkPad( "console_three_completed" ); + level thread WatchT7FlagStep( "snow_ee_completed", "t7_fa_perk_done" ); +} + +function WatchFactorySecretPerkPad( flagName ) +{ + level endon( "end_game" ); + WaitForT7FlagInit( flagName ); + level flag::wait_till( flagName ); + level.iw4m_perk_pad_idx = level.iw4m_perk_pad_idx + 1; + EmitEeStep( "t7_fa_perk_pad_" + level.iw4m_perk_pad_idx ); +} + +function EmitEeStep( stepKey ) +{ + if ( !IsDefined( level.iw4m_ee_steps_fired ) ) + { + level.iw4m_ee_steps_fired = []; + } + if ( IsDefined( level.iw4m_ee_steps_fired[ stepKey ] ) ) + { + return; + } + level.iw4m_ee_steps_fired[ stepKey ] = 1; + + roundStr = "?"; + if ( IsDefined( level.round_number ) ) { roundStr = "" + level.round_number; } + logprint( "[ZM-EE] Step fired: " + stepKey + " round=" + roundStr + "\n" ); + logprint( "GSE;ZW;easter_egg;step;" + stepKey + "\n" ); +} diff --git a/GameFiles/deploy.bat b/GameFiles/deploy.bat index 8b804506e..04082bd8e 100644 --- a/GameFiles/deploy.bat +++ b/GameFiles/deploy.bat @@ -1,21 +1,31 @@ @echo off +ECHO "Pluto T4" +xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts" +xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts" +xcopy /y .\GameInterface\_integration_utility.gsh "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts" +xcopy /y .\GameInterface\_integration_t4.gsc "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts\mp" +xcopy /y .\GameInterface\_integration_t4zm.gsc "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts\sp" +xcopy /y .\ZombieStats\_zm_stats_t4.gsc "%LOCALAPPDATA%\Plutonium\storage\t4\raw\scripts\sp" + ECHO "Pluto IW5" -xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" -xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" -xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" -xcopy /y .\GameInterface\_integration_utility.gsh "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" -xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp" +xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" +xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" +xcopy /y .\GameInterface\_integration_utility.gsh "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" +xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts" +xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp" ECHO "Pluto T5" -xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts" -xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts" -xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp" -xcopy /y .\GameInterface\_integration_t5zm.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom" +xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts" +xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts" +xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp" +xcopy /y .\GameInterface\_integration_t5zm.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom" +xcopy /y .\ZombieStats\_zm_stats_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom" ECHO "Pluto T6" -xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" -xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" -xcopy /y .\GameInterface\_integration_t6.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" -xcopy /y .\GameInterface\_integration_t6zm_helper.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm" -xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp" +xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" +xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" +xcopy /y .\GameInterface\_integration_t6.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts" +xcopy /y .\GameInterface\_integration_t6zm_helper.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm" +xcopy /y .\ZombieStats\_zm_stats_t6.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm" +xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp" diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index d6d2693e8..617066751 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29009.5 @@ -79,6 +80,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameFiles", "GameFiles", "{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}" ProjectSection(SolutionItems) = preProject GameFiles\deploy.bat = GameFiles\deploy.bat + GameFiles\README.MD = GameFiles\README.MD EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterface", "{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55}" @@ -125,6 +127,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GithubActions", "GithubActi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginDebugReference", "PluginDebugReference\PluginDebugReference.csproj", "{2EB30B9B-FEC4-4A30-A955-515D55D0555B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZombieStats", "Plugins\ZombieStats\ZombieStats.csproj", "{484E0792-1684-4B05-8BC4-24E2307B9EE7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ZombieStats", "ZombieStats", "{033B7724-0EFD-428E-AF15-A5DA74B35DAA}" + ProjectSection(SolutionItems) = preProject + GameFiles\ZombieStats\_zm_stats_t4.gsc = GameFiles\ZombieStats\_zm_stats_t4.gsc + GameFiles\ZombieStats\_zm_stats_t5.gsc = GameFiles\ZombieStats\_zm_stats_t5.gsc + GameFiles\ZombieStats\_zm_stats_t6.gsc = GameFiles\ZombieStats\_zm_stats_t6.gsc + GameFiles\ZombieStats\_zm_stats_t7.gsc = GameFiles\ZombieStats\_zm_stats_t7.gsc + GameFiles\ZombieStats\FEATURE_MATRIX.md = GameFiles\ZombieStats\FEATURE_MATRIX.md + GameFiles\ZombieStats\README.md = GameFiles\ZombieStats\README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -365,6 +379,8 @@ Global {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Debug|x64.Build.0 = Debug|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Debug|x86.ActiveCfg = Debug|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Debug|x86.Build.0 = Debug|Any CPU + {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -379,8 +395,6 @@ Global {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Release|x64.Build.0 = Release|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Release|x86.ActiveCfg = Release|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Release|x86.Build.0 = Release|Any CPU - {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU - {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -389,6 +403,8 @@ Global {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x64.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.ActiveCfg = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -403,8 +419,6 @@ Global {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x64.Build.0 = Release|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.ActiveCfg = Release|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.Build.0 = Release|Any CPU - {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU - {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -413,6 +427,8 @@ Global {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x64.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -427,8 +443,6 @@ Global {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x64.Build.0 = Release|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.ActiveCfg = Release|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU - {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU - {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.Build.0 = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -437,6 +451,8 @@ Global {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x64.Build.0 = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.ActiveCfg = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.Build.0 = Debug|Any CPU + {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -451,8 +467,6 @@ Global {259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x64.Build.0 = Release|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.ActiveCfg = Release|Any CPU {259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.Build.0 = Release|Any CPU - {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU - {259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -477,6 +491,30 @@ Global {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Release|x64.Build.0 = Release|Any CPU {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Release|x86.ActiveCfg = Release|Any CPU {2EB30B9B-FEC4-4A30-A955-515D55D0555B}.Release|x86.Build.0 = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|x64.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Debug|x86.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|Any CPU.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|x64.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Prerelease|x86.Build.0 = Debug|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|Any CPU.Build.0 = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|x64.ActiveCfg = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|x64.Build.0 = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|x86.ActiveCfg = Release|Any CPU + {484E0792-1684-4B05-8BC4-24E2307B9EE7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -499,6 +537,8 @@ Global {866F453D-BC89-457F-8B55-485494759B31} = {AB83BAC0-C539-424A-BF00-78487C10753C} {603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C} {DCCEED9F-816E-4595-8B74-D76A77FBE0BE} = {8C8F3945-0AEF-4949-A1F7-B18E952E50BC} + {484E0792-1684-4B05-8BC4-24E2307B9EE7} = {26E8B310-269E-46D4-A612-24601F16065F} + {033B7724-0EFD-428E-AF15-A5DA74B35DAA} = {6CBF412C-EFEE-45F7-80FD-AC402C22CDB9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} diff --git a/Integrations/Cod/CodRConConnection.cs b/Integrations/Cod/CodRConConnection.cs index b2827c6eb..2af194259 100644 --- a/Integrations/Cod/CodRConConnection.cs +++ b/Integrations/Cod/CodRConConnection.cs @@ -58,11 +58,11 @@ public void SetConfiguration(IRConParser parser) } public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", - CancellationToken token = default) + CancellationToken token = default, Action onPacketSent = null) { try { - return await SendQueryAsyncInternal(type, parameters, token); + return await SendQueryAsyncInternal(type, parameters, token, onPacketSent); } catch (RConException ex) when (ex.IsOperationCancelled) { @@ -97,7 +97,7 @@ public async Task SendQueryAsync(StaticHelpers.QueryType type, string } private async Task SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", - CancellationToken token = default) + CancellationToken token = default, Action onPacketSent = null) { if (!ActiveQueries.ContainsKey(Endpoint)) { @@ -241,9 +241,17 @@ string ConvertEncoding(string text) if (connectionState.ConnectionAttempts > 1) { + var retryLogLevel = connectionState.ConnectionAttempts switch + { + <= 4 => LogLevel.Debug, + <= 7 => LogLevel.Information, + <= 9 => LogLevel.Warning, + _ => LogLevel.Error + }; + using (LogContext.PushProperty("Server", Endpoint.ToString())) { - _log.LogInformation( + _log.Log(retryLogLevel, "Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts, {Timeout}ms timeout) with parameters {Payload}", connectionState.ConnectionAttempts, _retryAttempts, maxTimeout.TotalMilliseconds, parameters); @@ -252,7 +260,7 @@ string ConvertEncoding(string text) waitForResponse = waitForResponse && overrideTimeout.HasValue; var rttStopwatch = Stopwatch.StartNew(); - response = await SendPayloadAsync(socket, payload, waitForResponse, chainedTokenSource.Token); + response = await SendPayloadAsync(socket, payload, waitForResponse, chainedTokenSource.Token, onPacketSent); rttStopwatch.Stop(); if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse) @@ -357,7 +365,7 @@ private byte[] BuildPayload(string queryTemplate, string convertedRConPassword, } private async Task SendPayloadAsync(Socket rconSocket, byte[] payload, bool waitForResponse, - CancellationToken token = default) + CancellationToken token = default, Action onPacketSent = null) { var connectionState = ActiveQueries[Endpoint]; @@ -382,6 +390,8 @@ private async Task SendPayloadAsync(Socket rconSocket, byte[] payload, throw new NetworkException("Could not send data to remote RCon socket", rconSocket); } + onPacketSent?.Invoke(DateTime.UtcNow); + if (!waitForResponse) { return []; diff --git a/Integrations/Source/SourceRConConnection.cs b/Integrations/Source/SourceRConConnection.cs index b4e48f327..7bdc19ab9 100644 --- a/Integrations/Source/SourceRConConnection.cs +++ b/Integrations/Source/SourceRConConnection.cs @@ -61,7 +61,7 @@ public void Dispose() _logger.LogDebug("Disposed Source RCon connection for {Endpoint}", _ipEndPoint); } - public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default) + public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default, Action onPacketSent = null) { try { diff --git a/Plugins/Mute/Plugin.cs b/Plugins/Mute/Plugin.cs index 67fe2af91..b3e4a1d3a 100644 --- a/Plugins/Mute/Plugin.cs +++ b/Plugins/Mute/Plugin.cs @@ -257,7 +257,7 @@ private InteractionData CreateMuteInteraction(int targetClientId, Server server, { if (!targetId.HasValue) { - return "No target client id specified"; + return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_ACTION_NO_TARGET_ID"]; } var isTempMute = meta.ContainsKey(durationInput.Name) && @@ -328,7 +328,7 @@ private InteractionData CreateUnmuteInteraction(int targetClientId, Server serve { if (!targetId.HasValue) { - return "No target client id specified"; + return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_ACTION_NO_TARGET_ID"]; } var args = new List(); diff --git a/Plugins/ScriptPlugins/GameInterface.cs b/Plugins/ScriptPlugins/GameInterface.cs index ea648d4e1..916d58776 100644 --- a/Plugins/ScriptPlugins/GameInterface.cs +++ b/Plugins/ScriptPlugins/GameInterface.cs @@ -665,7 +665,7 @@ private bool ValidateEnabled(IGameServer server, EFClient origin) { var enabled = _state.GetServerState(server.Id) is { Enabled: true }; if (!enabled) - origin.Tell("Game interface is not enabled on this server"); + origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_GAMEINTERFACE_NOT_ENABLED"]); return enabled; } @@ -834,7 +834,7 @@ protected bool ValidateEnabled(GameEvent gameEvent) ArgumentNullException.ThrowIfNull(gameEvent); var enabled = state.GetServerState(gameEvent.Owner.Id) is { Enabled: true }; if (!enabled) - gameEvent.Origin.Tell("Game interface is not enabled on this server"); + gameEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_GAMEINTERFACE_NOT_ENABLED"]); return enabled; } diff --git a/Plugins/ScriptPlugins/SubnetBan.cs b/Plugins/ScriptPlugins/SubnetBan.cs index c321c9f7c..16b7a652a 100644 --- a/Plugins/ScriptPlugins/SubnetBan.cs +++ b/Plugins/ScriptPlugins/SubnetBan.cs @@ -72,8 +72,9 @@ private void RegisterInteraction() { var interaction = new InteractionData { - Name = "Subnet Banlist", - Description = $"List of banned subnets ({_config.SubnetBanList.Count} Total)", + Name = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_INTERACTION_TITLE"], + Description = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_INTERACTION_DESC"] + .FormatExt(_config.SubnetBanList.Count), DisplayMeta = "ph-x-circle", InteractionId = SubnetBanlistKey, MinimumPermission = EFClient.Permission.Moderator, @@ -89,13 +90,13 @@ private void RegisterInteraction() { { "InteractionId", "command" }, { "Data", "unbansubnet" }, - { "ActionButtonLabel", "Unban" }, - { "Name", "Unban Subnet" } + { "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_UNBAN_BUTTON"] }, + { "Name", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_UNBAN_LABEL"] } }; if (_config.SubnetBanList.Count == 0) { - table += "No subnets are banned."; + table += $"{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_EMPTY_TABLE"]}"; } else { @@ -104,6 +105,7 @@ private void RegisterInteraction() unbanSubnetInteraction["Data"] = "unbansubnet " + subnet; var encodedMeta = Uri.EscapeDataString(System.Text.Json.JsonSerializer.Serialize(unbanSubnetInteraction)); + var unbanSubnetLabel = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_SUBNETBAN_UNBAN_LABEL"]; table += $@" {subnet} @@ -113,7 +115,7 @@ private void RegisterInteraction() data-action-meta=""{encodedMeta}"">
- Unban Subnet + {unbanSubnetLabel}
@@ -208,7 +210,7 @@ public BanSubnetCommand(CommandConfiguration config, ITranslationLookup translat : base(config, translationLookup) { Name = "bansubnet"; - Description = "bans an IPv4 subnet"; + Description = translationLookup["PLUGINS_SUBNETBAN_COMMANDS_BAN_DESC"]; Alias = "bs"; Permission = EFClient.Permission.SeniorAdmin; RequiresTarget = false; @@ -222,14 +224,14 @@ public override async Task ExecuteAsync(GameEvent gameEvent) if (!IsValidCidr(input)) { - gameEvent.Origin.Tell("Invalid CIDR input"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_INVALID_CIDR"]); return; } // Check if already banned if (_config.SubnetBanList.Contains(input)) { - gameEvent.Origin.Tell($"Subnet {input} is already banned"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_ALREADY_BANNED"].FormatExt(input)); return; } @@ -239,7 +241,7 @@ public override async Task ExecuteAsync(GameEvent gameEvent) // Save configuration to disk await _configHandler.Set(_config); - gameEvent.Origin.Tell($"Added {input} to subnet banlist"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_BAN_SUCCESS"].FormatExt(input)); } private static bool IsValidCidr(string input) @@ -262,7 +264,7 @@ public UnbanSubnetCommand(CommandConfiguration config, ITranslationLookup transl : base(config, translationLookup) { Name = "unbansubnet"; - Description = "unbans an IPv4 subnet"; + Description = translationLookup["PLUGINS_SUBNETBAN_COMMANDS_UNBAN_DESC"]; Alias = "ubs"; Permission = EFClient.Permission.SeniorAdmin; RequiresTarget = false; @@ -276,13 +278,13 @@ public override async Task ExecuteAsync(GameEvent gameEvent) if (!IsValidCidr(input)) { - gameEvent.Origin.Tell("Invalid CIDR input"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_INVALID_CIDR"]); return; } if (!_config.SubnetBanList.Contains(input)) { - gameEvent.Origin.Tell("Subnet is not banned"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_NOT_BANNED"]); return; } @@ -292,7 +294,7 @@ public override async Task ExecuteAsync(GameEvent gameEvent) // Save configuration to disk await _configHandler.Set(_config); - gameEvent.Origin.Tell($"Removed {input} from subnet banlist"); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_SUBNETBAN_COMMANDS_UNBAN_SUCCESS"].FormatExt(input)); } private static bool IsValidCidr(string input) diff --git a/Plugins/Stats/Client/Abstractions/IServerDistributionCalculator.cs b/Plugins/Stats/Client/Abstractions/IServerDistributionCalculator.cs index 9630c1232..a3ebbdc71 100644 --- a/Plugins/Stats/Client/Abstractions/IServerDistributionCalculator.cs +++ b/Plugins/Stats/Client/Abstractions/IServerDistributionCalculator.cs @@ -5,7 +5,7 @@ namespace Stats.Client.Abstractions public interface IServerDistributionCalculator { Task Initialize(); - Task GetZScoreForServer(long serverId, double value); - Task GetRatingForZScore(double? value); + Task GetZScoreForServerOrBucket(double value, long? serverId = null, string performanceBucket = null); + Task GetRatingForZScore(double? value, string performanceBucket); } -} \ No newline at end of file +} diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index dc1cd51ee..f10648dc0 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -17,9 +17,13 @@ using SharedLibraryCore.Database.Models; using SharedLibraryCore.Events; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; + using SharedLibraryCore.Events.Management; +using SharedLibraryCore.Helpers; using Stats.Client.Abstractions; using Stats.Client.Game; +using Stats.Config; namespace IW4MAdmin.Plugins.Stats.Client; @@ -115,6 +119,40 @@ public async Task CalculateForEvent(CoreEvent coreEvent) return; } + if (coreEvent is GameEventV2 { Server: not null } gameEventV2 and (RoundEndEvent or MatchEndEvent)) + { + var server = gameEventV2.Server; + foreach (var client in server.ConnectedClients) + { + if (!_clientHitStatistics.TryGetValue(client.ClientId, out var state)) + { + continue; + } + + try + { + await state.OnTransaction.WaitAsync(); + await UpdateClientStatistics(client.ClientId, state); + } + + catch (Exception ex) + { + _logger.LogError(ex, "Could not handle round end calculations for client {Client}", + client.ToString()); + } + + finally + { + if (state.OnTransaction.CurrentCount == 0) + { + state.OnTransaction.Release(); + } + } + } + + return; + } + if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent) { // Atomically remove the state to prevent race conditions with hit processing @@ -188,9 +226,13 @@ public async Task CalculateForEvent(CoreEvent coreEvent) foreach (var hitInfo in new[] {attackerHitInfo, victimHitInfo}) { - if (hitInfo.MeansOfDeath == null || hitInfo.Location == null || hitInfo.Weapon == null) + if (hitInfo.MeansOfDeath == null || hitInfo.Location == null || hitInfo.Weapon == null || hitInfo.EntityId == 0) { - _logger.LogDebug("Skipping hit because it does not contain the required data"); + _logger.LogDebug("Skipping hit for EntityId={EntityId}: MoD={MeansOfDeath}, Location={Location}, Weapon={Weapon}", + hitInfo.EntityId, + hitInfo.MeansOfDeath ?? "NULL", + hitInfo.Location ?? "NULL", + hitInfo.Weapon?.Name ?? "NULL"); continue; } @@ -281,26 +323,26 @@ await Task.WhenAll(hitInfo.Weapon.Attachments.Select(attachment => var matchingLocation = await GetOrAddHitLocation(hitInfo.Location, hitInfo.Game); var meansOfDeath = await GetOrAddMeansOfDeath(hitInfo.MeansOfDeath, hitInfo.Game); - var baseTasks = new[] - { + List> baseTasks = + [ // just the client - GetOrAddClientHit(hitInfo.EntityId, null), + GetOrAddClientHit(hitInfo.EntityId), // client and server GetOrAddClientHit(hitInfo.EntityId, serverId), // just the location - GetOrAddClientHit(hitInfo.EntityId, null, matchingLocation.HitLocationId), + GetOrAddClientHit(hitInfo.EntityId, hitLocationId: matchingLocation.HitLocationId), // location and server - GetOrAddClientHit(hitInfo.EntityId, serverId, matchingLocation.HitLocationId), + GetOrAddClientHit(hitInfo.EntityId, serverId, hitLocationId: matchingLocation.HitLocationId), // per weapon - GetOrAddClientHit(hitInfo.EntityId, null, null, weapon.WeaponId), + GetOrAddClientHit(hitInfo.EntityId, weaponId: weapon.WeaponId), // per weapon and server - GetOrAddClientHit(hitInfo.EntityId, serverId, null, weapon.WeaponId), + GetOrAddClientHit(hitInfo.EntityId, serverId, weaponId: weapon.WeaponId), // means of death aggregate GetOrAddClientHit(hitInfo.EntityId, meansOfDeathId: meansOfDeath.MeansOfDeathId), // means of death per server aggregate GetOrAddClientHit(hitInfo.EntityId, serverId, meansOfDeathId: meansOfDeath.MeansOfDeathId) - }; + ]; var allTasks = baseTasks.AsEnumerable(); @@ -308,10 +350,10 @@ await Task.WhenAll(hitInfo.Weapon.Attachments.Select(attachment => { allTasks = allTasks // per weapon per attachment combo - .Append(GetOrAddClientHit(hitInfo.EntityId, null, null, - weapon.WeaponId, attachmentCombo.WeaponAttachmentComboId)) - .Append(GetOrAddClientHit(hitInfo.EntityId, serverId, null, - weapon.WeaponId, attachmentCombo.WeaponAttachmentComboId)); + .Append(GetOrAddClientHit(hitInfo.EntityId, + weaponId: weapon.WeaponId, attachmentComboId: attachmentCombo.WeaponAttachmentComboId)) + .Append(GetOrAddClientHit(hitInfo.EntityId, serverId, + weaponId: weapon.WeaponId, attachmentComboId: attachmentCombo.WeaponAttachmentComboId)); } return await Task.WhenAll(allTasks); @@ -365,8 +407,9 @@ private async Task> GetHitsForClient(int clientId) { try { - await using var context = _contextFactory.CreateContext(); + await using var context = _contextFactory.CreateContext(false); var hitLocations = await context.Set() + .AsNoTracking() .Where(stat => stat.ClientId == clientId) .ToListAsync(); @@ -394,7 +437,33 @@ private async Task UpdateClientStatistics(int clientId, HitState locState = null try { await using var context = _contextFactory.CreateContext(); - context.Set().UpdateRange(state.Hits); + + // Clear navigation properties to prevent EF Core from trying to + // insert/update related entities when attaching to this new context + foreach (var hit in state.Hits) + { + hit.Server = null; + hit.HitLocation = null; + hit.Weapon = null; + hit.WeaponAttachmentCombo = null; + hit.MeansOfDeath = null; + hit.PerformanceBucket = null; + hit.Client = null; + } + + var existingHits = state.Hits.Where(h => h.ClientHitStatisticId != 0).ToList(); + var newHits = state.Hits.Where(h => h.ClientHitStatisticId == 0).ToList(); + + if (existingHits.Count > 0) + { + context.Set().UpdateRange(existingHits); + } + + if (newHits.Count > 0) + { + context.Set().AddRange(newHits); + } + await context.SaveChangesAsync(); } @@ -404,7 +473,7 @@ private async Task UpdateClientStatistics(int clientId, HitState locState = null } } - private Task GetOrAddClientHit(int clientId, long? serverId = null, + private Task GetOrAddClientHit(int clientId, long? serverId = null, string performanceBucketCode = null, int? hitLocationId = null, int? weaponId = null, int? attachmentComboId = null, int? meansOfDeathId = null) { @@ -414,19 +483,26 @@ private Task GetOrAddClientHit(int clientId, long? serverI throw new InvalidOperationException($"No hit state found for client {clientId}"); } + // Defence-in-depth: PerformanceBucket.Code on persisted hits is always + // the lower-cased canonical form (the writer in IW4MServer normalises); + // a raw capitalised caller-supplied value would silently never match. + var normalizedBucket = performanceBucketCode is null + ? null + : PerformanceBucketCodes.Normalize(performanceBucketCode); + var hitStat = state.Hits .FirstOrDefault(hit => hit.HitLocationId == hitLocationId && hit.WeaponId == weaponId && hit.WeaponAttachmentComboId == attachmentComboId && hit.MeansOfDeathId == meansOfDeathId - && hit.ServerId == serverId); + && (normalizedBucket is not null && normalizedBucket == hit.PerformanceBucket?.Code || (normalizedBucket is null && hit.ServerId == serverId))); if (hitStat != null) { return Task.FromResult(hitStat); } - hitStat = new EFClientHitStatistic() + hitStat = new EFClientHitStatistic { ClientId = clientId, ServerId = serverId, @@ -442,7 +518,7 @@ private Task GetOrAddClientHit(int clientId, long? serverI } catch (Exception ex) { - _logger.LogError(ex, "Could not add {statsName} for {id}", nameof(EFClientHitStatistic), + _logger.LogError(ex, "Could not add {StatsName} for {Id}", nameof(EFClientHitStatistic), clientId); state.Hits.Remove(hitStat); } @@ -617,7 +693,7 @@ private void HandleDisconnectCalculations(EFClient client, HitState state) if (sessionScores.Count == 0) { - stat.Score += client.Score > 0 ? client.Score : client.GetAdditionalProperty(Helpers.StatManager.ESTIMATED_SCORE) ?? 0 * 50; + stat.Score += client.Score > 0 ? client.Score : (client.GetAdditionalProperty(StatManager.ESTIMATED_SCORE) ?? 0) * 50; } else diff --git a/Plugins/Stats/Client/ServerDistributionCalculator.cs b/Plugins/Stats/Client/ServerDistributionCalculator.cs index 76e31eae4..1364889b8 100644 --- a/Plugins/Stats/Client/ServerDistributionCalculator.cs +++ b/Plugins/Stats/Client/ServerDistributionCalculator.cs @@ -7,95 +7,208 @@ using Data.Models.Client; using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats; -using IW4MAdmin.Plugins.Stats.Config; using Microsoft.EntityFrameworkCore; using SharedLibraryCore; -using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Helpers; using Stats.Client.Abstractions; using Stats.Config; using Stats.Helpers; namespace Stats.Client { - public class ServerDistributionCalculator : IServerDistributionCalculator + /// + /// Computes log-normal distribution parameters and Z-scores for player performance, + /// scoped per server and per performance bucket. During initialization, it builds two caches: + /// + /// Distribution cache — fits a log-normal (mean/sigma) to each server's and + /// each bucket's player performance values (⅓ Elo + ⅔ Skill). Refreshed hourly (1 min in dev). + /// Max Z-score cache — tracks the highest playtime-weighted average Z-score per bucket, + /// used to normalize raw Z-scores into a 0–1 performance rating. Refreshed every 30 min. + /// + /// These caches are keyed by serverId (for per-server lookups) and by bucket code + /// (for cross-server aggregate rankings within a bucket). + /// + public class ServerDistributionCalculator( + IDatabaseContextFactory contextFactory, + IDataValueCache> distributionCache, + IDataValueCache maxZScoreCache, + StatsConfiguration config, + ApplicationConfiguration appConfig) + : IServerDistributionCalculator { - private readonly IDatabaseContextFactory _contextFactory; - - private readonly IDataValueCache> - _distributionCache; - - private readonly IDataValueCache - _maxZScoreCache; - - private readonly IConfigurationHandler _configurationHandler; - private readonly List _serverIds = new List(); + private readonly List> _serverIds = []; private const string DistributionCacheKey = nameof(DistributionCacheKey); private const string MaxZScoreCacheKey = nameof(MaxZScoreCacheKey); - public ServerDistributionCalculator(IDatabaseContextFactory contextFactory, - IDataValueCache> distributionCache, - IDataValueCache maxZScoreCache, - IConfigurationHandlerFactory configFactory) - { - _contextFactory = contextFactory; - _distributionCache = distributionCache; - _maxZScoreCache = maxZScoreCache; - _configurationHandler = configFactory.GetConfigurationHandler("StatsPluginSettings"); - } - public async Task Initialize() { await LoadServers(); - _distributionCache.SetCacheItem((async (set, token) => - { - await _configurationHandler.BuildAsync(); - var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3; - var distributions = new Dictionary(); + distributionCache.SetCacheItem(async (set, token) => + { + var distributions = new Dictionary(); await LoadServers(); - foreach (var serverId in _serverIds) + var iqPerformances = set + .Where(s => s.Skill > 0) + .Where(s => s.EloRating >= 0) + .Where(s => s.Client.Level != EFClient.Permission.Banned); + + foreach (var (serverId, performanceBucket) in _serverIds) + { + var bucketConfig = + config.PerformanceBuckets.FirstOrDefault(bucket => + bucket.Code == performanceBucket) ?? new PerformanceBucketConfiguration(); + + var oldestPerf = DateTime.UtcNow - bucketConfig.RankingExpiration; + var performances = await iqPerformances.Where(s => s.ServerId == serverId) + .Where(s => s.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds) + .Where(s => s.UpdatedAt >= oldestPerf) + .Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0) + .ToListAsync(token); + + var distributionParams = performances.GenerateDistributionParameters(); + distributions.Add(serverId.ToString(), distributionParams); + } + + // Bucket-level distribution fits. + // + // DB stores PerformanceBucket.Code lower-cased (IW4MServer normalises + // via ToLowerInvariant on insert), but IW4MAdminSettings may have + // mixed case ("Zombies") or omit the code entirely. Both situations + // historically produced empty caches because the iterator-side + // fallback ("null", "") never matched the DB filter. PerformanceBucketCodes + // collapses both to the canonical "default" string and lower-cases + // explicit codes, making the cache key, the LINQ comparison, and + // PerformanceBucketCodes.IsDefault all agree. + // + // The default-bucket OR-clause picks up legacy rows whose bucket FK + // is still NULL (servers that never had PerformanceBucketCode set, + // or were registered before the bucket column existed). Without this, + // a freshly-deployed bucket-aware build sees zero default-bucket data + // until every server is manually backfilled. + foreach (var performanceBucket in appConfig.Servers + .Select(server => PerformanceBucketCodes.Normalize(server.PerformanceBucketCode)) + .Distinct()) { - var performance = await set - .Where(s => s.ServerId == serverId) - .Where(s => s.Skill > 0) - .Where(s => s.EloRating > 0) - .Where(s => s.Client.Level != EFClient.Permission.Banned) - .Where(s => s.TimePlayed >= validPlayTime) - .Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo()) - .Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0).ToListAsync(); - var distributionParams = performance.GenerateDistributionParameters(); - distributions.Add(serverId, distributionParams); + var bucketConfig = + config.PerformanceBuckets.FirstOrDefault(bucket => + string.Equals(bucket.Code, performanceBucket, StringComparison.OrdinalIgnoreCase)) + ?? new PerformanceBucketConfiguration(); + + var isDefaultBucket = PerformanceBucketCodes.IsDefault(performanceBucket); + var oldestPerf = DateTime.UtcNow - bucketConfig.RankingExpiration; + var performances = await iqPerformances + .Where(perf => perf.Server.PerformanceBucket.Code == performanceBucket + || (isDefaultBucket && perf.Server.PerformanceBucketId == null)) + .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(performanceBucket, distributionParams); } return distributions; - }), DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1)); + }, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1)); + + // The maxZScore cache must be seeded after the distribution cache because + // its callback now reads from distributionCache to derive z-scores from the + // bucket distribution (rather than from the per-server EFClientStatistics.ZScore + // column — see callback comment for the full reasoning). Force the + // distribution cache populated first so each maxZScore seed sees fresh data. + await distributionCache.GetCacheItem(DistributionCacheKey, CancellationToken.None); - _maxZScoreCache.SetCacheItem(async (set, token) => + // Per-bucket max-z seed. + // + // Replaces the previous implementation, which read EFClientStatistics.ZScore + // (per-server fits) and took the max of playtime-weighted averages across + // a bucket. That model conflated per-server distributions: a single + // low-population server with a degenerate sigma fit produced per-row + // z-scores in the hundreds, and the aggregation pulled that outlier into + // the bucket-wide max. The poisoned max became the rating denominator + // (Rating = (z+3) / (max+z+3) * 1000), compressing every other player's + // rating in the bucket to single-digit percent. + // + // The new model derives max-z from the bucket-fitted log-normal in + // distributionCache: take the playtime-weighted Performance per qualifying + // client across the bucket, find the maximum, transform to z once. This + // gives a max in the same z-space the aggregate writer produces, with no + // dependence on the per-server ZScore column. A pathological per-server + // fit can no longer leak into bucket math. + foreach (var performanceBucket in appConfig.Servers + .Select(s => PerformanceBucketCodes.Normalize(s.PerformanceBucketCode)) + .Distinct()) { - await _configurationHandler.BuildAsync(); - var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3; + maxZScoreCache.SetCacheItem(async (set, ids, token) => + { + var localPerformanceBucket = (string)ids.FirstOrDefault(); + var isDefaultBucket = PerformanceBucketCodes.IsDefault(localPerformanceBucket); - var zScore = await set - .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime)) - .Where(s => s.Skill > 0) - .Where(s => s.EloRating > 0) - .GroupBy(stat => stat.ClientId) - .Select(group => - group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed)) - .MaxAsync(avgZScore => (double?) avgZScore, token); - return zScore ?? 0; - }, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30)); + // Resolve the same min-playtime / recency window the aggregate + // writer uses — keeps the cache and the write path consistent. + var validPlayTime = config.TopPlayersMinPlayTime; + var oldestStatWindow = TimeSpan.FromDays(15); + var bucketConfig = config.PerformanceBuckets.FirstOrDefault(cfg => + string.Equals(cfg.Code, localPerformanceBucket, StringComparison.OrdinalIgnoreCase)); + if (bucketConfig is not null) + { + validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds; + oldestStatWindow = bucketConfig.RankingExpiration; + } + + var distributions = await distributionCache.GetCacheItem(DistributionCacheKey, token); + if (!distributions.TryGetValue(localPerformanceBucket, out var bucketDist) || bucketDist.Sigma == 0) + { + // No bucket distribution available (cold start or empty bucket). + // Leave max as 0; the rating writer treats max==0 as "not ready" + // and skips the aggregate insert until the next refresh. Rare + // and self-healing. + return 0.0; + } + + var oldestUpdated = DateTime.UtcNow - oldestStatWindow; + var maxWeightedPerformance = await set + .Where(stat => stat.Skill > 0) + .Where(stat => stat.EloRating >= 0) + .Where(stat => stat.Skill < 999999) + .Where(stat => stat.Client.Level != EFClient.Permission.Banned) + .Where(stat => stat.TimePlayed >= validPlayTime) + .Where(stat => stat.UpdatedAt >= oldestUpdated) + .Where(stat => stat.Server.PerformanceBucket.Code == localPerformanceBucket + || (isDefaultBucket && stat.Server.PerformanceBucketId == null)) + .GroupBy(stat => stat.ClientId) + // Performance is [NotMapped] so we recompute the composite inline + // (same formula as the distribution fit and the aggregate writer's + // ranking comparison — three call sites must agree exactly). + .Select(g => g.Sum(s => (s.EloRating / 3.0 + s.Skill * 2.0 / 3.0) * s.TimePlayed) + / g.Sum(s => s.TimePlayed)) + .MaxAsync(p => (double?)p, token); - await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken()); - await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken()); + if (maxWeightedPerformance is null or <= 0) + { + return 0.0; + } + + // Single transform to z-space via the bucket distribution. Negative + // z would mean every qualifying client is below the bucket mean — + // floor at 0 so the rating denominator stays sane. + var maxZ = (Math.Log(maxWeightedPerformance.Value) - bucketDist.Mean) / bucketDist.Sigma; + return Math.Max(0.0, maxZ); + }, MaxZScoreCacheKey, new[] { performanceBucket }, + Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30)); + + await maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket }); + } /*foreach (var serverId in _serverIds) { await using var ctx = _contextFactory.CreateContext(enableTracking: true); - + var a = await ctx.Set() .Where(s => s.ServerId == serverId) //.Where(s=> s.ClientId == 216105) @@ -105,16 +218,16 @@ public async Task Initialize() .Where(s => s.TimePlayed >= 3600 * 3) .Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo()) .ToListAsync(); - + var b = a.Distinct(); - + foreach (var item in b) { await Plugin.Manager.UpdateHistoricalRanking(item.ClientId, item, item.ServerId); //item.ZScore = await GetZScoreForServer(serverId, item.Performance); //item.UpdatedAt = DateTime.UtcNow; } - + await ctx.SaveChangesAsync(); }*/ } @@ -123,36 +236,72 @@ private async Task LoadServers() { if (_serverIds.Count == 0) { - await using var context = _contextFactory.CreateContext(false); + await using var context = contextFactory.CreateContext(false); _serverIds.AddRange(await context.Servers .Where(s => s.EndPoint != null && s.HostName != null) - .Select(s => s.ServerId) + .Select(s => new Tuple(s.ServerId, s.PerformanceBucket == null ? null : s.PerformanceBucket.Code)) .ToListAsync()); } } - public async Task GetZScoreForServer(long serverId, double value) + /// + /// Converts a raw performance value into a Z-score using the cached log-normal distribution + /// for the given server or bucket. Looks up server-specific params first, falls back to bucket-level. + /// Returns 0 if no distribution data is available (e.g. too few players). + /// + public async Task GetZScoreForServerOrBucket(double value, long? serverId = null, + string performanceBucket = null) { - var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken()); - if (!serverParams.ContainsKey(serverId)) + if (serverId is null && performanceBucket is null) { return 0.0; } - var sdParams = serverParams[serverId]; - if (sdParams.Sigma == 0) + var serverParams = await distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken()); + Extensions.LogParams sdParams = null; + + if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1)) + { + sdParams = sdParams1; + } + + else if (performanceBucket is not null && serverParams.TryGetValue(performanceBucket, out var sdParams2)) + { + sdParams = sdParams2; + } + + if (sdParams is null || sdParams.Sigma == 0) { return 0.0; } + value = Math.Max(1, value); + var zScore = (Math.Log(value) - sdParams.Mean) / sdParams.Sigma; return zScore; } - public async Task GetRatingForZScore(double? value) + /// + /// Normalizes a Z-score into a 0–1 performance rating by dividing by the max Z-score + /// in the bucket. Returns null if no max is available (empty bucket or cache miss). + /// + public async Task GetRatingForZScore(double? value, string performanceBucket) { - var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken()); - return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore); + try + { + // Normalise so null/empty/"default" all hit the same cache entry as + // the seed loop wrote. Pre-fix this used `?? string.Empty` while the + // distribution-cache loop used `?? "null"` — three different fallbacks + // for the same logical bucket meant a null caller never hit any seeded + // entry and the writer aborted via the null-rating branch. + var key = PerformanceBucketCodes.Normalize(performanceBucket); + var maxZScore = await maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, [key]); + return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore); + } + catch (KeyNotFoundException) + { + return null; + } } } } diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index 85c362796..77f73b7de 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -1,4 +1,4 @@ -using SharedLibraryCore; +using SharedLibraryCore; using System.Linq; using System.Threading.Tasks; using Data.Abstractions; @@ -33,7 +33,7 @@ public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translat Required = false } }; - + _contextFactory = contextFactory; _statManager = statManager; } diff --git a/Plugins/Stats/Config/PerformanceBucketConfiguration.cs b/Plugins/Stats/Config/PerformanceBucketConfiguration.cs new file mode 100644 index 000000000..51707fe33 --- /dev/null +++ b/Plugins/Stats/Config/PerformanceBucketConfiguration.cs @@ -0,0 +1,34 @@ +using System; +using SharedLibraryCore; + +namespace Stats.Config; + +// PerformanceBucketCodes moved to SharedLibraryCore.Helpers so the shared +// PerformanceBucketClassifier (also in SharedLibraryCore) can reuse the same +// normaliser. Update consumers to `using SharedLibraryCore.Helpers;`. + +/// +/// Per-bucket settings that control how players are ranked within a performance bucket. +/// Configured in . +/// If a server's bucket code doesn't match any configured entry, defaults are used. +/// +public class PerformanceBucketConfiguration +{ + /// + /// Matches to link + /// this configuration to its database bucket entity. + /// + public string Code { get; set; } + + /// + /// Minimum total playtime before a player is eligible for ranking in this bucket. + /// Prevents new or low-activity players from appearing on leaderboards. + /// + public TimeSpan ClientMinPlayTime { get; set; } = Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(3); + + /// + /// How far back to look when computing rankings. Stats older than this are excluded + /// from Z-score and leaderboard calculations, keeping rankings current. + /// + public TimeSpan RankingExpiration { get; set; } = TimeSpan.FromDays(15); +} diff --git a/Plugins/Stats/Config/StatsConfiguration.cs b/Plugins/Stats/Config/StatsConfiguration.cs index 024b5008e..7bec5d1ba 100644 --- a/Plugins/Stats/Config/StatsConfiguration.cs +++ b/Plugins/Stats/Config/StatsConfiguration.cs @@ -19,6 +19,8 @@ public class StatsConfiguration : IBaseConfiguration public int MostKillsClientLimit { get; set; } = 5; public bool EnableAdvancedMetrics { get; set; } = true; + public List PerformanceBuckets { get; set; } = new(); + public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = { new() { diff --git a/Plugins/Stats/Dtos/AdvancedStatsInfo.cs b/Plugins/Stats/Dtos/AdvancedStatsInfo.cs index f7cd10ed0..6b07e03b2 100644 --- a/Plugins/Stats/Dtos/AdvancedStatsInfo.cs +++ b/Plugins/Stats/Dtos/AdvancedStatsInfo.cs @@ -44,6 +44,8 @@ public class AdvancedStatsInfo public List TopWeapons { get; set; } = []; public List TopHitLocations { get; set; } = []; public List PerformanceHistory { get; set; } = []; + public string? PerformanceBucket { get; set; } + public List CustomMetrics { get; set; } = []; } /// diff --git a/Plugins/Stats/Dtos/ClientRankingInfo.cs b/Plugins/Stats/Dtos/ClientRankingInfo.cs new file mode 100644 index 000000000..24645ca11 --- /dev/null +++ b/Plugins/Stats/Dtos/ClientRankingInfo.cs @@ -0,0 +1,3 @@ +namespace Stats.Dtos; + +public record ClientRankingInfo(int CurrentRanking, int TotalRankedClients, string PerformanceBucket); diff --git a/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs b/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs new file mode 100644 index 000000000..ea96c0ea6 --- /dev/null +++ b/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs @@ -0,0 +1,3 @@ +namespace Stats.Dtos; + +public class ClientRankingInfoRequest : StatsInfoRequest; diff --git a/Plugins/Stats/Dtos/StatsInfoRequest.cs b/Plugins/Stats/Dtos/StatsInfoRequest.cs index 0221eb0f5..d26632475 100644 --- a/Plugins/Stats/Dtos/StatsInfoRequest.cs +++ b/Plugins/Stats/Dtos/StatsInfoRequest.cs @@ -7,5 +7,19 @@ public class StatsInfoRequest /// public int? ClientId { get; set; } public string? ServerEndpoint { get; set; } + + /// + /// Performance-bucket code filter. The setter lower-cases so the DB + /// equality comparison can't silently miss on a capitalised input — + /// EFPerformanceBucket.Code stores the canonical lower-cased + /// form. Empty/whitespace stays null so "no filter" remains + /// distinguishable from the explicit default bucket. Mirrors + /// SharedLibraryCore.Helpers.PerformanceBucketCodes.Normalize. + /// + public string PerformanceBucketCode + { + get; + init => field = string.IsNullOrWhiteSpace(value) ? null : value.ToLowerInvariant(); + } } } diff --git a/Plugins/Stats/Dtos/TopStatsInfo.cs b/Plugins/Stats/Dtos/TopStatsInfo.cs index 56403ed71..15ffa9c60 100644 --- a/Plugins/Stats/Dtos/TopStatsInfo.cs +++ b/Plugins/Stats/Dtos/TopStatsInfo.cs @@ -1,11 +1,12 @@ using SharedLibraryCore.Dtos; +using SharedLibraryCore.Interfaces; using System; using System.Collections.Generic; -using System.Text; +using Data.Models; namespace IW4MAdmin.Plugins.Stats.Web.Dtos { - public class TopStatsInfo : SharedInfo + public class TopStatsInfo : SharedInfo, ITopStatsMutable { public int Ranking { get; set; } public string Name { get; set; } @@ -22,6 +23,7 @@ public class TopStatsInfo : SharedInfo public List PerformanceHistory { get; set; } public double? ZScore { get; set; } public long? ServerId { get; set; } + public List Metrics { get; set; } = new(); } public class PerformanceHistory diff --git a/Plugins/Stats/Extensions.cs b/Plugins/Stats/Extensions.cs index 993a750d0..b954dffc3 100644 --- a/Plugins/Stats/Extensions.cs +++ b/Plugins/Stats/Extensions.cs @@ -35,6 +35,11 @@ public class LogParams var items = stats.Where(validation).ToList(); var performancePlayTime = items.Sum(s => s.TimePlayed); + if (performancePlayTime == 0) + { + return null; + } + var propInfo = typeof(EFClientStatistics).GetProperty(propertyName); var weightedValues = items.Sum(item => (double?) propInfo?.GetValue(item) * (item.TimePlayed / (double) performancePlayTime)); @@ -52,27 +57,35 @@ public static LogParams GenerateDistributionParameters(this IEnumerable }; } - var ti = 0.0; - var ti2 = 0.0; + var sumLogSquared = 0.0; // Σ(log x)² + var sumLog = 0.0; // Σ log x var n = 0L; foreach (var val in values) { var logVal = Math.Log(val); - ti += logVal * logVal; - ti2 += logVal; + sumLogSquared += logVal * logVal; + sumLog += logVal; n++; - if (n % 50 == 0) // this isn't ideal, but we want to reduce the amount of CPU usage that the + if (n % 50 == 0) // this isn't ideal, but we want to reduce the amount of CPU usage that the // loops takes so people don't complain { Thread.Sleep(1); } } - var mean = ti2 / n; - ti2 *= ti2; + // Log-normal sample mean and standard deviation + var mean = sumLog / n; var bottom = n == 1 ? 1 : n * (n - 1); - var sigma = Math.Sqrt(((n * ti) - ti2) / bottom); + // Floor sigma to keep z-scores bounded when the fitted distribution + // collapses (small n with near-identical values). Without this, two + // similar players on a low-population server produce sigma ~= 0.001, + // which sends downstream z-scores into the hundreds and poisons any + // bucket-wide max that aggregates per-server z-scores. 0.25 is well + // below the natural fitted sigma of healthy buckets (~0.95) so it + // only kicks in for genuinely degenerate fits. + const double minSigma = 0.25; + var sigma = Math.Max(Math.Sqrt((n * sumLogSquared - sumLog * sumLog) / bottom), minSigma); return new LogParams() { diff --git a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs index 815e6a316..89101b9a9 100644 --- a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs +++ b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -8,6 +8,7 @@ using Data.Models.Client; using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats; +using IW4MAdmin.Plugins.Stats.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; @@ -15,8 +16,8 @@ using SharedLibraryCore.Dtos; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; +using Stats.Config; using Stats.Dtos; -using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Stats.Helpers { @@ -24,10 +25,14 @@ public class AdvancedClientStatsResourceQueryHelper( ILogger logger, IDatabaseContextFactory contextFactory, IManager manager, - DefaultSettings defaultSettings) - : IResourceQueryHelper + DefaultSettings defaultSettings, + IServerDataViewer serverDataViewer, + StatManager statManager + ) + : IResourceQueryHelper, + IResourceQueryHelper { - private readonly ILogger _logger = logger; + private readonly Microsoft.Extensions.Logging.ILogger _logger = logger; public async Task> QueryResource(StatsInfoRequest query) { @@ -56,9 +61,27 @@ public async Task> QueryResource(St return new ResourceQueryHelperResult(); } - var hitStats = await context.Set() - .Where(stat => stat.ClientId == query.ClientId) - .Where(stat => stat.ServerId == serverId) + var iqHitStats = context.Set() + .Where(stat => stat.ClientId == query.ClientId); + + // Default-bucket queries must include rows whose server FK is still + // NULL — communities upgrading to the bucket-aware build start with + // every server unbacked-filled (PerformanceBucketCode null in + // IW4MAdminSettings means the implicit default pool), and we cannot + // require an O(N rows) backfill of EFClientHitStatistics on first + // start. Without the OR-NULL clause the per-client hit-stats page + // would return zero rows for the default bucket on a fresh upgrade. + var hitStatsBucketIsDefault = PerformanceBucketCodes.IsDefault(query.PerformanceBucketCode); + // DB stores Code lower-cased (the writer in IW4MServer normalises on insert) + // — defence-in-depth normalise here so a capitalised bucket code from the + // request can't silently filter to zero rows. + var normalizedHitStatsBucket = PerformanceBucketCodes.Normalize(query.PerformanceBucketCode); + iqHitStats = !string.IsNullOrEmpty(query.PerformanceBucketCode) + ? iqHitStats.Where(stat => stat.Server.PerformanceBucket.Code == normalizedHitStatsBucket + || (hitStatsBucketIsDefault && stat.Server.PerformanceBucketId == null)) + : iqHitStats.Where(stat => stat.ServerId == serverId); + + var hitStats = await iqHitStats .Select(stat => new HitStatProjection { HitLocationId = stat.HitLocationId, @@ -90,10 +113,19 @@ public async Task> QueryResource(St }) .ToListAsync(); + // Same NULL-FK tolerance as the hit-stats query above: the per-client + // ranking-history graph for the default bucket must surface legacy + // rows whose PerformanceBucketId was never written (which is every + // ranking-history row for any server the operator hasn't manually + // tagged with a PerformanceBucketCode). + var ratingsBucketIsDefault = PerformanceBucketCodes.IsDefault(query.PerformanceBucketCode); + var normalizedRatingsBucket = PerformanceBucketCodes.Normalize(query.PerformanceBucketCode); var ratings = await context.Set() .Where(r => r.ClientId == clientInfo.ClientId) .Where(r => r.ServerId == serverId) .Where(r => r.Ranking != null) + .Where(r => r.PerformanceBucket.Code == normalizedRatingsBucket + || (ratingsBucketIsDefault && r.PerformanceBucketId == null)) .OrderByDescending(r => r.CreatedDateTime) .Take(100) .Select(r => new { r.Newest, r.PerformanceMetric, r.ZScore, r.CreatedDateTime, r.Ranking }) @@ -105,10 +137,19 @@ public async Task> QueryResource(St .Select(stat => new { stat.ServerId, stat.Skill, stat.EloRating, stat.SPM, stat.TimePlayed }) .ToListAsync(); + var rankingInfo = (await QueryResource(new ClientRankingInfoRequest + { + ClientId = query.ClientId, + ServerEndpoint = query.ServerEndpoint, + PerformanceBucketCode = query.PerformanceBucketCode + })).Results.First(); + var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest); var ranking = mostRecentRanking?.Ranking + 1; - if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < Extensions.FifteenDaysAgo()) + var bucketConfig = await statManager.GetBucketConfig(serverId); + + if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < DateTime.UtcNow - bucketConfig.RankingExpiration) { ranking = 0; } @@ -306,6 +347,8 @@ public async Task> QueryResource(St ZScore = mostRecentRanking?.ZScore, Rating = mostRecentRanking?.PerformanceMetric, Ranking = ranking, + TotalRankedClients = rankingInfo.TotalRankedClients, + PerformanceBucket = rankingInfo.PerformanceBucket, // Aggregate stats Kills = kills, @@ -381,14 +424,71 @@ private static string RebuildWeaponName(HitStatProjection? proj) : $"{proj.WeaponName}{string.Join("_", proj.Attachment1Name, proj.Attachment2Name, proj.Attachment3Name)}"; } - public static Expression> GetRankingFunc(int minPlayTime, double? zScore = null, + public static Expression> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null, long? serverId = null) { + var oldestStat = DateTime.UtcNow.Subtract(expiration); return stats => (serverId == null || stats.ServerId == serverId) && - stats.UpdatedAt >= Extensions.FifteenDaysAgo() && + stats.UpdatedAt >= oldestStat && stats.Client.Level != EFClient.Permission.Banned && stats.TimePlayed >= minPlayTime && (zScore == null || stats.ZScore > zScore); } + + public async Task> QueryResource(ClientRankingInfoRequest query) + { + await using var context = contextFactory.CreateContext(enableTracking: false); + + long? serverId = null; + + if (!string.IsNullOrEmpty(query.ServerEndpoint)) + { + serverId = manager.GetServers().FirstOrDefault(server => server.Id == query.ServerEndpoint) + ?.LegacyDatabaseId; + } + + var currentRanking = 0; + int totalRankedClients; + string performanceBucketCode; + + if (string.IsNullOrEmpty(query.PerformanceBucketCode) && serverId is null) + { + var maxPerformance = await context.Set() + .Where(r => r.ClientId == query.ClientId) + .Where(r => r.Ranking != null) + .Where(r => r.ServerId == serverId) + .Where(rating => rating.Newest) + .GroupBy(rating => rating.PerformanceBucket) + .Select(grp => new { grp.Key, PerformanceMetric = grp.Max(rating => rating.Ranking) }) + .Where(grp => grp.PerformanceMetric != null) + .FirstOrDefaultAsync(); + + if (maxPerformance is null) + { + currentRanking = 0; + totalRankedClients = 0; + performanceBucketCode = null; + } + else + { + currentRanking = + await statManager.GetClientOverallRanking(query.ClientId!.Value, null, maxPerformance.Key.Code); + totalRankedClients = await serverDataViewer.RankedClientsCountAsync(null, maxPerformance.Key.Code); + performanceBucketCode = maxPerformance.Key.Code; + } + } + else + { + performanceBucketCode = query.PerformanceBucketCode; + currentRanking = + await statManager.GetClientOverallRanking(query.ClientId!.Value, serverId, performanceBucketCode); + totalRankedClients = await serverDataViewer.RankedClientsCountAsync(serverId, performanceBucketCode); + } + + return new ResourceQueryHelperResult + { + Results = [new ClientRankingInfo(currentRanking, totalRankedClients, performanceBucketCode)] + }; + } } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 382f1cb48..1de7d7d3a 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -1,5 +1,4 @@ using IW4MAdmin.Plugins.Stats.Cheat; -using IW4MAdmin.Plugins.Stats.Web.Dtos; using Microsoft.EntityFrameworkCore; using SharedLibraryCore; using SharedLibraryCore.Helpers; @@ -19,6 +18,7 @@ using Data.Models.Client.Stats; using Data.Models.Server; using Humanizer; +using IW4MAdmin.Plugins.Stats.Web.Dtos; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Stats.Client.Abstractions; @@ -30,31 +30,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { - public class StatManager + public class StatManager( + ILogger logger, + IDatabaseContextFactory contextFactory, + StatsConfiguration statsConfig, + IServerDistributionCalculator serverDistributionCalculator, + ILookupCache serverCache) { private const int MAX_CACHED_HITS = 100; - private readonly ConcurrentDictionary _servers; - private readonly ILogger _log; - private readonly IDatabaseContextFactory _contextFactory; - private readonly StatsConfiguration _config; + private readonly ConcurrentDictionary _servers = new(); + private readonly ILogger _log = logger; public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; public static string ESTIMATED_SCORE = "EstimatedScore"; private readonly SemaphoreSlim _addPlayerWaiter = new(1, 1); - private readonly IServerDistributionCalculator _serverDistributionCalculator; - private readonly ILookupCache _serverCache; - - public StatManager(ILogger logger, IDatabaseContextFactory contextFactory, - StatsConfiguration statsConfig, - IServerDistributionCalculator serverDistributionCalculator, ILookupCache serverCache) - { - _servers = new ConcurrentDictionary(); - _log = logger; - _contextFactory = contextFactory; - _config = statsConfig; - _serverDistributionCalculator = serverDistributionCalculator; - _serverCache = serverCache; - } + private readonly ConcurrentDictionary _performanceBucketIdCache = new(); ~StatManager() { @@ -68,25 +58,30 @@ public Expression> GetRankingFunc(long? serverId = null) r.When > fifteenDaysAgo && r.RatingHistory.Client.Level != EFClient.Permission.Banned && r.Newest && - r.ActivityAmount >= _config.TopPlayersMinPlayTime; + r.ActivityAmount >= statsConfig.TopPlayersMinPlayTime; } /// /// gets a ranking across all servers for given client id /// /// client id of the player + /// + /// /// - public async Task GetClientOverallRanking(int clientId, long? serverId = null) + public async Task GetClientOverallRanking(int clientId, long? serverId = null, string performanceBucket = null) { - await using var context = _contextFactory.CreateContext(enableTracking: false); + await using var context = contextFactory.CreateContext(enableTracking: false); - if (_config.EnableAdvancedMetrics) + if (statsConfig.EnableAdvancedMetrics) { + var bucketConfig = await GetBucketConfig(null, performanceBucket); + var clientRanking = await context.Set() + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId, bucketConfig.Code)) .Where(r => r.ClientId == clientId) - .Where(r => r.ServerId == serverId) .Where(r => r.Newest) .FirstOrDefaultAsync(); + return clientRanking?.Ranking + 1 ?? 0; } @@ -110,25 +105,41 @@ public async Task GetClientOverallRanking(int clientId, long? serverId = nu return 0; } - public Expression> GetNewRankingFunc(int? clientId = null, - long? serverId = null) + private Expression> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, + long? serverId = null, string performanceBucketCode = null) { - return (ranking) => ranking.ServerId == serverId - && ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned - && ranking.CreatedDateTime >= Extensions.FifteenDaysAgo() - && ranking.ZScore != null - && ranking.PerformanceMetric != null - && ranking.Newest - && ranking.Client.TotalConnectionTime >= - _config.TopPlayersMinPlayTime; + var oldestDate = DateTime.UtcNow - oldestStat; + // Pre-bucket-migration rankings have PerformanceBucketId == null. Treat those + // as belonging to the "default" bucket so established servers don't lose their + // existing top-stats history once a bucket is configured. Communities migrating + // from older versions keep working even before they manually backfill bucket + // FKs — both NULL-FK legacy rows and properly-tagged new rows surface together. + var isDefaultBucket = PerformanceBucketCodes.IsDefault(performanceBucketCode); + // Defence-in-depth normalisation: callers reaching this expression usually + // come through GetBucketConfig (which already lower-cases via Normalize), + // but a direct call with a capitalised user-supplied bucket would + // otherwise filter to zero rows since the DB stores lower-case canonical. + var normalizedBucket = PerformanceBucketCodes.Normalize(performanceBucketCode); + return ranking => ranking.ServerId == serverId + && ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned + && ranking.CreatedDateTime >= oldestDate + && ranking.ZScore != null + && ranking.PerformanceMetric != null + && ranking.Newest + && (ranking.PerformanceBucket.Code == normalizedBucket + || (isDefaultBucket && ranking.PerformanceBucketId == null)) + && ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds; } - public async Task GetTotalRankedPlayers(long serverId) + public async Task GetTotalRankedPlayers(long? serverId = null, string performanceBucket = null) { - await using var context = _contextFactory.CreateContext(enableTracking: false); + var bucketConfig = await GetBucketConfig(serverId, performanceBucket); + + await using var context = contextFactory.CreateContext(enableTracking: false); return await context.Set() - .Where(GetNewRankingFunc(serverId: serverId)) + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId, + bucketConfig.Code)) .CountAsync(); } @@ -143,73 +154,153 @@ public class RankingSnapshot public DateTime CreatedDateTime { get; set; } } - public async Task> GetNewTopStats(int start, int count, long? serverId = null) + public async Task<(List Players, int RankingHistoryRowsConsumed)> GetNewTopStats( + int start, int count, long? serverId = null, string performanceBucketCode = null) { - await using var context = _contextFactory.CreateContext(false); - - var clientIdsList = await context.Set() - .Where(GetNewRankingFunc(serverId: serverId)) + var bucketConfig = await GetBucketConfig(serverId, performanceBucketCode); + + await using var context = contextFactory.CreateContext(false); + + // The page-fill loop: ranking history `RankedClientsCountAsync` may report N + // rows but the per-client stats join below filters on TimePlayed/Kills/Deaths + // — so a slice of `count` ranking rows can yield far fewer top-stats rows. + // Without chunk-fill, the leaderboard returns short pages and infinite-scroll + // stalls. We over-fetch ranking rows in chunks and validate against the stats + // filter until we have `count` qualifying clients (or exhaust the source). + // The caller advances its offset by the *ranking-history rows consumed* + // returned in the tuple, so successive pages don't re-walk rejected rows. + var rankingIdsQuery = context.Set() + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId, + bucketConfig.Code)) .OrderByDescending(ranking => ranking.PerformanceMetric) - .Select(ranking => ranking.ClientId) - .Skip(start) - .Take(count) - .ToListAsync(); - - // Fetch rankings without joins - much faster, less data transfer - var allRankings = await context.Set() - .Where(ranking => clientIdsList.Contains(ranking.ClientId)) - .Where(ranking => ranking.ServerId == serverId) - .OrderByDescending(ranking => ranking.CreatedDateTime) - .Select(ranking => new + .Select(ranking => ranking.ClientId); + + var clientIdsList = new List(count); + var consumed = 0; + // Chunk size = 2× requested page; small enough to stay cheap on each round- + // trip, large enough to amortize the round-trip cost when filter ratio is low. + var chunkSize = Math.Max(count * 2, 50); + // Hard cap iterations to avoid an unbounded loop on pathological data + // (e.g. a bucket where every single ranked client fails the stats filter). + var attempts = 0; + const int maxAttempts = 20; + + while (clientIdsList.Count < count && attempts++ < maxAttempts) + { + var chunk = await rankingIdsQuery + .Skip(start + consumed) + .Take(chunkSize) + .ToListAsync(); + if (chunk.Count == 0) break; + + var validIds = (await context.Set() + .Where(stat => chunk.Contains(stat.ClientId)) + .Where(stat => stat.TimePlayed > 0) + .Where(stat => stat.Kills > 0 || stat.Deaths > 0) + .Where(stat => serverId == null || stat.ServerId == serverId) + .Select(stat => stat.ClientId) + .Distinct() + .ToListAsync()) + .ToHashSet(); + + // Preserve original ranking order — `chunk` is already + // performance-descending — so the appended results stay sorted. + var lastWalkedToIdx = -1; + var filledMidChunk = false; + for (var i = 0; i < chunk.Count; i++) { - ranking.ClientId, - ranking.PerformanceMetric, - ranking.ZScore, - ranking.Ranking, - ranking.CreatedDateTime - }) - .ToListAsync(); + var id = chunk[i]; + if (!validIds.Contains(id)) continue; + if (clientIdsList.Contains(id)) continue; + clientIdsList.Add(id); + lastWalkedToIdx = i; + if (clientIdsList.Count >= count) + { + filledMidChunk = true; + break; + } + } - // Limit to 60 most recent per client in memory - var limitedRankings = allRankings - .GroupBy(r => r.ClientId) - .SelectMany(g => g.OrderByDescending(r => r.CreatedDateTime).Take(60)) - .ToList(); + // Advance `consumed` by what we actually walked, NOT by the whole + // chunk. If we filled the page mid-chunk, items past `lastWalkedToIdx` + // were never inspected — they belong to the next page. Eating the + // whole chunk here drops `chunk.Count - (lastWalkedToIdx + 1)` rows + // from the leaderboard (caller advances offset by `consumed`, and + // anything counted here is permanently skipped). Symptom was + // infinite-scroll terminating ~25 short of TotalRankedClients on + // every bucket: page 1 ate positions 0-49 to return 25 visible, + // page 2 ate 50-99 to return 25 more, but positions 25-49 and 75-99 + // never made it onscreen because consumed jumped past them. + // + // If we walked the whole chunk without filling (low validity ratio + // OR end-of-source short chunk), every position WAS inspected, so + // advance by chunk.Count. + consumed += filledMidChunk ? lastWalkedToIdx + 1 : chunk.Count; + + // Source exhausted: chunk smaller than requested means no more rows + // beyond this slice. Stop even if we didn't fill the page. + if (chunk.Count < chunkSize) break; + } - // Fetch client info separately (only 50 rows instead of thousands) - var clientInfo = await context.Set() - .Where(c => clientIdsList.Contains(c.ClientId)) - .Select(c => new - { - c.ClientId, - Name = c.CurrentAlias.Name, - c.LastConnection - }) - .ToDictionaryAsync(c => c.ClientId); + var rankingsDict = new Dictionary>(); - // Combine the data - var rankingsDict = limitedRankings - .Select(r => new RankingSnapshot + var includeNullBucket = PerformanceBucketCodes.IsDefault(bucketConfig.Code); + foreach (var clientId in clientIdsList) + { + var eachRank = await context.Set() + .Where(ranking => ranking.ClientId == clientId) + .Where(ranking => ranking.ServerId == serverId) + .Where(ranking => ranking.PerformanceBucket.Code == bucketConfig.Code + || (includeNullBucket && ranking.PerformanceBucketId == null)) + .OrderByDescending(ranking => ranking.CreatedDateTime) + .Select(ranking => new RankingSnapshot + { + ClientId = ranking.ClientId, + Name = ranking.Client.CurrentAlias.Name, + LastConnection = ranking.Client.LastConnection, + PerformanceMetric = ranking.PerformanceMetric, + ZScore = ranking.ZScore, + Ranking = ranking.Ranking, + CreatedDateTime = ranking.CreatedDateTime + }) + .Take(60) + .ToListAsync(); + + if (!rankingsDict.TryAdd(clientId, eachRank)) { - ClientId = r.ClientId, - Name = clientInfo[r.ClientId].Name, - LastConnection = clientInfo[r.ClientId].LastConnection, - PerformanceMetric = r.PerformanceMetric, - ZScore = r.ZScore, - Ranking = r.Ranking, - CreatedDateTime = r.CreatedDateTime - }) - .GroupBy(r => r.ClientId) - .ToDictionary( - g => g.Key, - g => g.OrderByDescending(r => r.CreatedDateTime).ToList() - ); + rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct() + .OrderByDescending(ranking => ranking.CreatedDateTime).ToList(); + } + } - var statsInfo = await context.Set() + // Bucket classification by live server population — replaces the legacy + // hardcoded literal "zombies" check so admins can name buckets freely. + // Drives both the metric-row gate below AND filters the EFClientStatistics + // sum to zombie servers only when a bucket qualifies as zombies (so + // mixed-bucket MP play doesn't pollute the displayed Played/Kills/Deaths). + var bucketClassification = await PerformanceBucketClassifier.ClassifyAsync( + Plugin.ServerManager, contextFactory, bucketConfig.Code); + var suppressMpMetrics = bucketClassification.IsZombieBucket; + var zombieServerIds = bucketClassification.ZombieServerIds; + + var statsQuery = context.Set() .Where(stat => clientIdsList.Contains(stat.ClientId)) .Where(stat => stat.TimePlayed > 0) .Where(stat => stat.Kills > 0 || stat.Deaths > 0) - .Where(stat => serverId == null || stat.ServerId == serverId) + .Where(stat => serverId == null || stat.ServerId == serverId); + + if (suppressMpMetrics && zombieServerIds.Count > 0) + { + // Restrict the sum to the bucket's actual zombie servers. Without + // this, a non-zombie server that happens to share the bucket would + // contribute its MP kills/deaths/playtime into the displayed + // zombie-bucket totals — fundamentally a different game mode and + // not comparable. + var zombieIdList = zombieServerIds.ToList(); + statsQuery = statsQuery.Where(stat => zombieIdList.Contains(stat.ServerId)); + } + + var statsInfo = await statsQuery .GroupBy(stat => stat.ClientId) .Select(s => new { @@ -252,17 +343,136 @@ public async Task> GetNewTopStats(int start, int count, long? .OrderBy(r => r.Ranking) .ToList(); - return finished; + // Run typed-field transformers BEFORE the metric-row loop below so any + // premium override of Kills/Deaths/KDR (e.g. zombies bucket sourcing + // from EFZombieClientStatAggregates instead of bridged EFClientStatistics) + // flows into the displayed metric values without us having to re-sync. + // Single source of truth: the typed DTO fields. CustomStatsMetrics still + // runs after the metric loop and can append additional rows. + foreach (var transformer in Plugin.ServerManager.CustomTopStatsTransformers) + { + await transformer(finished.Cast().ToList(), serverId, bucketConfig.Code); + } + + // Zombies bucket (classified above by live server ratio): suppress + // Kills/Deaths/KDR rows. Kills scale ~round^2 and deaths floor at 1 in + // zombies, so K/D is mathematically broken as a skill signal — the + // premium plugin appends RPD/Highest Round/Solo Index/etc as the + // canonical zombie metrics and reorders TimePlayed to end. + foreach (var topStatsInfo in finished) + { + if (!suppressMpMetrics) + { + topStatsInfo.Metrics.Add(new EFMeta + { + Extra = "Kills", + Value = topStatsInfo.Kills.ToNumericalString(), + Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"] + }); + topStatsInfo.Metrics.Add(new EFMeta + { + Extra = "Deaths", + Value = topStatsInfo.Deaths.ToNumericalString(), + Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"] + }); + topStatsInfo.Metrics.Add(new EFMeta + { + Extra = "KDR", + Value = topStatsInfo.KDR.ToNumericalString(), + Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"] + }); + } + topStatsInfo.Metrics.Add(new EFMeta + { + Extra = "TimePlayed", + Value = topStatsInfo.TimePlayedValue.HumanizeForCurrentCulture(), + Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"] + }); + } + + foreach (var customMetricFunc in Plugin.ServerManager.CustomStatsMetrics) + { + await customMetricFunc(finished.ToDictionary(kvp => kvp.ClientId, kvp => kvp.Metrics), serverId, + bucketConfig.Code, true); + } + + return (finished, consumed); } - public async Task> GetTopStats(int start, int count, long? serverId = null) + /// + /// Resolves the for a server or bucket code. + /// Resolution order: explicit bucket code → config match → DB lookup by serverId → defaults. + /// Returns default config (global min-playtime / 15-day expiry) when no bucket applies. + /// + public async Task GetBucketConfig(long? serverId = null, + string performanceBucketCode = null) { - if (_config.EnableAdvancedMetrics) + // Returned Code is ALWAYS the canonical normalised form (lower-cased, + // null/empty collapsed to "default"). Downstream consumers use Code + // as a cache key, DB filter value, and FK lookup token — letting an + // un-normalised Code escape historically caused cache mismatches + // (e.g. PerformanceBucketCode = "Zombies" in IW4MAdminSettings vs + // "zombies" in the DB; null vs "" vs "default" all meaning the same + // logical pool). Centralising normalisation here means callers can + // treat Code as the truth. + PerformanceBucketConfiguration BuildConfig(string sourceCode) { - return await GetNewTopStats(start, count, serverId); + var configured = !string.IsNullOrEmpty(sourceCode) + ? statsConfig.PerformanceBuckets.FirstOrDefault(bucket => + string.Equals(bucket.Code, sourceCode, StringComparison.OrdinalIgnoreCase)) + : null; + + var result = configured is not null + ? new PerformanceBucketConfiguration + { + ClientMinPlayTime = configured.ClientMinPlayTime, + RankingExpiration = configured.RankingExpiration + } + : new PerformanceBucketConfiguration + { + ClientMinPlayTime = TimeSpan.FromSeconds(statsConfig.TopPlayersMinPlayTime), + RankingExpiration = TimeSpan.FromDays(15) + }; + + result.Code = PerformanceBucketCodes.Normalize(sourceCode); + return result; + } + + // Explicit caller-supplied code wins over server's DB-recorded code. + if (performanceBucketCode is not null) + { + return BuildConfig(performanceBucketCode); + } + + // No serverId hint either → caller wants the default bucket. + if (serverId is null) + { + return BuildConfig(null); } - await using var context = _contextFactory.CreateContext(enableTracking: false); + // The server cache doesn't eagerly load the PerformanceBucket navigation, + // so we query the database directly for the bucket code. + await using var context = contextFactory.CreateContext(false); + var cachedServer = await serverCache.FirstAsync(server => server.Id == serverId); + var dbBucketCode = cachedServer is null + ? null + : await context.Set() + .Where(b => b.PerformanceBucketId == cachedServer.PerformanceBucketId) + .Select(b => b.Code) + .FirstOrDefaultAsync(); + + return BuildConfig(dbBucketCode); + } + + public async Task> GetTopStats(int start, int count, long? serverId = null, string performanceBucket = null) + { + if (statsConfig.EnableAdvancedMetrics) + { + var (players, _) = await GetNewTopStats(start, count, serverId, performanceBucket); + return players; + } + + await using var context = contextFactory.CreateContext(enableTracking: false); // setup the query for the clients within the given rating range var iqClientRatings = (from rating in context.Set() .Where(GetRankingFunc(serverId)) @@ -378,7 +588,14 @@ public async Task EnsureServerAdded(IGameServer gameServer, CancellationToken to { // check to see if the stats have ever been initialized var cachedServer = - await _serverCache.FirstAsync(cachedServer => cachedServer.EndPoint == gameServer.Id); + await serverCache.FirstAsync(cachedServer => cachedServer.EndPoint == gameServer.Id); + + if (cachedServer == null) + { + _log.LogError("[Stats::EnsureServerAdded] cachedServer is null for endpoint {Endpoint}", gameServer.Id); + return; + } + var serverStats = InitializeServerStats(gameServer.LegacyDatabaseId); _servers.TryAdd(cachedServer.ServerId, new ServerStats(cachedServer, serverStats, gameServer as Server) @@ -430,7 +647,7 @@ public async Task AddPlayer(EFClient pl) EFClientStatistics clientStats; - await using var ctx = _contextFactory.CreateContext(enableTracking: false); + await using var ctx = contextFactory.CreateContext(enableTracking: false); var clientStatsSet = ctx.Set(); clientStats = clientStatsSet .Include(cl => cl.HitLocations) @@ -481,8 +698,35 @@ public async Task AddPlayer(EFClient pl) await ctx.SaveChangesAsync(); } - // for stats before rating - if (clientStats.EloRating == 0.0) + // EloRating reconciliation. If an EloRatingFunction is attached + // for this client (e.g. zombie servers, where ZombieStats binds a + // function returning a fixed baseline because zombie kills are + // not PvP), the stored EloRating must equal the function's + // output — that's the invariant ApplyKill's per-kill override + // (line 1768 area) maintains during play. Sync on AddPlayer so + // the invariant also holds at session start, before the first + // kill. Without this, a stale stored EloRating from an earlier + // session (e.g. seeded from a high Skill via the legacy fallback + // below) propagates into clientStats.Performance and into the + // per-server snapshot writer's PerformanceMetric. No threshold + // check — the function is the source of truth whenever attached. + var eloRatingFunc = + pl.GetAdditionalProperty>("EloRatingFunction"); + if (eloRatingFunc is not null) + { + var synced = eloRatingFunc(pl, clientStats); + if (Math.Abs(clientStats.EloRating - synced) > 0.001) + { + _log.LogInformation( + "EloRatingSync: client={Name}({ClientId}) server={Server} prev={Prev} synced={Synced}", + pl.Name, pl.ClientId, pl.CurrentServer?.ToString(), + clientStats.EloRating, synced); + clientStats.EloRating = synced; + } + } + // for stats before rating (legacy seed for clients with no + // EloRatingFunction override — preserves MP behaviour unchanged) + else if (clientStats.EloRating == 0.0) { clientStats.EloRating = clientStats.Skill; } @@ -498,7 +742,7 @@ public async Task AddPlayer(EFClient pl) clientStats.SessionScore = pl.Score; clientStats.LastScore = pl.Score; - pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats, _config)); + pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats, statsConfig)); _log.LogDebug("Added {client} to stats", pl.ToString()); return clientStats; @@ -553,7 +797,7 @@ public async Task RemovePlayer(EFClient client, CancellationToken cancellationTo { clientStats = UpdateStats(clientStats, client); await SaveClientStats(clientStats); - if (_config.EnableAdvancedMetrics) + if (statsConfig.EnableAdvancedMetrics) { await UpdateHistoricalRanking(client.ClientId, clientStats, serverId); } @@ -571,7 +815,7 @@ public async Task RemovePlayer(EFClient client, CancellationToken cancellationTo private async Task SaveClientStats(EFClientStatistics clientStats) { - await using var ctx = _contextFactory.CreateContext(); + await using var ctx = contextFactory.CreateContext(); ctx.Update(clientStats); await ctx.SaveChangesAsync(); } @@ -688,7 +932,7 @@ public async Task AddScriptHit(bool isDamage, DateTime time, EFClient attacker, return; } - if (_config.StoreClientKills) + if (statsConfig.StoreClientKills) { var serverWaiter = _servers[serverId].OnSaving; try @@ -717,7 +961,7 @@ public async Task AddScriptHit(bool isDamage, DateTime time, EFClient attacker, } } - if (_config.AnticheatConfiguration.Enable && !attacker.IsBot && + if (statsConfig.AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId) { clientDetection.TrackedHits.Add(hit); @@ -792,7 +1036,7 @@ private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable= + (Utilities.IsDevelopment ? 0.5 : statsConfig.EnableAdvancedMetrics ? 5.0 : 2.5)) + { + try + { + await attackerStats.ProcessingHit.WaitAsync(Utilities.DefaultCommandTimeout, + Plugin.ServerManager.CancellationToken); + if (statsConfig.EnableAdvancedMetrics) + { + await UpdateHistoricalRanking(attacker.ClientId, attackerStats, serverId); + } + else + { + await UpdateStatHistory(attacker, attackerStats); + } + + attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; + } + catch (Exception e) + { + _log.LogWarning(e, "Could not update stat history for {attacker}", attacker.ToString()); + } + finally + { + if (attackerStats.ProcessingHit.CurrentCount == 0) + { + attackerStats.ProcessingHit.Release(1); + } + } + } + return; } @@ -956,9 +1260,11 @@ public async Task AddStandardKill(EFClient attacker, EFClient victim) victimStats.LastScore = estimatedVictimScore; // show encouragement/discouragement - var streakMessage = attackerStats.ClientId != victimStats.ClientId - ? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, _config) - : StreakMessage.MessageOnStreak(-1, -1, _config); + var streakMessage = attacker.CurrentServer.IsZombieServer() + ? string.Empty + : attackerStats.ClientId != victimStats.ClientId + ? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, statsConfig) + : StreakMessage.MessageOnStreak(-1, -1, statsConfig); if (streakMessage != string.Empty) { @@ -980,9 +1286,9 @@ public async Task AddStandardKill(EFClient attacker, EFClient victim) attackerStats.Skill = 0.0; } - // update their performance + // update their performance if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= - (Utilities.IsDevelopment ? 0.5 : _config.EnableAdvancedMetrics ? 5.0 : 2.5)) + (Utilities.IsDevelopment ? 0.5 : statsConfig.EnableAdvancedMetrics ? 5.0 : 2.5)) { try { @@ -991,7 +1297,7 @@ public async Task AddStandardKill(EFClient attacker, EFClient victim) // for stat history update, but one is already processing that invalidates the original await attackerStats.ProcessingHit.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken); - if (_config.EnableAdvancedMetrics) + if (statsConfig.EnableAdvancedMetrics) { await UpdateHistoricalRanking(attacker.ClientId, attackerStats, serverId); } @@ -1037,7 +1343,7 @@ public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientSt int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; - await using var ctx = _contextFactory.CreateContext(enableTracking: true); + await using var ctx = contextFactory.CreateContext(enableTracking: true); // select the rating history for client var iqHistoryLink = from history in ctx.Set() .Include(h => h.Ratings) @@ -1196,103 +1502,230 @@ public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientSt await ctx.SaveChangesAsync(); } + /// + /// Creates a point-in-time ranking snapshot for the client within their server's performance bucket. + /// Aggregates stats across all servers sharing the same bucket, weights Z-scores by playtime, + /// converts to a percentile-based performance metric, and persists as . + /// Only includes stats from servers in the same bucket that meet min-playtime and recency thresholds. + /// public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId) { - await using var context = _contextFactory.CreateContext(); - var minPlayTime = _config.TopPlayersMinPlayTime; - + var bucketConfig = await GetBucketConfig(serverId); + + await using var context = contextFactory.CreateContext(); + var oldestStateDate = DateTime.UtcNow - bucketConfig.RankingExpiration; + // Match the rest of the bucket-tolerance story: a server with no + // PerformanceBucketId FK (the universal post-update state for + // communities upgrading to this version — IW4MAdminSettings rarely + // sets PerformanceBucketCode on day one) is implicitly part of the + // default bucket. Without the OR-NULL fallback this query returned + // zero cross-server rows for default-bucket aggregation, so each + // kill rolled up only the current server's stats — leaderboard + // ratings reflected one server, not the player's combined skill. + var isDefaultBucket = PerformanceBucketCodes.IsDefault(bucketConfig.Code); var performances = await context.Set() .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 >= Extensions.FifteenDaysAgo()) - .Where(stats => stats.TimePlayed >= minPlayTime) + .Where(stat => stat.Server.PerformanceBucket.Code == bucketConfig.Code + || (isDefaultBucket && stat.Server.PerformanceBucketId == null)) + .Where(stats => stats.UpdatedAt >= oldestStateDate) + .Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds) .ToListAsync(); - if (clientStats.TimePlayed >= minPlayTime) + if (clientStats.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds) { - clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId, - clientStats.Performance); + await UpdateForServer(clientId, clientStats, context, (int)bucketConfig.ClientMinPlayTime.TotalSeconds, + bucketConfig.RankingExpiration, serverId, bucketConfig.Code); + clientStats.Server = await serverCache.FirstAsync(server => server.Id == serverId); + performances.Add(clientStats); + } - var serverRanking = await context.Set() - .Where(stats => stats.ClientId != clientStats.ClientId) - .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc( - _config.TopPlayersMinPlayTime, clientStats.ZScore, serverId)) - .CountAsync(); + if (performances.Any(performance => performance.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)) + { + await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, bucketConfig); + } + } - var serverRankingSnapshot = new EFClientRankingHistory - { - ClientId = clientId, - ServerId = serverId, - ZScore = clientStats.ZScore, - Ranking = serverRanking, - PerformanceMetric = clientStats.Performance, - Newest = true - }; + private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, + List performances, PerformanceBucketConfiguration bucketConfig) + { + // Compute the client's bucket-aggregate z-score from raw weighted Performance + // against the BUCKET log-normal distribution. + // + // Why not the previous approach (playtime-weighted average of EFClientStatistics.ZScore)? + // The ZScore column on EFClientStatistics is fitted per-server. Z-scores from + // different distributions are in different units and don't compose under + // averaging — combining them across the bucket's servers conflates unrelated + // scales. Concretely, on a low-population server (n=2 players with similar + // skill) the fitted sigma collapses toward 0, producing per-row z-scores in + // the hundreds. Averaging those into the bucket aggregate then poisoned the + // bucket-wide max, which became the rating denominator — every other player's + // rating compressed to single-digit percent of the 0-1000 range. + // + // The corrected model: take the playtime-weighted Performance composite + // (already a meaningful per-client scalar), transform once through the + // bucket-fitted log-normal. One distribution, one transform, every aggregate + // z-score directly comparable to every other within the bucket. + var totalPlaytime = performances.Sum(p => p.TimePlayed); + if (totalPlaytime <= 0) + { + return; + } - context.Add(serverRankingSnapshot); - await PruneOldRankings(context, clientId, serverId); - await context.SaveChangesAsync(); + var weightedPerformance = + performances.Sum(p => p.Performance * p.TimePlayed) / (double)totalPlaytime; + + var aggregateZScore = await serverDistributionCalculator.GetZScoreForServerOrBucket( + weightedPerformance, performanceBucket: bucketConfig.Code); + + // Rank by raw weighted Performance — this is equivalent to ranking by aggregate + // z-score (the log-normal transform is monotonically increasing in Performance), + // and lets the comparison run entirely server-side without re-deriving z's per + // candidate row. Same default-bucket OR-clause as GetNewRankingFunc so legacy + // NULL-FK rows participate alongside properly-tagged new rows. + var isDefaultBucket = PerformanceBucketCodes.IsDefault(bucketConfig.Code); + var aggregateRanking = await context.Set() + .Where(stat => stat.ClientId != clientId) + .Where(stat => stat.Server.PerformanceBucket.Code == bucketConfig.Code + || (isDefaultBucket && stat.Server.PerformanceBucketId == null)) + .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc( + (int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration)) + .GroupBy(stat => stat.ClientId) + .Where(group => group.Sum(stat => (stat.EloRating / 3.0 + stat.Skill * 2.0 / 3.0) * stat.TimePlayed) + / group.Sum(stat => stat.TimePlayed) > weightedPerformance) + .Select(c => c.Key) + .CountAsync(); - performances.Add(clientStats); + var newPerformanceMetric = await serverDistributionCalculator.GetRatingForZScore(aggregateZScore, bucketConfig.Code); - if (performances.Any(performance => performance.TimePlayed >= minPlayTime)) - { - var aggregateZScore = - performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime); - - int? aggregateRanking = await context.Set() - .Where(stat => stat.ClientId != clientId) - .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime)) - .GroupBy(stat => stat.ClientId) - .Where(group => - group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) > - aggregateZScore) - .Select(c => c.Key) - .CountAsync(); - - var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore); - - if (newPerformanceMetric == null) - { - _log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}", - clientStats.Client?.ToString(), aggregateZScore); - return; - } + if (newPerformanceMetric == null) + { + _log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}", + clientStats.Client?.ToString(), aggregateZScore); + return; + } - var aggregateRankingSnapshot = new EFClientRankingHistory - { - ClientId = clientId, - ZScore = aggregateZScore, - Ranking = aggregateRanking, - PerformanceMetric = newPerformanceMetric, - Newest = true, - }; + var performanceBucketId = await GetOrCachePerformanceBucketId(context, bucketConfig.Code); + + var aggregateRankingSnapshot = new EFClientRankingHistory + { + ClientId = clientId, + ZScore = aggregateZScore, + Ranking = aggregateRanking, + PerformanceMetric = newPerformanceMetric, + PerformanceBucketId = performanceBucketId, + Newest = true, + }; - context.Add(aggregateRankingSnapshot); + context.Add(aggregateRankingSnapshot); - await PruneOldRankings(context, clientId); - await context.SaveChangesAsync(); - } + await PruneOldRankings(context, clientId, performanceBucketCode: bucketConfig.Code); + await context.SaveChangesAsync(); + } + + private async Task UpdateForServer(int clientId, EFClientStatistics clientStats, DatabaseContext context, + int minPlayTime, TimeSpan oldestStat, long? serverId = null, string performanceBucketCode = null) + { + clientStats.ZScore = + await serverDistributionCalculator.GetZScoreForServerOrBucket(clientStats.Performance, serverId); + + // Persist ZScore back to EFClientStatistics so the distribution/maxZScore caches + // can find non-zero values on subsequent refreshes + await context.Set() + .Where(s => s.ClientId == clientStats.ClientId && s.ServerId == clientStats.ServerId) + .ExecuteUpdateAsync(s => s.SetProperty(p => p.ZScore, clientStats.ZScore)); + + var serverRanking = await context.Set() + .Where(stats => stats.ClientId != clientStats.ClientId) + .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat, + clientStats.ZScore, serverId)) + .CountAsync(); + + int? performanceBucketId = !string.IsNullOrEmpty(performanceBucketCode) + ? await GetOrCachePerformanceBucketId(context, performanceBucketCode) + : null; + + var serverRankingSnapshot = new EFClientRankingHistory + { + ClientId = clientId, + ServerId = serverId, + ZScore = clientStats.ZScore, + Ranking = serverRanking, + PerformanceMetric = clientStats.Performance, + PerformanceBucketId = performanceBucketId, + Newest = true + }; + + context.Add(serverRankingSnapshot); + await PruneOldRankings(context, clientId, serverId, performanceBucketCode); + await context.SaveChangesAsync(); + } + + /// + /// Resolves a bucket code to its database ID, caching the result for the process lifetime + /// to avoid repeated DB lookups on every ranking update. + /// + private async Task GetOrCachePerformanceBucketId(DatabaseContext context, string bucketCode) + { + if (string.IsNullOrEmpty(bucketCode)) + { + return null; + } + + if (_performanceBucketIdCache.TryGetValue(bucketCode, out var cachedId)) + { + return cachedId; } + + var bucketId = (await context.PerformanceBuckets + .FirstOrDefaultAsync(x => x.Code == bucketCode))?.PerformanceBucketId; + + if (bucketId.HasValue) + { + _performanceBucketIdCache.TryAdd(bucketCode, bucketId.Value); + } + + return bucketId; } - private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null) + /// + /// Caps ranking history entries per client/server/bucket combination to avoid unbounded growth. + /// Marks the previous "newest" entry as historical, then deletes the oldest entry when the + /// count exceeds maxRankingCount (1728 ≈ 3 days at one sample every 2.5 minutes). + /// + private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null, + string performanceBucketCode = null) { + // Pre-bucket rankings have PerformanceBucketId == null — treat them as + // part of the default bucket so the transition to bucketed ranking doesn't + // leave two Newest=true rows per client/server. + var includeNullBucket = PerformanceBucketCodes.IsDefault(performanceBucketCode); + // Defence-in-depth: DB stores Code lower-cased; normalise here so a + // capitalised caller can't silently produce 0-row prunes (would leave + // ranking history growing unbounded). + var normalizedBucket = PerformanceBucketCodes.Normalize(performanceBucketCode); + var totalRankingEntries = await context.Set() .Where(r => r.ClientId == clientId) .Where(r => r.ServerId == serverId) + .Where(r => r.PerformanceBucket.Code == normalizedBucket + || (includeNullBucket && r.PerformanceBucketId == null)) .CountAsync(); - var mostRecent = await context.Set() + var staleNewest = await context.Set() .Where(r => r.ClientId == clientId) .Where(r => r.ServerId == serverId) - .FirstOrDefaultAsync(r => r.Newest); + .Where(r => r.PerformanceBucket.Code == normalizedBucket + || (includeNullBucket && r.PerformanceBucketId == null)) + .Where(r => r.Newest) + .ToListAsync(); - if (mostRecent != null) + foreach (var stale in staleNewest) { - mostRecent.Newest = false; - context.Update(mostRecent); + stale.Newest = false; + context.Update(stale); } const int maxRankingCount = 1728; // 60 / 2.5 * 24 * 3 ( 3 days at sample every 2.5 minutes) @@ -1302,6 +1735,8 @@ private async Task PruneOldRankings(DatabaseContext context, int clientId, long? var lastRating = await context.Set() .Where(r => r.ClientId == clientId) .Where(r => r.ServerId == serverId) + .Where(r => r.PerformanceBucket.Code == normalizedBucket + || (includeNullBucket && r.PerformanceBucketId == null)) .OrderBy(r => r.CreatedDateTime) .FirstOrDefaultAsync(); @@ -1317,6 +1752,8 @@ private async Task PruneOldRankings(DatabaseContext context, int clientId, long? ///
/// Stats of the attacker /// Stats of the victim + /// Attacker + /// Victim public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats, EFClient attacker, EFClient victim) { @@ -1352,6 +1789,19 @@ public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics v attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2)); victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2)); + var attackerEloRatingFunc = + attacker.GetAdditionalProperty>("EloRatingFunction"); + + attackerStats.EloRating = + attackerEloRatingFunc?.Invoke(attacker, attackerStats) ?? attackerStats.EloRating; + + // Unused? New code. TODO: Check if needed? + //var victimEloRatingFunc = + // victim.GetAdditionalProperty>("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; @@ -1416,7 +1866,7 @@ private EFClientStatistics UpdateStats(EFClientStatistics clientStats, EFClient ? (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)); @@ -1428,10 +1878,44 @@ private EFClientStatistics UpdateStats(EFClientStatistics clientStats, EFClient } clientStats.SPM = Math.Round(clientStats.SPM, 3); - clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3); + var stdSkill = Math.Round((clientStats.SPM * KDRWeight), 3); + + var skillFunc = + client.GetAdditionalProperty>("SkillFunction"); + if (skillFunc is not null) + { + clientStats.Skill = Math.Round(skillFunc(client, clientStats), 3); + } + else if (stdSkill > 20000) + { + // Phase 1.5 zombie skill-leak: result-shape gate (mode-independent). + // MP's natural Skill ceiling on HGM was 12106 (per-bucket query + // 2026-05-07); std formula × zombie-shaped KDR produces 50k-900k. + // The earlier IsZombieServer() gate hid the leak from itself when + // gametype was stale at calc-time, so gate on the output instead. + // Write-gate stops the bleed; log captures full context to identify + // which precondition fails (CurrentServer null / Gametype stale / + // SkillFunction lost / different EFClient instance). + var leakServer = client.CurrentServer; + var leakFlag = $"ZmLog_Leak_{leakServer?.LegacyDatabaseId ?? 0}"; + if (!client.GetAdditionalProperty(leakFlag)) + { + client.SetAdditionalProperty(leakFlag, true); + _log.LogWarning( + "ZombieSkillLeak: client={Name}({ClientId}) server={Server} game={Game} gametype={Gametype} map={Map} isZombie={IsZombie} stdSkill={StdSkill} kills={Kills} deaths={Deaths} spm={SPM} kdrWeight={KDRWeight}", + client.Name, client.ClientId, leakServer?.ServerName, leakServer?.GameCode, + leakServer?.Gametype, leakServer?.CurrentMap?.Name, leakServer?.IsZombieServer(), + stdSkill, clientStats.Kills, clientStats.Deaths, clientStats.SPM, KDRWeight); + } + // leave clientStats.Skill unchanged — don't pollute further + } + else + { + clientStats.Skill = stdSkill; + } // 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 @@ -1451,7 +1935,7 @@ private EFClientStatistics UpdateStats(EFClientStatistics clientStats, EFClient public EFServerStatistics InitializeServerStats(long serverId) { - using var ctx = _contextFactory.CreateContext(enableTracking: false); + using var ctx = contextFactory.CreateContext(enableTracking: false); var serverStatsSet = ctx.Set(); var serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId); @@ -1517,7 +2001,7 @@ public async Task AddMessageAsync(int clientId, long serverId, bool sentIngame, return; } - await using var context = _contextFactory.CreateContext(enableTracking: false); + await using var context = contextFactory.CreateContext(enableTracking: false); context.Set().Add(new EFClientMessage() { ClientId = clientId, @@ -1538,7 +2022,7 @@ public async Task Sync(IGameServer gameServer, CancellationToken token) { await waiter.WaitAsync(token); - await using var context = _contextFactory.CreateContext(); + await using var context = contextFactory.CreateContext(); var serverStatsSet = context.Set(); serverStatsSet.Update(_servers[serverId].ServerStatistics); await context.SaveChangesAsync(token); diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 5d0fc156a..afe98aeda 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -18,8 +18,11 @@ using Microsoft.Extensions.Logging; using IW4MAdmin.Plugins.Stats.Client.Abstractions; using IW4MAdmin.Plugins.Stats.Events; + using Microsoft.Extensions.DependencyInjection; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; + using SharedLibraryCore.Events.Management; using SharedLibraryCore.Interfaces.Events; using Stats.Client.Abstractions; @@ -46,6 +49,7 @@ public class Plugin : IPluginV2 private readonly IServerDataViewer _serverDataViewer; private readonly StatsConfiguration _statsConfig; private readonly StatManager _statManager; + private IStatusResponse lastResponse; public static void RegisterDependencies(IServiceCollection serviceCollection) { @@ -115,16 +119,36 @@ public Plugin(ILogger logger, IDatabaseContextFactory databaseContextFac await _statManager.AddMessageAsync(messageEvent.Client.ClientId, messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token); } + + // var response = await responsePoc.GetResponse("mistralai/mixtral-8x7b-instruct", messageEvent.Message, messageEvent.Owner, lastResponse); + // Console.WriteLine(response); + //messageEvent.Owner.Broadcast("^2" + response); }; IGameEventSubscriptions.MatchEnded += OnMatchEvent; + IGameEventSubscriptions.RoundEnded += OnRoundEnded; IGameEventSubscriptions.MatchStarted += OnMatchEvent; IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; IGameEventSubscriptions.ClientKilled += OnClientKilled; IGameEventSubscriptions.ClientDamaged += OnClientDamaged; + IGameServerEventSubscriptions.ServerStatusReceived += (@event, @_) => + { + lastResponse = @event.Response; + return Task.CompletedTask; + }; IManagementEventSubscriptions.ClientCommandExecuted += OnClientCommandExecute; IManagementEventSubscriptions.Load += OnLoad; } + private async Task OnRoundEnded(RoundEndEvent roundEndedEvent, CancellationToken token) + { + await _statManager.Sync(roundEndedEvent.Server, token); + + foreach (var calculator in _statCalculators) + { + await calculator.CalculateForEvent(roundEndedEvent); + } + } + private async Task OnClientKilled(ClientKillEvent killEvent, CancellationToken token) { if (!ShouldIgnoreEvent(killEvent.Attacker, killEvent.Victim)) diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 373768fc7..4e3fb69eb 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -17,7 +17,13 @@ - + + false + + + false + + diff --git a/Plugins/ZombieStats/Events/ZombieEventParser.cs b/Plugins/ZombieStats/Events/ZombieEventParser.cs new file mode 100644 index 000000000..61c03cf62 --- /dev/null +++ b/Plugins/ZombieStats/Events/ZombieEventParser.cs @@ -0,0 +1,584 @@ +using Data.Models; +using Data.Models.Zombie; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; + +namespace IW4MAdmin.Plugins.ZombieStats.Events; + +public class ZombieEventParser(ILogger logger) +{ + private const char DataSeparator = ';'; + private readonly Dictionary> _eventParsers = new() + { + {"K", ParsePlayerKilledEvent}, + {"D", ParsePlayerDamageEvent}, + {"AD", ParseZombieDamageEvent}, + {"AK", ParseZombieKilledEvent}, + {"RD", ParsePlayerRoundDataEvent}, + {"RC", ParseRoundCompleteEvent}, + {"ZP", ParseZombieEvent}, // player-scoped: ZP;;;... + {"ZW", ParseWorldEvent}, // world-scoped: ZW;; + }; + + private const string GsePrefix = "GSE"; + + public GameEventV2? ParseScriptEvent(GameScriptEvent scriptEvent) + { + var eventArgs = scriptEvent.ScriptData.Split(DataSeparator); + + if (eventArgs.Length < 2) + { + logger.LogDebug("Ignoring {EventType} because there is not enough data {Data}", nameof(GameScriptEvent), + scriptEvent.ScriptData); + return null; + } + + // Other subsystems (latency probe, anti-cheat, live radar) emit script events + // that share the ScriptEventTriggered dispatch but use different wire formats. + // Silently skip anything not "GSE;;..." — those events have dedicated handlers. + if (eventArgs[0] != GsePrefix) + { + return null; + } + + if (!_eventParsers.TryGetValue(eventArgs[1], out var parser)) + { + logger.LogWarning("No parser registered for GSE type \"{Type}\"", eventArgs[1]); + return null; + } + + var parsedEvent = parser(scriptEvent, eventArgs[2..]); + + logger.LogDebug("Parsed GSE type {Type}", parsedEvent.GetType().Name); + + return parsedEvent; + } + + #region Combat events (unchanged) + + private static GameEventV2 ParsePlayerKilledEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + return new PlayerKilledGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + } + + private static GameEventV2 ParsePlayerDamageEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + return new PlayerDamageGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + } + + private static GameEventV2 ParseZombieDamageEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + return new ZombieDamageGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + } + + private static GameEventV2 ParseZombieKilledEvent(GameScriptEvent scriptEvent, string[] data) + { + var (victim, attacker) = ParseClientInfo(scriptEvent, data); + + return new ZombieKilledGameEvent + { + Target = victim, + Origin = attacker, + WeaponName = data[8], + Damage = Convert.ToInt32(data[9]), + MeansOfDeath = data[10], + HitLocation = data[11] + }; + } + + #endregion + + #region Round events (unchanged) + + private static GameEventV2 ParsePlayerRoundDataEvent(GameScriptEvent scriptEvent, string[] data) + { + var client = ParseVictimClient(scriptEvent, data); + + return new PlayerRoundDataGameEvent + { + Origin = client, + TotalScore = Convert.ToInt32(data[4]), + CurrentScore = Convert.ToInt32(data[5]), + CurrentRound = Convert.ToInt32(data[6]), + IsGameOver = data[7] == "1" + }; + } + + private static GameEventV2 ParseRoundCompleteEvent(GameScriptEvent scriptEvent, string[] data) + { + return new RoundEndEvent + { + RoundNumber = Convert.ToInt32(data[0]) + }; + } + + // GSE;ZW;; — unified prefix for "world-scoped" zombie events that + // don't have a single owning player. Mirrors the ZP pattern (which is the same + // idea for player-scoped events). The kind discriminator lets us add new world + // events without consuming another top-level prefix per event type. Round + // number is included on each kind that needs correlation against the live + // round (defends against the log-tail race where ZW arrives just before/after + // the matching RC and could otherwise mis-attribute). + // + // Current kinds: + // ZW;round_special;; — special-round designation + // ZW;zombies;;; — periodic spawn-state snapshot + // ZW;power;;;[player block] — power-state change + // ZW;easter_egg;step; — EE step waypoint + // ZW;easter_egg;complete; — EE canonical completion + private static GameEventV2 ParseWorldEvent(GameScriptEvent scriptEvent, string[] data) + { + var kind = data.Length > 0 ? data[0] : string.Empty; + var args = data.Length > 1 ? data[1..] : Array.Empty(); + return kind switch + { + "round_special" => ParseRoundSpecial(args), + "zombies" => ParseZombiesRemaining(args), + "power" => ParsePowerStateChange(scriptEvent, args), + "easter_egg" => ParseEasterEgg(args), + _ => throw new ArgumentException($"Unknown ZW kind: {kind}"), + }; + } + + // ZW;easter_egg;; + // step — args[1] is the canonical step key (e.g. t4_vr_radio_1) + // complete — args[1] is the map name (matches level.script) + // Folded into ZW from the legacy EE prefix; the discriminator-prefix ("step" / + // "complete") removes the previous "first-arg-could-be-anything" ambiguity. + private static GameEventV2 ParseEasterEgg(string[] args) + { + var subKind = args.Length > 0 ? args[0] : string.Empty; + if (string.Equals(subKind, "step", StringComparison.OrdinalIgnoreCase)) + { + return new EasterEggStepGameEvent + { + StepKey = args.Length > 1 ? args[1] : string.Empty + }; + } + // "complete" or anything else falls back to the canonical complete event + // (defensive — pre-rename emissions just had the map name as first arg). + return new EasterEggCompleteGameEvent + { + MapName = args.Length > 1 ? args[1] : string.Empty + }; + } + + // ZW;zombies;;; — periodic engine snapshot for live SPH. + // Emitted ~every 5s by the GSC WatchZombiesRemaining watcher when either count + // changes. args: [round, remaining, alive]. + private static GameEventV2 ParseZombiesRemaining(string[] args) + { + return new ZombiesRemainingGameEvent + { + RoundNumber = Convert.ToInt32(args[0]), + Remaining = Convert.ToInt32(args[1]), + Alive = Convert.ToInt32(args[2]), + }; + } + + // ZW;round_special;; — emitted at round-start for special rounds + // (dog/monkey/leaper). Lets the premium plugin tag the round and skip + // Seconds-Per-Horde where the static budget formula doesn't apply. args: + // [round, type]. Unknown tokens map to null (handler treats as no-op rather + // than crashing the pipeline). + private static GameEventV2 ParseRoundSpecial(string[] args) + { + var token = args.Length > 1 ? args[1] : string.Empty; + return new RoundSpecialGameEvent + { + RoundNumber = Convert.ToInt32(args[0]), + SpecialType = ZombieSpecialRoundTypeExtensions.FromGsc(token) + }; + } + + // ZW;power;;;[guid;cnum;team;name] + // state = on | off + // source = world | player + // player block present iff source == player + // No round number — power state spans the whole match, no correlation needed. + private static GameEventV2 ParsePowerStateChange(GameScriptEvent scriptEvent, string[] data) + { + var state = data[0] switch + { + "on" => PowerState.On, + "off" => PowerState.Off, + _ => throw new ArgumentException($"Unknown PWR state: {data[0]}") + }; + + var source = data[1] switch + { + "world" => PowerSource.World, + "player" => PowerSource.Player, + _ => throw new ArgumentException($"Unknown PWR source: {data[1]}") + }; + + var evt = new PowerStateChangeGameEvent + { + State = state, + Source = source + }; + + if (source == PowerSource.Player) + { + // Player block follows: data[2..5] = guid, cnum, team, name + // Reuse same field layout as ParseVictimClient but offset shifted. + var guid = data[2].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + evt.Origin = new EFClient + { + NetworkId = guid, + ClientNumber = Convert.ToInt32(data[3]), + TeamName = data[4], + CurrentAlias = new EFAlias { Name = data[5] } + }; + } + + return evt; + } + + #endregion + + #region Unified ZP (player) parser + + // Format: GSE;ZP;{guid;clientNum;team;name};{category};{action?};{...details} + // After split and eventArgs[2..], data is: [guid, clientNum, team, name, category, ...] + // (Renamed from ZE for symmetry with ZW — both are "Z" prefixes.) + private static GameEventV2 ParseZombieEvent(GameScriptEvent scriptEvent, string[] data) + { + var category = data[4]; + + return category switch + { + "down" => ParseZeDown(scriptEvent, data), + "revive" => ParseZeRevive(scriptEvent, data), + "perk" => ParseZePerk(scriptEvent, data), + "powerup" => ParseZePowerup(scriptEvent, data), + "weapon" => ParseZeWeapon(scriptEvent, data), + "box" => ParseZeBox(scriptEvent, data), + "door" => ParseZeDoor(scriptEvent, data), + "trap" => ParseZeTrap(scriptEvent, data), + "build" => ParseZeBuild(scriptEvent, data), + "gum" => ParseZeGobbleGum(scriptEvent, data), + "bank" => ParseZeBank(scriptEvent, data), + "locker" => ParseZeLocker(scriptEvent, data), + _ => throw new ArgumentException($"Unknown ZE category: {category}") + }; + } + + // ZE;{player};bank;deposit;{amount} — T6 Tranzit/Die Rise/Buried + // ZE;{player};bank;withdraw;{amount} + private static GameEventV2 ParseZeBank(GameScriptEvent scriptEvent, string[] data) + { + var action = data.Length > 5 ? data[5] : string.Empty; + var amount = data.Length > 6 && int.TryParse(data[6], out var parsed) ? parsed : 0; + + return action switch + { + "deposit" => new BankTransactionGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + IsDeposit = true, + Amount = amount + }, + "withdraw" => new BankTransactionGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + IsDeposit = false, + Amount = amount + }, + _ => throw new ArgumentException($"Unknown bank action: {action}") + }; + } + + // ZE;{player};locker;store;{weapon} — T6 Tranzit/Die Rise/Buried + // ZE;{player};locker;retrieve;{weapon} + private static GameEventV2 ParseZeLocker(GameScriptEvent scriptEvent, string[] data) + { + var action = data.Length > 5 ? data[5] : string.Empty; + var weaponName = data.Length > 6 ? data[6] : string.Empty; + + return action switch + { + "store" => new WeaponLockerGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + IsStore = true, + WeaponName = weaponName + }, + "retrieve" => new WeaponLockerGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + IsStore = false, + WeaponName = weaponName + }, + _ => throw new ArgumentException($"Unknown locker action: {action}") + }; + } + + // ZE;{player};down + private static GameEventV2 ParseZeDown(GameScriptEvent scriptEvent, string[] data) + { + return new PlayerDownedGameEvent + { + Origin = ParseVictimClient(scriptEvent, data) + }; + } + + // ZE;{revived};revive;{reviver guid;cnum;team;name} — co-op revive + // ZE;{revived};revive;self — self-revive + // T5: solo Quick Revive auto + // T6: solo QR auto, Who's Who + // T7: solo QR auto, Self Revive gobblegum + private static GameEventV2 ParseZeRevive(GameScriptEvent scriptEvent, string[] data) + { + var revived = ParseVictimClient(scriptEvent, data); + + if (data.Length > 5 && string.Equals(data[5], "self", StringComparison.Ordinal)) + { + return new PlayerRevivedGameEvent + { + Origin = revived, + Target = revived, + IsSelfRevive = true + }; + } + + var reviverGuid = data[5].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + var reviver = new EFClient + { + NetworkId = reviverGuid, + ClientNumber = Convert.ToInt32(data[6]), + TeamName = data[7], + CurrentAlias = new EFAlias { Name = data[8] } + }; + + return new PlayerRevivedGameEvent + { + Origin = reviver, + Target = revived + }; + } + + // ZE;{player};perk;buy;{perkName};{cost} + private static GameEventV2 ParseZePerk(GameScriptEvent scriptEvent, string[] data) + { + return new PlayerConsumedPerkGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + PerkName = data[6], + Cost = data.Length > 7 ? Convert.ToInt32(data[7]) : 0 + }; + } + + // ZE;{player};powerup;grab;{powerupName} + private static GameEventV2 ParseZePowerup(GameScriptEvent scriptEvent, string[] data) + { + return new PlayerGrabbedPowerupGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + PowerupName = data[6] + }; + } + + // ZE;{player};gum;{action};{bgbName};[cost] — T7 only. + // activate: player consumed an "activated" limit_type gum (Perkaholic etc.) + // take: player grabbed a gum from a BGB machine + // leave: player paid but didn't grab — cost forfeited (ghost-ball excluded) + // Auto-trigger gum types (time/rounds/event-limited) don't fire bgb_activation + // and aren't surfaced. + private static GameEventV2 ParseZeGobbleGum(GameScriptEvent scriptEvent, string[] data) + { + var action = data.Length > 5 ? data[5] : string.Empty; + var gumName = data.Length > 6 ? data[6] : string.Empty; + + return action switch + { + "activate" => new GobbleGumActivatedGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + GumName = gumName + }, + "take" => new GobbleGumTakenGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + GumName = gumName, + Cost = data.Length > 7 ? Convert.ToInt32(data[7]) : 0 + }, + "leave" => new GobbleGumAbandonedGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + GumName = gumName, + Cost = data.Length > 7 ? Convert.ToInt32(data[7]) : 0 + }, + _ => throw new ArgumentException($"Unknown ZE gum action: {action}") + }; + } + + // ZE;{player};weapon;buy;{weaponName};{cost} + // ZE;{player};weapon;upgrade;{oldWeapon};{newWeapon};{cost} + // ZE;{player};weapon;abandon;{weaponName};{cost} + private static GameEventV2 ParseZeWeapon(GameScriptEvent scriptEvent, string[] data) + { + var action = data[5]; + var client = ParseVictimClient(scriptEvent, data); + + return action switch + { + "buy" => new WeaponPurchaseGameEvent + { + Origin = client, + WeaponName = data[6], + Cost = Convert.ToInt32(data[7]) + }, + "upgrade" => new PackAPunchGameEvent + { + Origin = client, + Outcome = PackAPunchGameEvent.PaPOutcome.Upgrade, + OldWeapon = data[6], + NewWeapon = data[7], + Cost = Convert.ToInt32(data[8]) + }, + "abandon" => new PackAPunchGameEvent + { + Origin = client, + Outcome = PackAPunchGameEvent.PaPOutcome.Abandon, + OldWeapon = data[6], + Cost = Convert.ToInt32(data[7]) + }, + _ => throw new ArgumentException($"Unknown weapon action: {action}") + }; + } + + // ZE;{player};box;take;{weaponName};{cost} + // ZE;{player};box;pass;{weaponName};{cost} + // ZE;{player};box;teddy;{cost} + private static GameEventV2 ParseZeBox(GameScriptEvent scriptEvent, string[] data) + { + var action = data[5]; + var client = ParseVictimClient(scriptEvent, data); + + return action switch + { + "take" => new BoxUseGameEvent + { + Origin = client, + Outcome = BoxUseGameEvent.BoxOutcome.Take, + WeaponName = data[6], + Cost = Convert.ToInt32(data[7]) + }, + "pass" => new BoxUseGameEvent + { + Origin = client, + Outcome = BoxUseGameEvent.BoxOutcome.Pass, + WeaponName = data[6], + Cost = Convert.ToInt32(data[7]) + }, + "teddy" => new BoxUseGameEvent + { + Origin = client, + Outcome = BoxUseGameEvent.BoxOutcome.Teddy, + Cost = Convert.ToInt32(data[6]) + }, + _ => throw new ArgumentException($"Unknown box action: {action}") + }; + } + + // ZE;{player};door;buy;{cost} + private static GameEventV2 ParseZeDoor(GameScriptEvent scriptEvent, string[] data) + { + return new DoorPurchaseGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + Cost = Convert.ToInt32(data[6]) + }; + } + + // ZE;{player};trap;activate;{trapType};{cost} + private static GameEventV2 ParseZeTrap(GameScriptEvent scriptEvent, string[] data) + { + return new TrapActivateGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + TrapType = data[6], + Cost = Convert.ToInt32(data[7]) + }; + } + + // ZE;{player};build;complete;{buildableName} + private static GameEventV2 ParseZeBuild(GameScriptEvent scriptEvent, string[] data) + { + return new BuildCompleteGameEvent + { + Origin = ParseVictimClient(scriptEvent, data), + BuildableName = data[6] + }; + } + + #endregion + + #region Client parsing helpers + + private static (EFClient victim, EFClient attacker) ParseClientInfo(GameScriptEvent scriptEvent, string[] data) + { + var victim = ParseVictimClient(scriptEvent, data); + + var attackerGuid = data[4].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + var attacker = new EFClient + { + NetworkId = attackerGuid, + ClientNumber = Convert.ToInt32(data[5]), + TeamName = data[6], + CurrentAlias = new EFAlias { Name = data[7] } + }; + + return (victim, attacker); + } + + private static EFClient ParseVictimClient(GameScriptEvent scriptEvent, string[] data) + { + var victimGuid = data[0].ConvertGuidToLong(scriptEvent.Owner.EventParser.Configuration.GuidNumberStyle); + + return new EFClient + { + NetworkId = victimGuid, + ClientNumber = Convert.ToInt32(data[1]), + TeamName = data[2], + CurrentAlias = new EFAlias { Name = data[3] } + }; + } + + #endregion +} diff --git a/Plugins/ZombieStats/Plugin.cs b/Plugins/ZombieStats/Plugin.cs new file mode 100644 index 000000000..a09834231 --- /dev/null +++ b/Plugins/ZombieStats/Plugin.cs @@ -0,0 +1,332 @@ +using Data.Abstractions; +using Data.Models; +using Data.Models.Client.Stats; +using Humanizer; +using IW4MAdmin.Plugins.ZombieStats.Events; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; +using SharedLibraryCore.Events.Management; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Interfaces.Events; + +namespace IW4MAdmin.Plugins.ZombieStats; + +public class Plugin : IPluginV2 +{ + private readonly ILogger _logger; + private readonly ZombieEventParser _zombieEventParser; + private readonly IDatabaseContextFactory _contextFactory; + private readonly IZombieStatsEnhancer? _enhancer; + private readonly HashSet _knownZombieServerIds = []; + + public string Name { get; } = nameof(Plugin).Titleize(); + public string Author => "RaidMax"; + public string Version => Utilities.GetVersionAsString(); + + public Plugin(ILogger logger, ZombieEventParser zombieEventParser, + IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider) + { + _logger = logger; + _zombieEventParser = zombieEventParser; + _contextFactory = contextFactory; + _enhancer = serviceProvider.GetService(typeof(IZombieStatsEnhancer)) as IZombieStatsEnhancer; + + IManagementEventSubscriptions.Load += OnLoad; + IManagementEventSubscriptions.Unload += OnUnload; + IManagementEventSubscriptions.ClientStateAuthorized += OnClientAuthorized; + IManagementEventSubscriptions.ClientStateDisposed += OnClientDisposed; + IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; + IGameEventSubscriptions.MatchEnded += OnMatchEnded; + IGameEventSubscriptions.MatchStarted += OnMatchStarted; + } + + public static void RegisterDependencies(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + } + + private async Task OnClientDisposed(ClientStateDisposeEvent clientEvent, CancellationToken token) + { + if (!clientEvent.Client.CurrentServer.IsZombieServer()) + { + return; + } + + if (_enhancer is not null) + { + await _enhancer.OnClientDisposed(clientEvent.Client, clientEvent.Client.CurrentServer); + await _enhancer.UpdateState(token); + } + } + + private async Task OnClientAuthorized(ClientStateAuthorizeEvent clientEvent, CancellationToken token) + { + var server = clientEvent.Client.CurrentServer; + if (server is null || !server.IsZombieServer()) + { + // DIAGNOSTIC (zombie skill-leak phase 1): the server is a CoD + // zombie-capable game (T4/T5/T6/T7) but IsZombieServer returned false, + // so gametype was likely stale at auth time — SkillFunction will + // never attach for this session. One-shot per (client, server). + if (server is not null + && (server.GameCode == Reference.Game.T4 + || server.GameCode == Reference.Game.T5 + || server.GameCode == Reference.Game.T6 + || server.GameCode == Reference.Game.T7)) + { + var raceFlag = $"ZmLog_AuthRace_{server.LegacyDatabaseId}"; + if (!clientEvent.Client.GetAdditionalProperty(raceFlag)) + { + clientEvent.Client.SetAdditionalProperty(raceFlag, true); + _logger.LogWarning( + "ZombieAuthRace: client={Name}({ClientId}) server={Server} game={Game} gametype={Gametype}", + clientEvent.Client.Name, clientEvent.Client.ClientId, server.ServerName, + server.GameCode, server.Gametype); + } + } + return; + } + + var skillFunc = _enhancer?.GetSkillCalculation() ?? ((_, stats) => stats.Skill); + clientEvent.Client.SetAdditionalProperty("SkillFunction", skillFunc); + clientEvent.Client.SetAdditionalProperty("EloRatingFunction", (EFClient _, EFClientStatistics _) => 1.0); + + // DIAGNOSTIC (zombie skill-leak phase 1): confirm SkillFunction was + // attached for this client/server. One-shot per (client, server). + var attachFlag = $"ZmLog_Attached_{server.LegacyDatabaseId}"; + if (!clientEvent.Client.GetAdditionalProperty(attachFlag)) + { + clientEvent.Client.SetAdditionalProperty(attachFlag, true); + _logger.LogWarning( + "ZombieSkillFunctionAttached: client={Name}({ClientId}) server={Server}", + clientEvent.Client.Name, clientEvent.Client.ClientId, server.ServerName); + } + + if (_enhancer is not null) + { + await _enhancer.OnClientAuthorized(clientEvent.Client, clientEvent.Client.CurrentServer); + await _enhancer.UpdateState(token); + } + } + + private async Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token) + { + if (!scriptEvent.Server.IsZombieServer()) + { + return; + } + + _knownZombieServerIds.Add(scriptEvent.Server.LegacyDatabaseId); + + var parsedScriptEvent = _zombieEventParser.ParseScriptEvent(scriptEvent); + + if (parsedScriptEvent is null) + { + return; + } + + parsedScriptEvent.Owner = scriptEvent.Owner; + + // Track current round number on the server for webfront display + switch (parsedScriptEvent) + { + case PlayerRoundDataGameEvent roundData: + scriptEvent.Owner.ZombieRoundNumber = roundData.CurrentRound; + break; + case RoundEndEvent roundEnd: + scriptEvent.Owner.ZombieRoundNumber = roundEnd.RoundNumber; + break; + } + + // Bridge zombie kills/damage/deaths to the standard Stats plugin (K/D/Score/hit locations) + ConvertToStatsEvent(scriptEvent, parsedScriptEvent); + + // Forward to premium for full zombie-specific processing + if (_enhancer is not null) + { + _enhancer.ProcessEvent(parsedScriptEvent); + await _enhancer.UpdateState(token); + } + } + + private static void ConvertToStatsEvent(GameScriptEvent scriptEvent, GameEventV2 parsedScriptEvent) + { + var zombieClient = new EFClient + { + CurrentServer = scriptEvent.Owner, + CurrentAlias = new EFAlias + { + Name = "Zombie" + } + }; + zombieClient.SetAdditionalProperty("ClientStats", new EFClientStatistics()); + + switch (parsedScriptEvent) + { + case ZombieKilledGameEvent zombieKilledGameEvent: + var zkAttacker = scriptEvent.Server.ConnectedClients.FirstOrDefault(client => + client.NetworkId == zombieKilledGameEvent.Attacker.NetworkId); + if (zkAttacker == null) break; + scriptEvent.Owner.Manager.QueueEvent(new ClientKillEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Origin = zkAttacker, + Target = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = zombieKilledGameEvent.Owner + }); + break; + case ZombieDamageGameEvent zombieDamageGameEvent: + var zdAttacker = scriptEvent.Server.ConnectedClients.FirstOrDefault(client => + client.NetworkId == zombieDamageGameEvent.Attacker.NetworkId); + if (zdAttacker == null) break; + scriptEvent.Owner.Manager.QueueEvent(new ClientDamageEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Origin = zdAttacker, + Target = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = zombieDamageGameEvent.Owner + }); + break; + case PlayerKilledGameEvent playerKilledGameEvent: + var pkTarget = scriptEvent.Server.ConnectedClients.FirstOrDefault(client => + client.NetworkId == playerKilledGameEvent.Target.NetworkId); + if (pkTarget == null) break; + scriptEvent.Owner.Manager.QueueEvent(new ClientKillEvent + { + Type = GameEvent.EventType.Kill, + Data = string.Join(';', scriptEvent.ScriptData.Split(';')[1..]).TrimStart('A'), + Target = pkTarget, + Origin = zombieClient, + GameTime = scriptEvent.GameTime, + Source = GameEvent.EventSource.Log, + Owner = playerKilledGameEvent.Owner + }); + break; + } + } + + private async Task OnMatchEnded(MatchEndEvent matchEvent, CancellationToken token) + { + if (!matchEvent.Server.IsZombieServer()) + { + return; + } + + matchEvent.Owner.ZombieRoundNumber = null; + + if (_enhancer is not null) + { + await _enhancer.OnMatchEnded(matchEvent.Server); + await _enhancer.UpdateState(token); + } + } + + private async Task OnMatchStarted(MatchStartEvent matchEvent, CancellationToken token) + { + if (!matchEvent.Server.IsZombieServer()) + { + return; + } + + // Reset round display BEFORE the ConnectedClients guard. On T7x the + // game log doesn't emit ExitLevel/ShutdownGame between matches, so + // OnMatchEnded never fires; this is the only opportunity to clear + // the stale ZombieRoundNumber from the previous match. The J event + // also fires same-second as InitGame so ConnectedClients is racey + // here — clearing before the guard makes the reset deterministic. + matchEvent.Owner.ZombieRoundNumber = null; + + if (!matchEvent.Server.ConnectedClients.Any()) + { + return; + } + + _knownZombieServerIds.Add(matchEvent.Server.LegacyDatabaseId); + + if (_enhancer is not null) + { + _enhancer.OnMatchStarted(matchEvent.Server); + await _enhancer.UpdateState(token); + } + } + + private async Task OnUnload(IManager manager, CancellationToken token) + { + // Flush any queued zombie persistence so in-flight match/round/event rows + // don't leave the DB in an inconsistent state on restart. + if (_enhancer is not null) + { + try + { + await _enhancer.UpdateState(token); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to flush zombie stats on unload"); + } + } + } + + private async Task OnLoad(IManager manager, CancellationToken token) + { + _logger.LogInformation("{Plugin} by {Author} v{Version} loading...", Name, Author, Version); + + if (_enhancer is not null) + { + await _enhancer.Initialize(); + manager.CustomStatsMetrics.Add(_enhancer.GetTopStatsMetrics); + manager.CustomStatsMetrics.Add(_enhancer.GetAdvancedStatsMetrics); + manager.CustomTopStatsTransformers.Add(_enhancer.TransformTopStats); + manager.GetPageList().Pages.Add( + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_ZOMBIE_STATS_PAGE_NAME"], + "/stats/zombies"); + } + else + { + manager.CustomStatsMetrics.Add(GetPremiumUpsellMetrics); + } + } + + private async Task GetPremiumUpsellMetrics(Dictionary> meta, long? serverId, + string performanceBucketCode, bool isTopStats) + { + if (isTopStats || !meta.Any() || serverId is null) + { + return; + } + + // Check in-memory first (servers we've seen zombie events from this session) + var isZombieServer = _knownZombieServerIds.Contains(serverId.Value); + + // Fall back to DB check (zombie data may exist from a previous session with premium) + if (!isZombieServer) + { + await using var context = _contextFactory.CreateContext(false); + isZombieServer = await context.ZombieClientStatAggregates + .AnyAsync(stat => stat.ServerId == serverId); + } + + if (!isZombieServer) + { + return; + } + + meta.First().Value.Add(new EFMeta + { + Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_ZOMBIE_STATS_PREMIUM_UPSELL_KEY"], + Value = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_ZOMBIE_STATS_PREMIUM_UPSELL_VALUE"] + }); + } +} diff --git a/Plugins/ZombieStats/States/MatchState.cs b/Plugins/ZombieStats/States/MatchState.cs new file mode 100644 index 000000000..14e5a3388 --- /dev/null +++ b/Plugins/ZombieStats/States/MatchState.cs @@ -0,0 +1,127 @@ +using Data.Models.Client.Stats; +using Data.Models.Zombie; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Plugins.ZombieStats.States; + +public record MatchState(IGameServer Server, ZombieMatch PersistentMatch) +{ + /// + /// Per-round client state, keyed by (NetworkId, RoundNumber). Composite key lets + /// late-arriving end-of-round (RD) events find their original round entry even + /// after StartNextRound has advanced to the next round — eliminates the + /// Clear/repopulate race that produced Points=0 and zero-duration rounds. + /// Old-round entries remain until ; + /// memory cost is bounded by rounds × players per match. + /// + public Dictionary<(long NetworkId, int RoundNumber), RoundState> RoundStates { get; } = new(); + public Dictionary PersistentMatchAggregateStats { get; } = new(); + public Dictionary PersistentLifetimeAggregateStats { get; } = new(); + public Dictionary PersistentLifetimeServerAggregateStats { get; } = new(); + public Dictionary> PersistentStatTagValues { get; } = new(); + public int RoundNumber { get; set; } + + /// + /// Timestamp of the last StartNextRound for this match. + /// Diagnostic-only — surfaced in "Missing state data" warnings to correlate + /// an RD failure with a recent round transition that may have wiped state. + /// + public DateTimeOffset? LastStartNextRoundUtc { get; set; } + + /// + /// True when this match was created by a mid-match bootstrap path + /// (TrackClient finding no active match) and the dvar query returned + /// no usable round number, so the round / JoinedRound seeds may be wrong. + /// Cleared when a real RC event arrives and force-rebases the match. + /// + public bool BootstrapPendingFirstRound { get; set; } + + /// + /// First-observed disconnection timestamp per NetworkId. Populated lazily by the + /// live-snapshot service when a tracked player no longer appears in the server's + /// ConnectedClients list. Cleared on reconnect. Used to age disconnected players + /// out of the live modal after a TTL — they linger briefly so a brief drop is + /// visible, then disappear so the modal doesn't accumulate ghosts. + /// + public Dictionary DisconnectedFirstSeen { get; } = new(); + + /// + /// Round number captured at EE-fire time. Null when EE hasn't fired or + /// RoundNumber was 0/indeterminate at fire time. + /// + public int? EasterEggRound { get; set; } + + /// + /// UTC timestamp captured at EE-fire time. Authoritative "EE fired" signal — + /// presence is used to gate re-emit and to flag persistence. Flushed to + /// on match-end persist. + /// + public DateTimeOffset? EasterEggOccurredAt { get; set; } + + /// + /// Step keys logged for this match's EE-progress tracking. Match-level cache + /// for in-process dedup (avoid double-fire from engine notify quirks) and for + /// the "all steps fired" derivation of on + /// maps without a canonical terminal notify. Persisted state lives in the + /// event log; this is the runtime mirror. + /// + public HashSet EasterEggStepsLogged { get; } = []; + + /// + /// Quest ids (per MapEasterEggConfig) that have completed during this + /// match, either via the canonical terminal notify or via "all steps logged" + /// derivation. Match-level cache to gate canonical re-emits and re-derivations. + /// For branching quests, holds the variant id (e.g. "transit_maxis"), not the + /// group id — completion is per-variant and the group's "the EE happened in + /// this match" signal is whichever variant lands first. + /// + public HashSet EasterEggQuestsCompleted { get; } = []; + + /// + /// Hard-lock map: branching-quest group id → variant id of the first variant + /// to fire any step in the current match. Subsequent steps from sibling + /// variants are rejected at the writer (see ZombieEventProcessor.OnEasterEggStep) + /// — Maxis-then-Richtofen mid-match is impossible in the GSC (power state + /// gates), so any cross-variant step is treated as bad data and dropped. + /// Transient match-only state; not persisted (BuildQuests recovers the + /// active variant from step records at read time). + /// + public Dictionary EasterEggLockedVariantByGroup { get; } = + new(StringComparer.OrdinalIgnoreCase); + + /// + /// Player count captured at each round's start. Keyed by RoundNumber; value is the + /// count of qualifying clients when the round began (zombie spawn count is fixed + /// for the round at this number, regardless of mid-round joins/leaves). Used by + /// the round-duration EMA to key its (Map, Round, PlayerCount) cell so solo and + /// co-op rounds normalize against their own cohorts. + /// + public Dictionary RoundPlayerCounts { get; } = new(); + + /// + /// Special-round type per round number — populated from GSC + /// GSE;ZW;round_special;<round>;<type> emissions. Absence = normal round. Drives the + /// Round Breakdown UI badge and the !ztimings SPH gate (special rounds replace + /// the regular spawn budget so the static SPH formula doesn't apply). Mid-round + /// mini-bosses (panzer/brutus/mechz/ghost/sloth) are deliberately NOT recorded + /// here — those add a small fixed enemy count alongside regular zombies; SPH + /// stays approximately correct. + /// + public Dictionary RoundSpecialTypes { get; } = new(); + + /// + /// Latest engine snapshot for the current round: zombies still to be SPAWNED + /// (level.zombie_total). Updated by the GSC WatchZombiesRemaining watcher + /// every ~2s. Null between match start and the first ZR emission, or when the + /// emitted RoundNumber doesn't match the live round (stale arrival ignored). + /// Drives the live-modal SPH calculation: cleared = budget − remaining − alive. + /// + public int? CurrentRoundZombiesRemaining { get; set; } + + /// + /// Latest engine snapshot for the current round: zombies currently alive on the + /// map (get_enemy_count / get_current_zombie_count). Paired with + /// . Null until the first ZR emission. + /// + public int? CurrentRoundZombiesAlive { get; set; } +} diff --git a/Plugins/ZombieStats/States/RoundState.cs b/Plugins/ZombieStats/States/RoundState.cs new file mode 100644 index 000000000..713fbeb95 --- /dev/null +++ b/Plugins/ZombieStats/States/RoundState.cs @@ -0,0 +1,24 @@ +using Data.Models.Zombie; + +namespace IW4MAdmin.Plugins.ZombieStats.States; + +public record RoundState +{ + public ZombieRoundClientStat PersistentClientRound { get; init; } = null!; + public DateTimeOffset? DiedAt { get; set; } + public int Hits { get; set; } + + /// + /// True between a Downed event and a subsequent Revived event for this player + /// in this round. Drives the live snapshot's per-player "down" status. + /// Reset by a fresh round entry (next round, new RoundState). + /// + public bool IsDowned { get; set; } + + /// + /// Number of qualifying (kill-recording) players in the match at the moment this + /// round began. Frozen at round-start so the EMA cell key matches the round's + /// fixed zombie spawn count, regardless of mid-round joins/leaves. + /// + public int PlayerCountAtRoundStart { get; set; } +} diff --git a/Plugins/ZombieStats/ZombieStats.csproj b/Plugins/ZombieStats/ZombieStats.csproj new file mode 100644 index 000000000..15a9a9827 --- /dev/null +++ b/Plugins/ZombieStats/ZombieStats.csproj @@ -0,0 +1,29 @@ + + + + Library + net10.0 + enable + enable + IW4MAdmin.Plugins.ZombieStats + RaidMax.IW4MAdmin.Plugins.ZombieStats + RaidMax + Forever None + Zombie Statistics + Zombie Statistics Plugin for IW4MAdmin + 2026 + Debug;Release;Prerelease + Latest + false + + + + + false + + + false + + + + diff --git a/SharedLibraryCore/Configuration/ServerConfiguration.cs b/SharedLibraryCore/Configuration/ServerConfiguration.cs index 369a3456b..b2e7c1baf 100644 --- a/SharedLibraryCore/Configuration/ServerConfiguration.cs +++ b/SharedLibraryCore/Configuration/ServerConfiguration.cs @@ -47,6 +47,7 @@ public class ServerConfiguration : IBaseConfiguration [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_CUSTOM_HOSTNAME")] [ConfigurationOptional] public string CustomHostname { get; set; } + public string PerformanceBucketCode { get; set; } public IBaseConfiguration Generate() { diff --git a/SharedLibraryCore/Dtos/ServerInfo.cs b/SharedLibraryCore/Dtos/ServerInfo.cs index b87f34947..03e571212 100644 --- a/SharedLibraryCore/Dtos/ServerInfo.cs +++ b/SharedLibraryCore/Dtos/ServerInfo.cs @@ -50,7 +50,10 @@ public double? LobbyZScore } } public Reference.Game Game { get; set; } + public bool IsZombieServer { get; set; } + public int? ZombieRoundNumber { get; set; } public double? RconRoundTripMs { get; set; } - public double? GameLogPipelineMs { get; set; } + public double? GameLogIngestMs { get; set; } + public string PerformanceBucket { get; set; } } } diff --git a/SharedLibraryCore/Dtos/SideContextMenuItems.cs b/SharedLibraryCore/Dtos/SideContextMenuItems.cs index d0e346e61..8efbfccbe 100644 --- a/SharedLibraryCore/Dtos/SideContextMenuItems.cs +++ b/SharedLibraryCore/Dtos/SideContextMenuItems.cs @@ -9,6 +9,7 @@ public class SideContextMenuItem public bool IsButton { get; set; } public bool IsActive { get; set; } public bool IsCollapse { get; set; } + public bool IsSectionHeader { get; set; } public string Title { get; set; } public string Reference { get; set; } public string Icon { get; set; } diff --git a/SharedLibraryCore/Events/EventExtensions.cs b/SharedLibraryCore/Events/EventExtensions.cs index 2d7813b2a..8507aaf45 100644 --- a/SharedLibraryCore/Events/EventExtensions.cs +++ b/SharedLibraryCore/Events/EventExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Serilog; namespace SharedLibraryCore.Events; @@ -24,8 +25,15 @@ private static async Task RunHandler(Func(Func +/// Match-level event fired exactly once per match when the map's main Easter Egg +/// quest reaches its terminal state (Maxis ending on Origins, Tower of Babble on +/// Tranzit, Fly Trap on Der Riese, etc.). No specific player "owns" the EE — it's +/// a team achievement, so this event has no base. +/// +/// GSC emits exactly once via a guard flag (level.zm_stats_ee_fired) so +/// re-emit on script reload is impossible. +/// +public class EasterEggCompleteGameEvent : GameEventV2 +{ + /// The map's internal name (e.g. zm_tomb for Origins). + public string MapName { get; init; } = string.Empty; +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/EasterEggStepGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/EasterEggStepGameEvent.cs new file mode 100644 index 000000000..98adb5c1f --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/EasterEggStepGameEvent.cs @@ -0,0 +1,22 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Match-level progress marker for a single EE step (radio shot, fly-trap link, staff +/// charged, etc.). Distinct from which fires +/// once at the canonical terminal notify — steps are the granular waypoints leading +/// up to it. No specific player "owns" the step (team milestone), mirroring the +/// canonical event's no-ClientGameEvent base. +/// +/// Idempotency lives at the consumer (premium handler dedups by +/// (MatchId, StepKey)) — the GSC is allowed to fire a step once per real +/// in-game trigger, but engine quirks may double-fire and we tolerate it. +/// +public class EasterEggStepGameEvent : GameEventV2 +{ + /// + /// Globally-unique step identifier (e.g. t4_vr_radio_1). Format is + /// <game>_<map_short>_<type>_<index>; the premium plugin's + /// MapEasterEggConfig determines which keys are valid for which map. + /// + public string StepKey { get; init; } = string.Empty; +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumAbandonedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumAbandonedGameEvent.cs new file mode 100644 index 000000000..b2066c3a6 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumAbandonedGameEvent.cs @@ -0,0 +1,25 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Fired when a player paid for a Gobble Gum at a BGB machine but did not +/// grab it before the machine cycle ended (walked away / engine auto-return). +/// Cost was deducted upfront and not refunded — this is the loss event. +/// Wire shape: GSE;ZP;{player};gum;leave;{bgbName};{cost}. +/// +/// +/// Ghost-ball cycles (engine couldn't offer a gum because the selected one was +/// already used out for the match) refund the cost and are deliberately not +/// surfaced — they are not a player decision. +/// +public class GobbleGumAbandonedGameEvent : ClientGameEvent +{ + public EFClient Buyer => Origin; + + /// GSC bgb key of the gum that was offered but not taken. + public string GumName { get; init; } + + /// Cost the player forfeited. + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumActivatedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumActivatedGameEvent.cs new file mode 100644 index 000000000..27a7f9ced --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumActivatedGameEvent.cs @@ -0,0 +1,21 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Fired when a player activates a T7 Gobble Gum (BGB) of the "activated" +/// limit_type. Non-activated types (time / rounds / event-limited) do not fire +/// the underlying bgb_activation GSC notify and therefore do not surface +/// here. Wire shape: GSE;ZP;{player};gum;activate;{bgbName}. +/// +public class GobbleGumActivatedGameEvent : ClientGameEvent +{ + public EFClient Activator => Origin; + + /// + /// The GSC bgb key, e.g. zm_bgb_perkaholic / zm_bgb_anywhere_but_here. + /// Not normalised here — downstream callers decide whether to strip the + /// zm_bgb_ prefix for display. + /// + public string GumName { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumTakenGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumTakenGameEvent.cs new file mode 100644 index 000000000..222f8a4f9 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/GobbleGumTakenGameEvent.cs @@ -0,0 +1,27 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Fired when a player grabs a T7 Gobble Gum from a BGB machine (the "take" +/// branch of the machine flow). Wire shape: +/// GSE;ZP;{player};gum;take;{bgbName};{cost}. +/// +/// +/// "Leave" (player walks away without grabbing) has no engine notify, so it is +/// not surfaced. Activated-gum consumption is a separate event — +/// . +/// +public class GobbleGumTakenGameEvent : ClientGameEvent +{ + public EFClient Buyer => Origin; + + /// GSC bgb key, e.g. zm_bgb_cache_back. + public string GumName { get; init; } + + /// + /// Machine cost charged. Default 500 (base) or 3000 (Mega/Ultra rarity tiers + /// — engine bumps self.current_cost accordingly). + /// + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PackAPunchGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PackAPunchGameEvent.cs new file mode 100644 index 000000000..76c8135c9 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PackAPunchGameEvent.cs @@ -0,0 +1,30 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +// Fires when a player commits to a Pack-a-Punch buy (5000 points deducted, +// weapon placed in machine). Outcome distinguishes whether the player took +// the upgraded weapon (Upgrade — engine pap_taken notify) or walked away +// before the timeout (Abandon — engine pap_timeout notify). +// +// Matches the BoxUseGameEvent pattern — single event class with an outcome +// enum rather than separate classes per outcome. +public class PackAPunchGameEvent : ClientGameEvent +{ + public enum PaPOutcome + { + // Player took the upgraded weapon. NewWeapon is set. + Upgrade, + // Player walked away; engine timed out. NewWeapon is null/empty. + Abandon, + } + + public PaPOutcome Outcome { get; init; } + + // The weapon the player put into the machine. + public string OldWeapon { get; init; } + + // The upgraded weapon the player received. Only populated for Outcome=Upgrade + // (= OldWeapon + "_upgraded" by Treyarch convention). Null for Abandon. + public string? NewWeapon { get; init; } + + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerConsumedPerkGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerConsumedPerkGameEvent.cs new file mode 100644 index 000000000..f56f25f8b --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerConsumedPerkGameEvent.cs @@ -0,0 +1,10 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerConsumedPerkGameEvent : ClientGameEvent +{ + public EFClient Consumer => Origin; + public string PerkName { get; init; } + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDamageGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDamageGameEvent.cs new file mode 100644 index 000000000..a39780922 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDamageGameEvent.cs @@ -0,0 +1,6 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerDamageGameEvent : ClientDamageEvent +{ + +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDownedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDownedGameEvent.cs new file mode 100644 index 000000000..15d18e250 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerDownedGameEvent.cs @@ -0,0 +1,6 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerDownedGameEvent : ClientGameEvent +{ + +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerGrabbedPowerupameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerGrabbedPowerupameEvent.cs new file mode 100644 index 000000000..ebc198c6f --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerGrabbedPowerupameEvent.cs @@ -0,0 +1,9 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerGrabbedPowerupGameEvent : ClientGameEvent +{ + public EFClient Grabber => Origin; + public string PowerupName { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerKilledGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerKilledGameEvent.cs new file mode 100644 index 000000000..d5e53f5ee --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerKilledGameEvent.cs @@ -0,0 +1,9 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerKilledGameEvent : PlayerDamageGameEvent +{ + public PlayerKilledGameEvent() + { + RequiredEntity = EventRequiredEntity.Target; + } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRevivedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRevivedGameEvent.cs new file mode 100644 index 000000000..dab1eb97f --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRevivedGameEvent.cs @@ -0,0 +1,10 @@ +using Data.Models.Client; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerRevivedGameEvent : ClientGameEvent +{ + public EFClient Reviver => Origin; + public EFClient Revived => Target; + public bool IsSelfRevive { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRoundDataGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRoundDataGameEvent.cs new file mode 100644 index 000000000..5b2f56da9 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerRoundDataGameEvent.cs @@ -0,0 +1,9 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerRoundDataGameEvent : ClientGameEvent +{ + public int TotalScore { get; init; } + public int CurrentScore { get; init; } + public int CurrentRound { get; init; } + public bool IsGameOver { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs new file mode 100644 index 000000000..43316b8b7 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PlayerStatUpdatedGameEvent.cs @@ -0,0 +1,15 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class PlayerStatUpdatedGameEvent : ClientGameEvent +{ + public enum StatUpdateType + { + Absolute, + Increment, + Decrement + } + + public string StatTag { get; set; } + public int StatValue { get; set; } + public StatUpdateType UpdateType { get; set; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/PowerStateChangeGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/PowerStateChangeGameEvent.cs new file mode 100644 index 000000000..bf5ad6f09 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/PowerStateChangeGameEvent.cs @@ -0,0 +1,31 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Match-level event fired when the map's power state changes — power switch hit, +/// pylon repaired (TranZit), bus power lost, etc. Distinct from +/// player-action events because power is a world property, not a player property. +/// +/// Attribution is optional. When a player triggers the change (use trigger fired) +/// the wire format includes the player block and is populated. +/// When the world flips state (TranZit bus power loss, scripted auto-activation) +/// is null and is . +/// +public class PowerStateChangeGameEvent : GameEventV2 +{ + public PowerState State { get; init; } + public PowerSource Source { get; init; } +} + +public enum PowerState +{ + On = 0, + Off = 1 +} + +public enum PowerSource +{ + /// Game/world flipped power state without a player triggering it. + World = 0, + /// Player flipped the switch / repaired the pylon. is set. + Player = 1 +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/RoundSpecialGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/RoundSpecialGameEvent.cs new file mode 100644 index 000000000..e26e1e730 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/RoundSpecialGameEvent.cs @@ -0,0 +1,27 @@ +using Data.Models.Zombie; + +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Marks the round about to begin as a "special" round — one whose spawn budget +/// is decoupled from the normal round_spawning formula (dogs/leapers/ +/// monkeys replace the entire round's zombie pool). Lets IW4MAdmin tag the +/// round in the breakdown UI and skip the static Seconds-Per-Horde calculation +/// for round types where the formula doesn't apply. +/// +/// Mid-round mini-bosses (panzer, brutus, mechz, ghost, sloth) are NOT emitted +/// here — those add a small fixed enemy count alongside regular zombies; SPH +/// stays approximately correct without per-type compensation. +/// +public class RoundSpecialGameEvent : GameEventV2 +{ + /// The round number this special-round designation applies to. + public int RoundNumber { get; init; } + + /// + /// Parsed special-round type. Null when the GSC emitted a token the parser + /// doesn't yet recognise — handler treats unknown specials as no-op rather + /// than crashing the event pipeline. + /// + public ZombieSpecialRoundType? SpecialType { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/TrapActivateGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/TrapActivateGameEvent.cs new file mode 100644 index 000000000..7d793d297 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/TrapActivateGameEvent.cs @@ -0,0 +1,7 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class TrapActivateGameEvent : ClientGameEvent +{ + public string TrapType { get; init; } + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponLockerGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponLockerGameEvent.cs new file mode 100644 index 000000000..b1888d835 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponLockerGameEvent.cs @@ -0,0 +1,10 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +// T6 Tranzit / Die Rise / Buried weapon-locker transaction. Player swaps their +// currently-held weapon into the locker (store) or pulls a previously-stored +// weapon back out (retrieve). WeaponName is the engine weapon string. +public class WeaponLockerGameEvent : ClientGameEvent +{ + public bool IsStore { get; init; } + public string WeaponName { get; init; } = string.Empty; +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponPurchaseGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponPurchaseGameEvent.cs new file mode 100644 index 000000000..5321e6c44 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/WeaponPurchaseGameEvent.cs @@ -0,0 +1,7 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class WeaponPurchaseGameEvent : ClientGameEvent +{ + public string WeaponName { get; init; } + public int Cost { get; init; } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieDamageGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieDamageGameEvent.cs new file mode 100644 index 000000000..c20ebe051 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieDamageGameEvent.cs @@ -0,0 +1,6 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class ZombieDamageGameEvent : ClientDamageEvent +{ + +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieKilledGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieKilledGameEvent.cs new file mode 100644 index 000000000..54dce87ab --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombieKilledGameEvent.cs @@ -0,0 +1,9 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +public class ZombieKilledGameEvent : ZombieDamageGameEvent +{ + public ZombieKilledGameEvent() + { + RequiredEntity = EventRequiredEntity.Origin; + } +} diff --git a/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombiesRemainingGameEvent.cs b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombiesRemainingGameEvent.cs new file mode 100644 index 000000000..e0283d8aa --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameScript/Zombie/ZombiesRemainingGameEvent.cs @@ -0,0 +1,34 @@ +namespace SharedLibraryCore.Events.Game.GameScript.Zombie; + +/// +/// Periodic engine snapshot of the current round's zombie state — emitted from +/// the GSC WatchZombiesRemaining watcher every ~2s during an active round. +/// +/// Lets IW4MAdmin compute true "zombies cleared" = budget − remaining − alive, +/// which captures trap kills / environmental kills / friendly-grenade splash — +/// all things that don't credit to a player's kill count but still reduce the +/// round's spawn pool. Without this signal, the live-modal SPH calculation +/// systematically over-estimated pace on trap-heavy strategies. +/// +public class ZombiesRemainingGameEvent : GameEventV2 +{ + /// Round number this snapshot belongs to. Lets the handler discard + /// stale emissions from a previous round if they arrive after StartNextRound. + public int RoundNumber { get; init; } + + /// + /// Engine's level.zombie_total — count of zombies still to be SPAWNED + /// this round. Set to the round's full spawn budget at round start, decrements + /// as each zombie spawns. Reaches 0 once the spawner has dispatched everything; + /// may still be non-zero past that point. + /// + public int Remaining { get; init; } + + /// + /// Engine's currently-alive zombie count (get_enemy_count on T4/T5, + /// get_current_zombie_count on T6). Includes zombies in the process of + /// despawning between rounds, so brief stale-positive values mid-transition are + /// expected. + /// + public int Alive { get; init; } +} diff --git a/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs index 977b38a1e..3d3adf765 100644 --- a/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs +++ b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs @@ -5,5 +5,4 @@ namespace SharedLibraryCore.Events.Server; public class ServerStatusReceiveEvent : GameServerEvent { public IStatusResponse Response { get; set; } - public string RawData { get; set; } } diff --git a/SharedLibraryCore/Helpers/PerformanceBucketClassifier.cs b/SharedLibraryCore/Helpers/PerformanceBucketClassifier.cs new file mode 100644 index 000000000..1e1c50c0e --- /dev/null +++ b/SharedLibraryCore/Helpers/PerformanceBucketClassifier.cs @@ -0,0 +1,67 @@ +using Data.Abstractions; +using Data.Models.Server; +using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Interfaces; + +namespace SharedLibraryCore.Helpers; + +/// +/// Determines whether a performance bucket should be treated as a "zombies bucket" +/// based on the live server population, replacing the legacy hardcoded literal +/// "zombies" bucket-code check. A bucket counts as zombies when more than +/// of the live servers tagged to that +/// bucket pass . +/// +/// Single source of truth for both base Stats (StatManager) and the premium +/// ZombieStats plugin so admins can name buckets freely (e.g. "T4 - Zombies", +/// "Comp Zombies") without losing zombie-specific top-stats columns or the +/// KDR-suppression rule. +/// +public static class PerformanceBucketClassifier +{ + public const double DefaultZombieRatioThreshold = 0.75; + + public readonly record struct BucketClassification( + bool IsZombieBucket, + IReadOnlySet ZombieServerIds); + + public static async Task ClassifyAsync( + IManager manager, + IDatabaseContextFactory contextFactory, + string bucketCode, + double threshold = DefaultZombieRatioThreshold, + CancellationToken cancellationToken = default) + { + var normalized = PerformanceBucketCodes.Normalize(bucketCode); + var liveServers = manager.GetServers(); + if (liveServers.Count == 0) + { + return new BucketClassification(false, new HashSet()); + } + + var liveIds = liveServers.Select(s => s.LegacyDatabaseId).ToList(); + await using var ctx = contextFactory.CreateContext(false); + var bucketCodes = await ctx.Set() + .Where(s => liveIds.Contains(s.ServerId)) + .Select(s => new { s.ServerId, Code = s.PerformanceBucket != null ? s.PerformanceBucket.Code : null }) + .ToDictionaryAsync(x => x.ServerId, x => PerformanceBucketCodes.Normalize(x.Code), cancellationToken); + + var inBucket = liveServers + .Where(s => bucketCodes.TryGetValue(s.LegacyDatabaseId, out var c) && c == normalized) + .ToList(); + + if (inBucket.Count == 0) + { + return new BucketClassification(false, new HashSet()); + } + + var zombieIds = inBucket + .Where(s => s.IsZombieServer()) + .Select(s => s.LegacyDatabaseId) + .ToHashSet(); + + var ratio = (double)zombieIds.Count / inBucket.Count; + var isZombieBucket = ratio > threshold; + return new BucketClassification(isZombieBucket, zombieIds); + } +} diff --git a/SharedLibraryCore/Helpers/PerformanceBucketCodes.cs b/SharedLibraryCore/Helpers/PerformanceBucketCodes.cs new file mode 100644 index 000000000..4e86272a4 --- /dev/null +++ b/SharedLibraryCore/Helpers/PerformanceBucketCodes.cs @@ -0,0 +1,30 @@ +namespace SharedLibraryCore.Helpers; + +/// +/// Canonical handling for performance-bucket codes. A null/empty code in +/// IW4MAdminSettings.PerformanceBucketCode represents the implicit +/// "default" bucket — same logical pool as a server explicitly tagged +/// "default". Centralising the normalisation here avoids the +/// historical inconsistency where seed loops, query filters, and writers +/// each picked a different fallback ("null" vs "" vs SQL +/// NULL FK) and silently produced empty leaderboards. +/// +/// Lives in SharedLibraryCore so both base Stats and SharedLibraryCore-level +/// helpers (e.g. ) can share one +/// normaliser instead of redefining it per layer. +/// +public static class PerformanceBucketCodes +{ + public const string Default = "default"; + + /// True when represents the default bucket + /// (null, empty, or the literal "default" in any casing). + public static bool IsDefault(string code) => + string.IsNullOrEmpty(code) || string.Equals(code, Default, System.StringComparison.OrdinalIgnoreCase); + + /// Lower-cases the code (the DB writer in IW4MServer normalises on insert) + /// and collapses null/empty to . All cache keys, DB filter + /// values, and writer FK lookups must run through this. + public static string Normalize(string code) => + string.IsNullOrEmpty(code) ? Default : code.ToLowerInvariant(); +} diff --git a/SharedLibraryCore/Helpers/ServerLatencyMetrics.cs b/SharedLibraryCore/Helpers/ServerLatencyMetrics.cs index fa3e8fdb1..81b690b3f 100644 --- a/SharedLibraryCore/Helpers/ServerLatencyMetrics.cs +++ b/SharedLibraryCore/Helpers/ServerLatencyMetrics.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace SharedLibraryCore.Helpers; @@ -7,14 +9,16 @@ public class ServerLatencyMetrics(double alpha = 0.3) private readonly Lock _lock = new(); private readonly double _alpha = Math.Clamp(alpha, 0.01, 1.0); private int _rconSampleCount; - private int _logProbeSampleCount; private double _rconRtt; - private double _logPipeline; - private const int MinSamplesRequired = 3; + private const int MinRconSamples = 3; + + private const int LogIngestWindowSize = 60; + private const int MinLogIngestSamples = 10; + private readonly Queue _logIngestSamples = new(LogIngestWindowSize); /// /// EMA-smoothed RCON round-trip time in milliseconds (two-way). - /// Returns null until at least samples are collected. + /// Returns null until at least samples are collected. /// public double? RconRoundTripMs { @@ -22,42 +26,41 @@ public double? RconRoundTripMs { lock (_lock) { - return _rconSampleCount >= MinSamplesRequired ? _rconRtt : null; + return _rconSampleCount >= MinRconSamples ? _rconRtt : null; } } } /// - /// EMA-smoothed total log pipeline latency in milliseconds (one-way: dvar set → log line parsed). - /// Requires GSC companion. Returns null if not available or insufficient samples. + /// Sliding-window median of log ingest latency in milliseconds — the one-way path + /// a natural game-side log event traverses: game writes log line → GLS file poll → + /// GLS forward → IW4MAdmin parse. Does NOT include RCon out-leg (use + /// /2 to compose a probe round-trip if needed) nor + /// C#-side semaphore/flood-protect/retry. + /// Computed per-sample by subtracting estimated one-way RCon delivery (rtt/2) from + /// the measured probe-to-parse window, then aggregated as a median over the most + /// recent samples. Median rejects per-sample + /// outliers from probe-vs-poll-cycle phase aliasing. + /// Requires GSC companion + established RCon RTT. Returns null until at least + /// samples are in the window. /// - public double? GameLogPipelineMs + public double? GameLogIngestMs { get { lock (_lock) { - return _logProbeSampleCount >= MinSamplesRequired ? _logPipeline : null; - } - } - } + if (_logIngestSamples.Count < MinLogIngestSamples) + { + return null; + } - /// - /// Estimated log-only overhead in milliseconds (GameLogPipeline minus estimated one-way RCON delivery). - /// Returns null if either component is unavailable. - /// - public double? EstimatedLogOverheadMs - { - get - { - var rtt = RconRoundTripMs; - var log = GameLogPipelineMs; - if (rtt is null || log is null) - { - return null; + var sorted = _logIngestSamples.OrderBy(v => v).ToArray(); + var mid = sorted.Length / 2; + return sorted.Length % 2 == 1 + ? sorted[mid] + : (sorted[mid - 1] + sorted[mid]) / 2.0; } - - return Math.Max(0, log.Value - rtt.Value / 2.0); } } @@ -94,25 +97,20 @@ public void RecordRconRtt(double rttMs) } } - public void RecordLogProbeLatency(double totalMs) + public void RecordLogProbeLatency(double pipelineMs) { - if (totalMs < 0) + if (pipelineMs < 0) { return; } lock (_lock) { - if (_logProbeSampleCount == 0) + _logIngestSamples.Enqueue(pipelineMs); + while (_logIngestSamples.Count > LogIngestWindowSize) { - _logPipeline = totalMs; + _logIngestSamples.Dequeue(); } - else - { - _logPipeline = _alpha * totalMs + (1 - _alpha) * _logPipeline; - } - - _logProbeSampleCount++; LastLogProbeSample = DateTime.UtcNow; } } diff --git a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs index 986728fbc..b3c0d74e0 100644 --- a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs +++ b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using SharedLibraryCore.Events; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript; +using SharedLibraryCore.Events.Game.GameScript.Zombie; namespace SharedLibraryCore.Interfaces.Events; @@ -22,6 +24,12 @@ public interface IGameEventSubscriptions /// static event Func MatchEnded; + /// + /// Raised when game log prints round ended + /// typically only triggered when using a script integration + /// + static event Func RoundEnded; + /// /// Raised when game log printed that client has entered the match /// J;clientNetworkId;clientSlotNumber;clientName @@ -97,6 +105,7 @@ static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token) { 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, diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 760b14597..74c4c8653 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Concurrent; +using Data.Models; using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Events; @@ -126,5 +127,15 @@ public interface IManager /// Cancellation token /// True if server was removed successfully Task RemoveServerAsync(string serverId, bool persistConfig = false, CancellationToken token = default); + IList>, long?, string, bool, Task>> CustomStatsMetrics { get; } + + // Premium plugins can replace typed top-stats DTO fields (Kills/Deaths/KDR) + // for buckets where base EFClientStatistics figures don't represent the + // bucket domain. CustomStatsMetrics only mutates the Razor-facing metric + // dict — the typed fields back the JSON API, OG share image, and in-game + // !topstats command, which would otherwise drift from the displayed + // values. Invoked AFTER DTOs are built but BEFORE the metric-row loop + // runs so the displayed metrics naturally pick up post-transform values. + IList, long?, string, Task>> CustomTopStatsTransformers { get; } } } diff --git a/SharedLibraryCore/Interfaces/IRConConnection.cs b/SharedLibraryCore/Interfaces/IRConConnection.cs index 41cb1b0b0..d0d2262fb 100644 --- a/SharedLibraryCore/Interfaces/IRConConnection.cs +++ b/SharedLibraryCore/Interfaces/IRConConnection.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using SharedLibraryCore.RCon; @@ -15,8 +16,15 @@ public interface IRConConnection : IDisposable /// type of RCon query to perform /// optional parameter list /// + /// + /// optional callback invoked synchronously the moment the request packet + /// has been written to the wire (post semaphore + flood-protect, before any + /// wait-for-response). Receives the UTC timestamp of the send. Used by the + /// latency probe to anchor T1 to actual transmission rather than queue time. + /// Invoked once per successful socket send; on retries, fires once per attempt. + /// /// - Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default); + Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default, Action onPacketSent = null); /// /// sets the rcon parser diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index 6316276dd..680ec3ef0 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -64,8 +64,12 @@ Task> GetDvarAsync(IRConConnection connection, string dvarName, T fal /// name of DVAR to set /// value to set DVAR to /// + /// + /// optional callback invoked when the SET_DVAR packet is written to the wire. + /// See for semantics. + /// /// - Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default); + Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default, Action onPacketSent = null); /// /// executes a console command on the server diff --git a/SharedLibraryCore/Interfaces/IServerDataViewer.cs b/SharedLibraryCore/Interfaces/IServerDataViewer.cs index bd8b1be47..0457781c9 100644 --- a/SharedLibraryCore/Interfaces/IServerDataViewer.cs +++ b/SharedLibraryCore/Interfaces/IServerDataViewer.cs @@ -45,8 +45,9 @@ Task> ClientHistoryAsync(TimeSpan? overPeriod = n /// Retrieves the number of ranked clients for given server id /// /// ServerId to query on + /// /// CancellationToken /// - Task RankedClientsCountAsync(long? serverId = null, CancellationToken token = default); + Task RankedClientsCountAsync(long? serverId = null, string performanceBucketCode = null, CancellationToken token = default); } } diff --git a/SharedLibraryCore/Interfaces/ITopStatsMutable.cs b/SharedLibraryCore/Interfaces/ITopStatsMutable.cs new file mode 100644 index 000000000..3d8b4f533 --- /dev/null +++ b/SharedLibraryCore/Interfaces/ITopStatsMutable.cs @@ -0,0 +1,18 @@ +namespace SharedLibraryCore.Interfaces; + +// Narrow mutator surface exposed to top-stats transformer hooks (see +// IManager.CustomTopStatsTransformers). Lets premium plugins replace base typed +// fields on top-stats DTOs without SharedLibraryCore taking a dependency on the +// Stats plugin's TopStatsInfo type. The Stats plugin's TopStatsInfo implements +// this interface; transformers receive IList and can rewrite +// Kills/Deaths/KDR for buckets where base EFClientStatistics figures don't +// represent the bucket's domain (e.g. zombies bucket, where base bridges only +// zombie kills/damage/deaths to MP-style stats and the resulting weighted KDR +// is meaningless when a player has 0 deaths across most rows). +public interface ITopStatsMutable +{ + int ClientId { get; } + int Kills { get; set; } + int Deaths { get; set; } + double KDR { get; set; } +} diff --git a/SharedLibraryCore/Interfaces/IZombieLeaderboardService.cs b/SharedLibraryCore/Interfaces/IZombieLeaderboardService.cs new file mode 100644 index 000000000..d0b0f5594 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IZombieLeaderboardService.cs @@ -0,0 +1,167 @@ +#nullable enable +using Data.Models; + +namespace SharedLibraryCore.Interfaces; + +/// +/// Provides zombie leaderboard data. Implemented by the premium plugin. +/// When not registered in DI, the leaderboard page returns 404. +/// +public interface IZombieLeaderboardService +{ + /// + /// Returns the available games, maps, and player counts for the leaderboard navigation. + /// + Task GetLeaderboardMetadataAsync(); + + /// + /// Returns paginated leaderboard entries for a specific game/map/player-count combination. + /// + Task GetLeaderboardEntriesAsync( + Reference.Game game, int mapId, int playerCount, + int offset, int count); + + /// + /// Returns the stat records for a specific map (across all player counts). + /// + Task> GetMapRecordsAsync(Reference.Game game, int mapId); +} + +public class ZombieMapStatRecord +{ + public string Label { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string PlayerName { get; set; } = string.Empty; + public int ClientId { get; set; } +} + +public class ZombieLeaderboardMetadata +{ + public List Games { get; set; } = []; +} + +public class ZombieLeaderboardGame +{ + public Reference.Game Game { get; set; } + public string DisplayName { get; set; } = string.Empty; + public List Maps { get; set; } = []; +} + +public class ZombieLeaderboardMap +{ + public int MapId { get; set; } + public string MapName { get; set; } = string.Empty; + public List PlayerCounts { get; set; } = []; +} + +public class ZombieLeaderboardResponse +{ + public List Entries { get; set; } = []; + public int TotalCount { get; set; } +} + +public class ZombieLeaderboardEntry +{ + public int Rank { get; set; } + public int MatchId { get; set; } + public int HighestRound { get; set; } + public DateTimeOffset MatchDate { get; set; } + public string MapName { get; set; } = string.Empty; + public string? Duration { get; set; } + public string? ServerName { get; set; } + public List Players { get; set; } = []; + + /// + /// Total distinct players that participated in the match (any round entry), + /// before the leaderboard qualifier filtered the list. When this exceeds + /// .Count, the bucket entry is showing fewer players + /// than were actually in the match (e.g. a 2-player match where one player + /// joined too late or played too few rounds to qualify lands in the 1-player + /// bucket). The card surfaces this so viewers know the entry isn't a true + /// solo run. Equal to .Count for honest entries. + /// + public int TotalPlayerCount { get; set; } + + /// Round at which the EE fired (when known). Drives the "EE R{n}" titlebar badge. + public int? EasterEggRound { get; set; } + + /// Per-quest EE progress for this match's map. Empty when unconfigured. + public List EasterEggQuests { get; set; } = []; + + /// + /// Iconic buildables completed (distinct, capped at ). + /// Drives the "All Built" titlebar badge when equal to . + /// On unconfigured maps falls back to all distinct names built. + /// + public int BuildablesBuilt { get; set; } + + /// + /// Total iconic buildables on this map (per MapBuildableConfig), or null when + /// the map has no configured total. Null hides the All-Built badge. + /// + public int? BuildablesTotal { get; set; } + + /// + /// Distinct non-iconic buildables completed (side-quest / extras). Drives the "+N + /// extra" indicator alongside the All-Built badge. Always 0 on unconfigured maps. + /// + public int ExtraBuildablesBuilt { get; set; } + + /// + /// Total downs across all qualified players in this match. Drives the + /// "No-Down" titlebar badge when zero (and ≥ 5 + /// to filter trivially short matches). + /// + public int TotalDowns { get; set; } +} + +public class ZombieLeaderboardPlayer +{ + public int ClientId { get; set; } + public string Name { get; set; } = string.Empty; + public int Kills { get; set; } + public int Deaths { get; set; } + public int Downs { get; set; } + public int Revives { get; set; } + public long PointsEarned { get; set; } + public long PointsSpent { get; set; } + public int HeadshotKills { get; set; } + public long DamageDealt { get; set; } + public int DamageReceived { get; set; } + + /// + /// Rounds in this match where this player had at least one other tracked teammate. + /// Null on legacy rows. + /// + public int? AssistedRounds { get; set; } + + /// + /// First round at which this player became "solo to the end". Drives the + /// "Solo from R<N>" badge on the leaderboard card. Null when player was assisted + /// to the final round or the metric wasn't computed. + /// + public int? SoloFromRound { get; set; } + + /// + /// Whether this player passed the leaderboard qualifier (last-round-1 + ≥20% rounds). + /// False for unqualified drop-ins surfaced behind the "All Players" expander. + /// + public bool IsQualified { get; set; } + + /// + /// Aggregate round-pace ratio across this player's rounds in the match, + /// expressed as (sumPlayerSeconds - sumEmaSeconds) / sumEmaSeconds. + /// Positive = slower than typical, negative = faster. Same band thresholds + /// as per-round pace (±5% / ±15%). Null when no round had a matched EMA cell + /// (cold map / pre-EMA legacy rows). Computed on read so it stays in lock-step + /// with the dynamic population EMA — no persistence. + /// + public double? RoundDurationPaceRatio { get; set; } + + /// + /// Pace band derived from . Null when the + /// ratio is null. Lets the UI pick the same 5-colour scale as per-round + /// tinting without re-classifying client-side. + /// + public PaceBand? PaceBand { get; set; } +} diff --git a/SharedLibraryCore/Interfaces/IZombieLiveMatchService.cs b/SharedLibraryCore/Interfaces/IZombieLiveMatchService.cs new file mode 100644 index 000000000..c668876b7 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IZombieLiveMatchService.cs @@ -0,0 +1,154 @@ +#nullable enable + +using Data.Models.Zombie; + +namespace SharedLibraryCore.Interfaces; + +/// +/// Read-only access to the in-memory zombie match state for a server, for "live snapshot" +/// UIs (e.g. the home-page server card's skull-icon modal). All data is assembled from +/// in-memory state (authoritative for current values) plus DB-backed historical context +/// (recent events, completed rounds). Returns null when the server has no active match. +/// Implemented by the premium plugin. +/// +public interface IZombieLiveMatchService +{ + Task GetLiveMatchSnapshotAsync(string serverId); +} + +public class ZombieLiveMatchSnapshot +{ + public int MatchId { get; set; } + public string Map { get; set; } = string.Empty; + public string ServerName { get; set; } = string.Empty; + public int CurrentRound { get; set; } + public DateTimeOffset MatchStartedAt { get; set; } + + /// + /// Wall-clock timestamp at which the current round started — drives the + /// per-round timer in the live modal. Null if the round-start time isn't + /// available (e.g. immediately after a stitching resume, before the first + /// post-resume RC event arrives). + /// + public DateTimeOffset? CurrentRoundStartedAt { get; set; } + + /// + /// Population EMA (in seconds) for the current round at the current player + /// count. Null when no sample exists yet for the bucket — UI hides the + /// "avg" annotation. Lets the live banner render +2m43s · avg 4m12s + /// and tint the elapsed text as it crosses pace bands. + /// + public double? CurrentRoundEmaSeconds { get; set; } + + /// + /// Player count snapshot at the moment the current round began — frozen so + /// the EMA bucket lookup is stable even if a player joins/leaves mid-round. + /// + public int? CurrentRoundPlayerCount { get; set; } + + /// + /// Special-round classification for the current round (Dog/Monkey/Leaper/Thief) + /// when the round replaced the regular zombie spawn pool. Null on normal rounds. + /// Sourced live from MatchState.RoundSpecialTypes. Lets the live banner + /// surface a "Dog Round" badge so viewers immediately understand why the timer + /// is moving fast and why no pace tint is applied. + /// + public ZombieSpecialRoundType? CurrentRoundSpecialType { get; set; } + + /// + /// Engine-deterministic spawn budget for the current round (game/round/players). + /// Null on special rounds (formula doesn't apply) or unknown game. Live SPH + /// uses budget − remaining − alive to derive zombies cleared regardless of who + /// got the credit (handles trap kills, environmental kills, friendly splash). + /// + public int? CurrentRoundBudget { get; set; } + + /// + /// Engine's level.zombie_total (zombies still to be spawned this round). + /// Sourced from the GSC WatchZombiesRemaining ~2s watcher. Null until + /// the first post-round-start ZR emission arrives, or when MatchState was reset + /// across a round boundary. + /// + public int? CurrentRoundZombiesRemaining { get; set; } + + /// + /// Engine's currently-alive zombie count for the current round. Sourced from + /// the GSC WatchZombiesRemaining ~2s watcher. Null until first ZR. + /// + public int? CurrentRoundZombiesAlive { get; set; } + + public List Players { get; set; } = []; + + /// + /// Recent timeline events — current round + ~5 from previous round for context. + /// Same shape as the post-match timeline so the modal can reuse rendering helpers. + /// + public List RecentEvents { get; set; } = []; + + /// + /// Round at which the EE-complete event fired during this live match, if any. + /// Null when EE hasn't fired yet (or the map has no watcher). Surfaced as a + /// "EE R{n}" badge in the live modal. + /// + public int? EasterEggRound { get; set; } + + /// + /// Per-quest EE progress for this map. Empty when no configured quests. Each entry + /// drives one chip + mini progress strip in the live modal. + /// + public List EasterEggQuests { get; set; } = []; + + /// + /// Chronological power-state transitions during the live match (oldest first). + /// Same semantics as — empty + /// when no power events fired (no-power maps or pre-activation). + /// + public List PowerStateChanges { get; set; } = []; +} + +/// +/// Live "alive state" for a player. Drives the badge in each player card. +/// Disconnected supersedes everything else (ghost players are pruned from the +/// snapshot after a TTL anyway). +/// +public enum ZombieLivePlayerStatus +{ + Alive, + Down, + Dead, + Disconnected +} + +public class ZombieLivePlayerSnapshot +{ + public int ClientId { get; set; } + public string Name { get; set; } = string.Empty; + + /// True when the client is in the server's current ConnectedClients list. + public bool IsConnected { get; set; } + + /// Combined alive/down/dead/disconnected status — drives the per-player badge. + public ZombieLivePlayerStatus Status { get; set; } = ZombieLivePlayerStatus.Alive; + + /// The round at which this player first appeared in the match (joined-late detection). + public int? JoinedRound { get; set; } + + /// Kills accumulated this round only — resets on round transition. + public int CurrentRoundKills { get; set; } + public int CurrentRoundDowns { get; set; } + public int CurrentRoundDeaths { get; set; } + + /// Cumulative match stats — kept in sync with persisted EFZombieMatchClientStats. + public int MatchKills { get; set; } + public int MatchDeaths { get; set; } + public int MatchDowns { get; set; } + public int MatchRevives { get; set; } + public long MatchPointsEarned { get; set; } + public long MatchPointsSpent { get; set; } + public long MatchDamageDealt { get; set; } + public long MatchDamageReceived { get; set; } + public int MatchHeadshotKills { get; set; } + + /// Per-round breakdown for collapsible drill-down. Populated for completed rounds only. + public List RoundsCompleted { get; set; } = []; +} diff --git a/SharedLibraryCore/Interfaces/IZombieMatchHistoryService.cs b/SharedLibraryCore/Interfaces/IZombieMatchHistoryService.cs new file mode 100644 index 000000000..f84c7dacc --- /dev/null +++ b/SharedLibraryCore/Interfaces/IZombieMatchHistoryService.cs @@ -0,0 +1,472 @@ +#nullable enable + +using Data.Models.Zombie; + +namespace SharedLibraryCore.Interfaces; + +/// +/// Provides per-player zombie match history with round breakdowns and event timelines. +/// Implemented by the premium plugin. +/// +public interface IZombieMatchHistoryService +{ + Task> GetPlayerMatchHistoryAsync(int clientId, string? serverEndpoint, + int offset = 0, int count = 5); + + /// + /// Returns full match detail with all players' round breakdowns and event timelines. + /// Used by the leaderboard's expandable match detail view. + /// + Task GetMatchDetailAsync(int matchId); +} + +public class ZombieMatchDetail +{ + public int MatchId { get; set; } + public string Map { get; set; } = string.Empty; + public DateTimeOffset Date { get; set; } + public string? ServerName { get; set; } + public double DurationMinutes { get; set; } + public int HighestRound { get; set; } + public bool Completed { get; set; } + public List Players { get; set; } = []; + + /// + /// Distinct iconic buildables completed in this match (capped at + /// ). Sourced from event-log entries of type + /// BuildComplete, classified against MapBuildableConfig's iconic + /// list. For unconfigured maps (custom maps, T4/T5), this falls back to all + /// distinct names built — same value BuildableNames.Count. + /// + public int BuildablesBuilt { get; set; } + + /// + /// Total iconic buildables on this map (per MapBuildableConfig), or null + /// when the map has no configured total — custom maps, T4/T5 (no buildable + /// system), or unmapped maps. Null hides the denominator in the UI. + /// + public int? BuildablesTotal { get; set; } + + /// + /// Iconic buildables completed (distinct, in build order) — paired raw key + + /// resolved display name so the UI never has to call back into a config the + /// premium plugin owns. Empty on configured-but-nothing-built maps. + /// + public List BuildableNames { get; set; } = []; + + /// + /// Full iconic buildable inventory for the map — every iconic item the + /// engine could register on this map, in canonical order, regardless of + /// whether it was built. Lets the UI render a "checklist" view (muted + /// chip when not built, filled when built — mirrors the EE step grid). + /// Empty for unconfigured maps (custom maps, T4/T5 — no iconic concept); + /// the UI falls back to listing only chips + /// in that case so unconfigured maps still show what was built. + /// + public List BuildableInventory { get; set; } = []; + + /// + /// Distinct non-iconic buildables completed — side-quest items, PaP-on-Tranzit, + /// situational extras, etc. Always 0 on unconfigured maps (everything lands in + /// iconic by default). Drives the "+N extra" chip alongside the All-Built badge. + /// + public int ExtraBuildablesBuilt { get; set; } + + /// + /// Non-iconic buildables completed (distinct, in build order) — paired raw + /// key + resolved display name, same shape as . + /// Empty on unconfigured maps. + /// + public List ExtraBuildableNames { get; set; } = []; + + /// Round at which the EE fired (when known). + public int? EasterEggRound { get; set; } + + /// + /// UTC timestamp at which the EE fired. Authoritative "EE happened" signal — + /// non-null implies completed. Drives the scrubber timeline marker. + /// + public DateTimeOffset? EasterEggOccurredAt { get; set; } + + /// + /// Per-quest EE progress for this map. A map may have multiple distinct quests + /// (Der Riese ships with both the Meteor song-egg and the Fly Trap teleporter + /// quest), each tracked independently. Empty when the map has no configured + /// quests. UI renders one chip / progress card per entry. + /// + public List EasterEggQuests { get; set; } = []; + + /// + /// Chronological power-state transitions observed during the match (chronological + /// = oldest first). Stock single-switch maps yield a single On entry; TranZit can + /// produce multi-step On→Off→On chains via bus power loss / pylon repair. + /// Empty for maps without a power switch (Nacht der Untoten) — UI hides the row. + /// + public List PowerStateChanges { get; set; } = []; +} + +/// +/// One power-state transition observed during the match. +/// is null when the change wasn't player-attributed (TranZit world events, +/// scripted activation). UI shows world-attributed transitions without a name. +/// +public sealed class PowerStateChange +{ + /// true = ON, false = OFF. + public bool IsOn { get; set; } + /// Round at which the change fired. Null when fired before round 1. + public int? Round { get; set; } + /// UTC timestamp of the change (drives chronological ordering + scrubber markers). + public DateTimeOffset OccurredAt { get; set; } + /// Activating player display name; null for world-attributed events. + public string? PlayerName { get; set; } + /// Activating player ClientId; null for world-attributed events. Drives profile-link rendering. + public int? PlayerClientId { get; set; } +} + +/// +/// Per-quest progress: configured inventory + observed step records + completion +/// state. One per quest on the map; a quest +/// is "complete" iff every step in has a matching record +/// in , OR the quest is canonical-notify driven and the +/// canonical event fired. +/// +public sealed class EasterEggQuestProgress +{ + /// Quest id ("song", "flytrap"). Stable, matches MapEasterEggConfig. + public string Id { get; set; } = string.Empty; + + /// Translation key for the quest's full display title (progress card header). + public string LocKey { get; set; } = string.Empty; + + /// + /// Translation key for the quest's compact label (titlebar chip / mini-strip). + /// Always quest-specific so two chips on the same map don't both read "EE R{n}". + /// + public string ShortLocKey { get; set; } = string.Empty; + + /// Phosphor icon name for the quest's titlebar chip. + public string Icon { get; set; } = string.Empty; + + /// + /// True when the map has a terminal GSC notify for this quest (T6 main quests, + /// Der Riese fly trap). False when completion is derived from "all steps logged" + /// (T4 song eggs). + /// + public bool HasCanonicalNotify { get; set; } + + /// Configured step inventory in static order (drives checklist render). + public List Inventory { get; set; } = []; + + /// Logged step records for this quest, in fire order. + public List Steps { get; set; } = []; + + /// Total steps configured on the quest. Equal to .Count. + public int Total { get; set; } + + /// Number of distinct steps logged so far. Equal to .Count. + public int Completed { get; set; } + + /// True when the quest is fully complete (all steps logged OR canonical fired). + public bool IsComplete { get; set; } + + /// Round of the most recent step (or canonical fire). Drives partial-chip "R{n}" suffix. + public int? LastRound { get; set; } + + /// Round at which the quest was completed. Null until complete. + public int? CompletedRound { get; set; } + + /// UTC timestamp when the quest completed. Null until complete. + public DateTimeOffset? CompletedAt { get; set; } + + /// + /// For branching quests (Maxis-vs-Richtofen on TranZit etc.): the group id + /// shared by both variants. Null on single-variant quests. , + /// , etc. on a branching entry refer to the GROUP — the + /// active variant's identity lives in / + /// below. Lifetime EE counts dedupe by this + /// group id, so completing Maxis in one match and Richtofen in another counts + /// as 2 EE completions of the same achievement, not 2 separate achievements. + /// + public string? BranchGroupId { get; set; } + + /// + /// On branching quests: the variant locked in this match (first variant to + /// fire any step). Null when the group has had no steps logged yet (no + /// variant chosen) and on non-branching quests. Steps/Inventory/Total/etc. + /// reflect this variant when set; show 0/null counts when unset. + /// + public string? ActiveVariantId { get; set; } + + /// Translation key for the active variant's full title (e.g. "Maxis path"). + public string? ActiveVariantLocKey { get; set; } + + /// Translation key for the active variant's compact label (chip subtitle). + public string? ActiveVariantShortLocKey { get; set; } +} + +public sealed class EasterEggStepInventoryEntry +{ + /// Step key — matches when fired. + public string Key { get; set; } = string.Empty; + + /// Translation key for the step's display label. + public string LocKey { get; set; } = string.Empty; + + /// Phosphor icon name (e.g. "ph-radio") for the step's visual marker. + public string Icon { get; set; } = string.Empty; +} + +public sealed class EasterEggStepRecord +{ + /// Step key (e.g. "t4_vr_radio_1"). Matches MapEasterEggConfig entries. + public string Key { get; set; } = string.Empty; + + /// Round number at which the step fired. Null when fired pre-round-1. + public int? RoundNumber { get; set; } + + /// UTC timestamp the step was logged. + public DateTimeOffset OccurredAt { get; set; } +} + +public class ZombieMatchDetailPlayer +{ + public int ClientId { get; set; } + public string Name { get; set; } = string.Empty; + public int Kills { get; set; } + public int Deaths { get; set; } + public int Downs { get; set; } + public int Revives { get; set; } + public long PointsEarned { get; set; } + + /// + /// Total points spent by this player across the match (Pack-a-Punch, doors, + /// box draws, perks, traps, etc). Surfaces in the per-player stat card as + /// the subtitle to Net Points so the user can see earned-vs-spent context. + /// + public long PointsSpent { get; set; } + + public int Headshots { get; set; } + + /// + /// Subset of that were headshots. Used to compute the + /// "HS%" stat (HeadshotKills / Kills * 100) — same metric the leaderboard + /// scoreboard surfaces. is total headshot HITS + /// (which can exceed kills when a body absorbs multiple HS hits before + /// dying); HeadshotKills is the kill-attribution subset. + /// + public int HeadshotKills { get; set; } + + public long DamageDealt { get; set; } + public int DamageReceived { get; set; } + + /// + /// Rounds in this match where this player had at least one other tracked teammate. + /// Null on legacy matches. + /// + public int? AssistedRounds { get; set; } + + /// + /// First round at which the player became "solo to the end". Drives the + /// "Solo from R<N>" badge. Null if the player was assisted to the final round. + /// + public int? SoloFromRound { get; set; } + + /// + /// Whether this player meets the canonical match-qualifier rule (same one driving + /// the leaderboard listing). Drives the scrubber's default lane visibility — the + /// timeline shows qualified players first, with a toggle to surface drop-ins. + /// + public bool IsQualified { get; set; } + + public List Rounds { get; set; } = []; + public List Events { get; set; } = []; +} + +public class ZombieMatchHistoryMatch +{ + public int MatchId { get; set; } + public string Map { get; set; } = string.Empty; + public string Date { get; set; } = string.Empty; + public string? ServerName { get; set; } + public int HighestRound { get; set; } + public double DurationMinutes { get; set; } + public long Kills { get; set; } + public long Deaths { get; set; } + public long PointsEarned { get; set; } + public bool Completed { get; set; } + + /// Round at which the EE fired (when known). + public int? EasterEggRound { get; set; } + + /// + /// UTC timestamp at which the EE fired. Non-null implies completed. + /// + public DateTimeOffset? EasterEggOccurredAt { get; set; } + + /// Per-quest EE progress for this match's map. Empty when unconfigured. + public List EasterEggQuests { get; set; } = []; + + /// Iconic buildables completed (capped at ; falls back to all distinct on unconfigured maps). + public int BuildablesBuilt { get; set; } + + /// Total iconic buildables on this map (per MapBuildableConfig), or null. + public int? BuildablesTotal { get; set; } + + /// Distinct non-iconic buildables completed (side-quest / extras). 0 on unconfigured maps. + public int ExtraBuildablesBuilt { get; set; } + + /// This player's total downs across the match — drives the "Personal No-Down" badge. + public int Downs { get; set; } + + /// + /// Rounds in this match where this player had at least one other tracked teammate. + /// Null on legacy matches that pre-date the metric. + /// + public int? AssistedRounds { get; set; } + + /// + /// First round at which the player became "solo to the end". Drives the + /// "Solo from R<N>" badge. Null if the player was assisted to the final round + /// or the metric wasn't computed for this match (legacy data). + /// + public int? SoloFromRound { get; set; } + + public List Rounds { get; set; } = []; + public List Events { get; set; } = []; + + /// + /// Power-state transitions during this match. Same shape and semantics as + /// . Empty for maps without power. + /// Surfaces on the per-client scrubber as match-level marker(s). + /// + public List PowerStateChanges { get; set; } = []; +} + +public class ZombieMatchHistoryRound +{ + public int RoundNumber { get; set; } + public long Kills { get; set; } + public long Deaths { get; set; } + public long Downs { get; set; } + public long Revives { get; set; } + public int Points { get; set; } + public double DurationSeconds { get; set; } + + /// + /// Player count snapshot at the moment this round began — frozen so the + /// EMA cell key matches the round's fixed zombie spawn count regardless of + /// mid-round joins/leaves. Null on legacy rounds pre-dating the column + /// (readers can fall back to match-level PlayerCount as a best-guess). + /// + public int? PlayerCountAtRoundStart { get; set; } + + /// + /// Pace classification of this round's duration vs the population EMA for + /// (map, round, player count). Null when no EMA sample exists yet for the + /// bucket — UI renders neutral. Computed server-side; the UI just maps + /// the band to a colour. + /// + public PaceBand? PaceBand { get; set; } + + /// + /// Signed delta vs typical, expressed as a fraction (0.12 = 12% slower than + /// typical, -0.08 = 8% faster). Null when is null. + /// Surfaces in the tooltip alongside the band colour. + /// + public double? PaceRatio { get; set; } + + /// + /// EMA value (in seconds) the round was compared against — the "typical" + /// duration shown in the tooltip. Null when is null. + /// + public double? PaceTypicalSeconds { get; set; } + + /// + /// Special-round classification when the round replaced the regular zombie + /// spawn pool (Dog / Monkey / Leaper). Null on normal rounds. Sourced from + /// GSC GSE;RS emissions captured at round start. Drives the Round + /// Breakdown UI badge. + /// + public ZombieSpecialRoundType? SpecialType { get; set; } +} + +/// +/// Round-pace classification bands. Ordered fastest → slowest. Maps to a +/// 5-colour scale on the webfront (deep green → grey → deep red). +/// +public enum PaceBand +{ + /// > 15% faster than the population EMA. + MuchFaster, + + /// 5–15% faster than the population EMA. + Faster, + + /// Within ±5% of the population EMA — typical pace. + Neutral, + + /// 5–15% slower than the population EMA. + Slower, + + /// > 15% slower than the population EMA. + MuchSlower, +} + +/// +/// A pre-processed, display-ready event for the match timeline. +/// All mapping from internal event types to labels/categories is done by the provider. +/// +public class ZombieMatchHistoryEvent +{ + /// Display time string (e.g. "20:03:05"). + public string Time { get; set; } = string.Empty; + + /// Time as total seconds from midnight, for timeline positioning. + public double Seconds { get; set; } + + /// Human-readable label (e.g. "Downed", "Double Points", "Round 5"). + public string Label { get; set; } = string.Empty; + + /// + /// Visual category for rendering. One of: "round", "powerup", "danger", "critical", "success", + /// "perk", "weapon", "box", "box-pass", "door", "trap", "build", "session-join", "session-leave". + /// The component maps these to colors/icons. + /// + public string Category { get; set; } = string.Empty; + + /// + /// For round-category events, the round number this marker represents. Lets the + /// timeline component compute gap ranges (where the player skipped rounds) without + /// parsing the string. Null for non-round events. + /// + public int? RoundNumber { get; set; } + + /// + /// Pace band for the round this event represents (round-category events only). + /// Lets the timeline tint the round marker AND surface a "vs avg" annotation + /// so live viewers see whether each completed round was on/off pace at a glance. + /// Null on non-round events or when no EMA sample exists yet. + /// + public PaceBand? PaceBand { get; set; } + + /// Signed delta vs typical (0.12 = 12% slower). Null when is null. + public double? PaceRatio { get; set; } + + /// EMA value (in seconds) the round was compared against. Null when is null. + public double? PaceTypicalSeconds { get; set; } +} + +/// +/// One buildable/craftable, paired with its community-recognisable display name. +/// is the GSC-internal name as registered via +/// add_zombie_buildable / add_zombie_craftable and stored in +/// BuildComplete.TextualValue; is what the +/// webfront renders. Pairing them in the DTO means the UI never has to know +/// about premium-side display config. +/// +public sealed class BuildableEntry +{ + public string Key { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; +} diff --git a/SharedLibraryCore/Interfaces/IZombieStatsEnhancer.cs b/SharedLibraryCore/Interfaces/IZombieStatsEnhancer.cs new file mode 100644 index 000000000..527f8f7df --- /dev/null +++ b/SharedLibraryCore/Interfaces/IZombieStatsEnhancer.cs @@ -0,0 +1,80 @@ +using Data.Models; +using Data.Models.Client; +using Data.Models.Client.Stats; +using SharedLibraryCore.Events.Game; + +namespace SharedLibraryCore.Interfaces; + +/// +/// Optional interface for premium zombie stats functionality. +/// When implemented and registered in DI, enables full zombie stat tracking, +/// skill scoring, rolling averages, and rich webfront metrics. +/// Without this, the free plugin only bridges zombie kills/damage/deaths +/// to the standard Stats plugin (K/D/Score like MP). +/// +public interface IZombieStatsEnhancer +{ + /// + /// Called once on plugin load to initialize caches and DB state. + /// + Task Initialize(); + + /// + /// Process a parsed zombie game event (kills, deaths, downs, revives, + /// perks, powerups, round data, stat updates, etc.). + /// + void ProcessEvent(GameEventV2 parsedEvent); + + /// + /// Called when a client connects to a zombie server. + /// Sets up match/round state, loads aggregate stats from DB. + /// + Task OnClientAuthorized(EFClient client, IGameServer server); + + /// + /// Called when a client disconnects from a zombie server. + /// Finalizes round state and cleans up tracking. + /// + Task OnClientDisposed(EFClient client, IGameServer server); + + /// + /// Called when a new match starts on a zombie server. + /// + void OnMatchStarted(IGameServer server); + + /// + /// Called when a match ends on a zombie server. + /// + Task OnMatchEnded(IGameServer server); + + /// + /// Persists all pending state changes to the database. + /// + Task UpdateState(CancellationToken token); + + /// + /// Returns the skill calculation function for zombie clients. + /// + Func GetSkillCalculation(); + + /// + /// Provides zombie-specific metrics for the top stats leaderboard page. + /// + Task GetTopStatsMetrics(Dictionary> meta, + long? serverId, string performanceBucketCode, bool isTopStats); + + /// + /// Provides advanced zombie metrics for the player stats page. + /// + Task GetAdvancedStatsMetrics(Dictionary> meta, + long? serverId, string performanceBucketCode, bool isTopStats); + + /// + /// Replaces typed Kills/Deaths/KDR fields on top-stats DTOs with zombie-domain + /// values from the zombie aggregate store. Invoked via + /// on the zombies bucket + /// only — base values (bridged MP-style stats) stay in place for other buckets. + /// + Task TransformTopStats(IList stats, long? serverId, + string performanceBucketCode); +} diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 86efa788a..6b4629eaf 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -85,6 +85,9 @@ public Server(ILogger logger, Interfaces.ILogger deprecatedLogger, RConConnectionFactory = rconConnectionFactory; ServerLogger = logger; DefaultSettings = serviceProvider.GetRequiredService(); + PerformanceCode = string.IsNullOrWhiteSpace(ServerConfig.PerformanceBucketCode) + ? "default" + : ServerConfig.PerformanceBucketCode.ToLowerInvariant(); InitializeTokens(); InitializeAutoMessages(); } @@ -130,6 +133,11 @@ public string Hostname public Map CurrentMap { get; set; } public Map Map => CurrentMap; + /// + /// Current zombie round number. Null when not in a zombie match. + /// + public int? ZombieRoundNumber { get; set; } + public int ClientNum { get { return IsErrorState ? 0 : Clients.Count(p => p != null && (Utilities.IsDevelopment || !p.IsBot)); } @@ -167,6 +175,7 @@ public int ClientNum public bool IsInitialized { get; set; } public int Port { get; protected set; } public int ListenPort => Port; + public string PerformanceCode { get; init; } public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty); public abstract Task ExecuteCommandAsync(string command, CancellationToken token = default); public abstract Task SetDvarAsync(string name, object value, CancellationToken token = default); diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index ea99601e4..629c46a39 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -41,7 +41,8 @@ public class ClientService : IEntityService, IResourceQueryHelper client.NetworkId == networkId && client.GameName == game) ); diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 273bc0102..91100721d 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -1,20 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using Data.Models; using Humanizer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos.Meta; @@ -222,9 +213,12 @@ public static string FormatMessageForEngine(this string str, IRConParserConfigur /// /// /// - public static bool IsZombieServer(this Server server) + public static bool IsZombieServer(this Server server) => (server as IGameServer).IsZombieServer(); + + public static bool IsZombieServer(this IGameServer server) { - return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) && + return new[] { Reference.Game.T4, Reference.Game.T5, Reference.Game.T6, Reference.Game.T7 }.Contains(server.GameCode) && + !string.IsNullOrEmpty(server.Gametype) && ZmGameTypes.Contains(server.Gametype.ToLower()); } @@ -511,7 +505,7 @@ public static Game GetGame(string gameName) public static TimeSpan ParseTimespan(this string input) { - var expressionMatch = Regex.Match(input, @"^([0-9]{1,5})(\p{L}+)"); + var expressionMatch = Regex.Match(input, @"([0-9]+)(\w+)"); if (!expressionMatch.Success) // fallback to default tempban length of 1 hour { @@ -836,9 +830,9 @@ public static async Task> GetMappedDvarValueOrDefaultAsync(this Serve } public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue, - CancellationToken token) + CancellationToken token, Action onPacketSent = null) { - await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token); + await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token, onPacketSent); } public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue) @@ -1231,6 +1225,11 @@ public static string ToNumericalString(this int value) return value.ToString("#,##0", CurrentLocalization.Culture); } + public static string ToNumericalString(this long value) + { + return value.ToString("#,##0", CurrentLocalization.Culture); + } + public static string ToNumericalString(this double value, int precision = 0) { return value.ToString( diff --git a/WebfrontCore/Components/App.razor b/WebfrontCore/Components/App.razor index 58c9423be..ecd77e1e7 100644 --- a/WebfrontCore/Components/App.razor +++ b/WebfrontCore/Components/App.razor @@ -11,7 +11,7 @@ - + } else { @@ -41,10 +41,14 @@ + } else { + @* zombie-scrubber loads standalone (kept out of the main bundle for cache + isolation — UI changes ship without invalidating app.min.js). *@ + } diff --git a/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor index c8f287d11..cf295a7d3 100644 --- a/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor +++ b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor @@ -140,7 +140,7 @@ @if (announcement.IsActive) { - @Loc["WEBFRONT_ANNOUNCEMENT_ACTIVE"] + @Loc["WEBFRONT_ANNOUNCEMENT_ACTIVE"] } else diff --git a/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor b/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor index 0cb96e257..2af29da6b 100644 --- a/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor +++ b/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor @@ -310,17 +310,17 @@ } - -
- @if (_isLoading) - { -
- } - else if (HasMoreResults) - { -
- } -
+ @if (HasMoreResults) + { + + +
+
+
+ } } diff --git a/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor.cs b/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor.cs index 80c11ed2d..594466954 100644 --- a/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor.cs +++ b/WebfrontCore/Components/Features/Admin/Pages/AuditLog.razor.cs @@ -2,18 +2,16 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.JSInterop; using SharedLibraryCore.Dtos; using WebfrontCore.Core.Auth; using WebfrontCore.Core.Services; namespace WebfrontCore.Components.Features.Admin.Pages; -public partial class AuditLog : IAsyncDisposable +public partial class AuditLog { [Inject] public required IWebfrontDataService DataService { get; set; } [Inject] public required AppState AppState { get; set; } - [Inject] public required IJSRuntime JS { get; set; } [Inject] public required NavigationManager Navigation { get; set; } [Inject] public required ILogger Logger { get; set; } @@ -67,7 +65,6 @@ private bool HasMoreResults private bool _isLoading; private string? _error; - private DotNetObjectReference? _dotNetRef; // Pagination private int Offset @@ -164,15 +161,6 @@ protected override async Task OnInitializedAsync() await LoadStatistics(); } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - _dotNetRef = DotNetObjectReference.Create(this); - await JS.InvokeVoidAsync("window.infiniteScroll.initialize", _dotNetRef, "loadMoreAuditTrigger"); - } - } - private void ParseQueryParameters() { var uri = new Uri(Navigation.Uri); @@ -358,8 +346,7 @@ private async Task HandleSearchKeyDown(KeyboardEventArgs e) } } - [JSInvokable] - public async Task LoadMore() + private async Task LoadMore() { if (!HasMoreResults || _isLoading) return; @@ -368,22 +355,6 @@ public async Task LoadMore() StateHasChanged(); } - public async ValueTask DisposeAsync() - { - if (_dotNetRef is not null) - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - _dotNetRef.Dispose(); - } - catch (Exception ex) when (ex is InvalidOperationException or JSDisconnectedException) - { - // Ignored - } - } - } - public class AuditLogState { public List Results { get; set; } = []; diff --git a/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor b/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor index 6095680e5..5808faf24 100644 --- a/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor +++ b/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor @@ -1,6 +1,5 @@ @page "/Admin/BanManagement" @page "/manage-bans" -@implements IAsyncDisposable @attribute [Authorize(Policy = "Permissions.BanManagementPage.Read")] @rendermode InteractiveServer @@ -277,16 +276,17 @@ } -
@if (HasMoreResults && Results != null && Results.Any()) { -
- @if (IsLoading) - { + + - } -
+ + } @if (!HasMoreResults && Results != null && Results.Any()) { diff --git a/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor.cs b/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor.cs index d1ff60250..706082572 100644 --- a/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor.cs +++ b/WebfrontCore/Components/Features/Admin/Pages/BanManagement.razor.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; using SharedLibraryCore; using SharedLibraryCore.Configuration; using WebfrontCore.Core.QueryHelpers.Models; @@ -12,7 +11,6 @@ public partial class BanManagement [Inject] public required IWebfrontDataService DataService { get; set; } [Inject] public required AppState AppState { get; set; } [Inject] public required IToastService ToastService { get; set; } - [Inject] public required IJSRuntime JS { get; set; } [Inject] public required ApplicationConfiguration AppConfig { get; set; } private BanInfoRequest Request { get; set; } = new(); @@ -23,18 +21,6 @@ public partial class BanManagement private long TotalCount { get; set; } private string? ValidationError { get; set; } - private DotNetObjectReference? _dotNetRef; - - protected override Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - _dotNetRef = DotNetObjectReference.Create(this); - } - - return Task.CompletedTask; - } - private async Task Search() { HasSearched = true; @@ -59,23 +45,9 @@ private async Task Search() TotalCount = result?.TotalResultCount ?? 0; HasMoreResults = Results.Count >= Request.Count && Results.Count < TotalCount; StateHasChanged(); - - // Initialize infinite scroll after first results are loaded - if (_dotNetRef != null && HasMoreResults) - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.initialize", _dotNetRef, "loadMoreBansTrigger"); - } - catch (InvalidOperationException) - { - // JS interop not available - safe to ignore - } - } } - [JSInvokable] - public async Task LoadMore() + private async Task LoadMore() { if (IsLoading || !HasMoreResults) return; @@ -96,34 +68,11 @@ public async Task LoadMore() if (result?.RetrievedResultCount < Request.Count || Results.Count >= TotalCount) { HasMoreResults = false; - // Disconnect the observer when no more results - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - } - catch (InvalidOperationException) - { - // JS interop not available - safe to ignore - } } StateHasChanged(); } - public async ValueTask DisposeAsync() - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - } - catch (Exception ex) when (ex is JSDisconnectedException or InvalidOperationException) - { - // JS interop not available during static rendering - safe to ignore - } - - _dotNetRef?.Dispose(); - } - private int _unbanTargetId; private string _unbanReason = string.Empty; private bool _showUnbanModal; diff --git a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor index 21c1d51cd..6c021ebfb 100644 --- a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor +++ b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor @@ -80,7 +80,7 @@ else break; default:
- Unknown Meta Type: @item.GetType().Name (@item.Type) + @AppState.Loc("WEBFRONT_META_ERROR_UNKNOWN_TYPE").FormatExt(item.GetType().Name, item.Type)
break; } @@ -91,7 +91,10 @@ else @if (HasMore) { -
+ } } diff --git a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor.cs b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor.cs index 166af0c22..0223b840f 100644 --- a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor.cs +++ b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor.cs @@ -1,16 +1,14 @@ using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Interfaces; using WebfrontCore.Core.Services; namespace WebfrontCore.Components.Features.Clients.Components; -public partial class ClientMetaList : IAsyncDisposable +public partial class ClientMetaList { [Inject] public required IWebfrontDataService DataService { get; set; } [Inject] public required AppState AppState { get; set; } - [Inject] public required IJSRuntime JS { get; set; } [Inject] public required ILogger Logger { get; set; } [Parameter] public int ClientId { get; set; } @@ -25,8 +23,6 @@ public partial class ClientMetaList : IAsyncDisposable private bool HasMore { get; set; } = true; private int _previousClientId; private MetaType? _previousMetaFilter; - private DotNetObjectReference? _objRef; - private bool _observerSetup; // State container for individual meta items (expansion, loading, extra data) private Dictionary _itemStates = new(); @@ -56,7 +52,6 @@ protected override async Task OnParametersSetAsync() HasMore = true; _previousClientId = ClientId; _previousMetaFilter = MetaFilterType; - _observerSetup = false; } if (MetaItems.Count == 0) @@ -65,16 +60,6 @@ protected override async Task OnParametersSetAsync() } } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (MetaItems.Count != 0 && HasMore && !_observerSetup) - { - _objRef = DotNetObjectReference.Create(this); - await JS.InvokeVoidAsync("window.infiniteScroll.initialize", _objRef, "loadMoreMetaTrigger"); - _observerSetup = true; - } - } - private async Task LoadData() { if (Loading || !HasMore) @@ -148,30 +133,12 @@ private async Task LoadData() } } - [JSInvokable] - public async Task LoadMore() + private async Task LoadMore() { await LoadData(); StateHasChanged(); } - public async ValueTask DisposeAsync() - { - if (_observerSetup) - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - } - catch (Exception ex) when (ex is InvalidOperationException or JSDisconnectedException) - { - // ignored - } - } - - _objRef?.Dispose(); - } - // --- Item Logic --- private MetaItemState GetState(object item) diff --git a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor index 3a89a2246..d3d4647d7 100644 --- a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor +++ b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor @@ -1,6 +1,5 @@ @page "/Client/AdvancedFind" @page "/find-client" -@implements IAsyncDisposable @rendermode InteractiveServer @AppState.Loc("WEBFRONT_SEARCH_RESULTS_TITLE") | @AppState.WebfrontBranding @@ -133,7 +132,7 @@ {
-
No results found
+
@AppState.Loc("WEBFRONT_SEARCH_NO_RESULTS")
} else @@ -160,7 +159,7 @@ @client.ClientLevel
- Last Seen + @AppState.Loc("WEBFRONT_ADVANCED_FIND_LABEL_LAST_SEEN") @client.LastConnection.HumanizeForCurrentCulture()
@@ -187,24 +186,26 @@ - -
- @if (_isLoading && Results.Count > 0) - { -
- - @AppState.Loc("WEBFRONT_LOADING") -
- } - else if (_hasMore) - { -
- } - else if (Results.Count > 0) - { + @if (_hasMore) + { + + +
+ + @AppState.Loc("WEBFRONT_LOADING") +
+
+
+ } + else if (Results.Count > 0) + { +
@AppState.Loc("WEBFRONT_SEARCH_NO_MORE_RESULTS")
- } -
+
+ } @code {} diff --git a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor.cs b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor.cs index 2b3ce2ab3..6b4871cfc 100644 --- a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor.cs +++ b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor.cs @@ -4,7 +4,6 @@ using SharedLibraryCore.Dtos; using WebfrontCore.Core.QueryHelpers.Models; using WebfrontCore.Core.Services; -using Microsoft.JSInterop; using SharedLibraryCore; using SharedLibraryCore.Configuration; @@ -119,20 +118,7 @@ private bool IsStateMatch(AdvancedFindState state) state.SortColumn == SortColumn; } - [Inject] public required IJSRuntime JS { get; set; } - private DotNetObjectReference? _dotNetRef; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - _dotNetRef = DotNetObjectReference.Create(this); - await JS.InvokeVoidAsync("window.infiniteScroll.initialize", _dotNetRef, "loadMoreTrigger"); - } - } - - [JSInvokable] - public async Task LoadMore() + private async Task LoadMore() { if (_isLoading || !_hasMore || State == null) return; @@ -227,20 +213,6 @@ private async Task LoadDataAsync() } } - public async ValueTask DisposeAsync() - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - } - catch (Exception ex) when (ex is InvalidOperationException or JSDisconnectedException) - { - // Ignored - } - - _dotNetRef?.Dispose(); - } - public class AdvancedFindState { public List Results { get; set; } = []; diff --git a/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor b/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor index 714484d88..a815eef77 100644 --- a/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor +++ b/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor @@ -36,7 +36,7 @@ + Value="@(Stats.PerformanceBucket != null ? System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(Stats.PerformanceBucket.ToLower()) : Stats.Servers.FirstOrDefault(server => server.Endpoint == Stats.ServerEndpoint)?.Name ?? AppState.Loc("WEBFRONT_STATS_INDEX_ALL_SERVERS"))"/> @@ -150,7 +150,76 @@ @RenderStatCard(AppState.Loc("WEBFRONT_ADV_STATS_TOTAL_ACTIVE_TIME"), Stats.ActiveTime?.HumanizeForCurrentCulture()) + @{ + var metricGroups = Stats.CustomMetrics + .GroupBy(m => string.IsNullOrEmpty(m.Extra) ? "General" : m.Extra) + .OrderBy(g => g.Key switch + { + "Zombie Combat" => 0, + "Zombie Economy" => 1, + "Zombie Progress" => 2, + "Zombie Averages" => 3, + _ => 99 + }) + .ToList(); + } + + @foreach (var group in metricGroups) + { +
+
+ "ph-crosshair", + "Zombie Economy" => "ph-coins", + "Zombie Progress" => "ph-trophy", + "Zombie Averages" => "ph-chart-line", + _ => "ph-squares-four" + }) text-primary text-sm"> +

@group.Key

+
+
+ @foreach (var metric in group) + { + @RenderStatCard(metric.Key, metric.Value) + } +
+
+ } + + @if (_matchHistory is { Count: > 0 }) + { + + + @if (_hasMoreMatches) + { +
+ +
+ } + } + +
+ +

Combat Breakdown

+
diff --git a/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor.cs index edcaa6866..5a488485a 100644 --- a/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor.cs +++ b/WebfrontCore/Components/Features/Clients/Statistics/AdvancedStats.razor.cs @@ -3,6 +3,7 @@ using Microsoft.JSInterop; using SharedLibraryCore; using SharedLibraryCore.Dtos; +using SharedLibraryCore.Interfaces; using Stats.Dtos; using WebfrontCore.Core.Services; @@ -15,9 +16,11 @@ public partial class AdvancedStats [Inject] public required NavigationManager NavManager { get; set; } [Inject] public required IJSRuntime JS { get; set; } [Inject] public required ILogger Logger { get; set; } + [Inject] public required IServiceProvider ServiceProvider { get; set; } [Parameter] public int ClientId { get; set; } [SupplyParameterFromQuery] public string? serverId { get; set; } + [SupplyParameterFromQuery(Name = "category")] public string? performanceBucket { get; set; } [PersistentState(AllowUpdates = true)] public AdvancedStatsState? State { get; set; } @@ -28,9 +31,14 @@ public partial class AdvancedStats private bool _chartsInitialized; private bool _showAllHitLocations; private bool _showAllWeapons; + private List? _matchHistory; + private bool _hasMoreMatches = true; + private bool _loadingMoreMatches; + private const int MatchHistoryPageSize = 5; private const int DefaultTableRowCount = 10; private int _lastLoadedId; private string? _lastLoadedServerId; + private string? _lastLoadedBucket; protected override async Task OnParametersSetAsync() { @@ -40,7 +48,8 @@ protected override async Task OnParametersSetAsync() if (State?.Stats != null && _lastLoadedId == ClientId && State.Stats.ClientId == ClientId && - EqualityComparer.Default.Equals(_lastLoadedServerId, serverId)) + EqualityComparer.Default.Equals(_lastLoadedServerId, serverId) && + EqualityComparer.Default.Equals(_lastLoadedBucket, performanceBucket)) { // Verify server endpoint match if serverId param is provided if (serverId == null || State.Stats.ServerEndpoint == serverId) @@ -54,14 +63,25 @@ protected override async Task OnParametersSetAsync() { State ??= new AdvancedStatsState(); - State.Stats = await DataService.GetClientStatisticsAsync(ClientId, serverId); + State.Stats = await DataService.GetClientStatisticsAsync(ClientId, serverId, performanceBucket); _lastLoadedId = ClientId; _lastLoadedServerId = serverId; + _lastLoadedBucket = performanceBucket; + + // Load match history from premium service if available + var matchHistoryService = ServiceProvider.GetService(); + if (matchHistoryService is not null) + { + _matchHistory = await matchHistoryService.GetPlayerMatchHistoryAsync(ClientId, serverId, + 0, MatchHistoryPageSize); + _hasMoreMatches = _matchHistory.Count >= MatchHistoryPageSize; + } GenerateMenu(); } - catch (Exception) + catch (Exception ex) { + Logger.LogError(ex, "Failed to load advanced stats for client {ClientId}", ClientId); NavManager.NavigateTo("/client/" + ClientId); } } @@ -94,28 +114,79 @@ await JS.InvokeVoidAsync("initAdvancedStats", Stats.PerformanceHistory, hitLocat } } + private async Task LoadMoreMatches() + { + if (_loadingMoreMatches || !_hasMoreMatches || _matchHistory is null) return; + + _loadingMoreMatches = true; + + var matchHistoryService = ServiceProvider.GetService(); + if (matchHistoryService is not null) + { + var moreMatches = await matchHistoryService.GetPlayerMatchHistoryAsync(ClientId, serverId, + _matchHistory.Count, MatchHistoryPageSize); + _matchHistory.AddRange(moreMatches); + _hasMoreMatches = moreMatches.Count >= MatchHistoryPageSize; + } + + _loadingMoreMatches = false; + } + private void GenerateMenu() { if (Stats == null) return; - MenuItems = new SideContextMenuItems + var items = new List { - MenuTitle = AppState.Loc("WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"), - Items = Stats.Servers.Select(server => new SideContextMenuItem - { - IsLink = true, - Reference = $"/client/{ClientId}/stats?serverId={server.Endpoint}", - Title = server.Name.StripColors(), - IsActive = Stats.ServerEndpoint == server.Endpoint, - Meta = server.Game.ToString(), - IsCollapse = true - }).Prepend(new SideContextMenuItem + new() { IsLink = true, Reference = $"/client/{ClientId}/stats", Title = AppState.Loc("WEBFRONT_STATS_INDEX_ALL_SERVERS"), - IsActive = Stats.ServerEndpoint == null - }).ToList() + IsActive = serverId == null && performanceBucket == null + } + }; + + // "Category" section header + bucket items (alphabetical) + var bucketGroups = Stats.Servers + .Where(s => !string.IsNullOrEmpty(s.PerformanceBucket)) + .GroupBy(s => s.PerformanceBucket, StringComparer.OrdinalIgnoreCase) + .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (bucketGroups.Count > 0) + { + items.Add(new SideContextMenuItem { IsSectionHeader = true, Title = "Category" }); + items.AddRange(bucketGroups.Select(group => new SideContextMenuItem + { + IsLink = true, + Reference = $"/client/{ClientId}/stats?category={group.Key}", + Title = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(group.Key.ToLower()), + IsActive = string.Equals(performanceBucket, group.Key, StringComparison.OrdinalIgnoreCase) && serverId == null, + Meta = group.First().Game.ToString(), + IsCollapse = false + })); + } + + // Individual servers filtered by selected bucket (collapsible, grouped by game) + var filteredServers = performanceBucket != null + ? Stats.Servers.Where(s => string.Equals(s.PerformanceBucket, performanceBucket, StringComparison.OrdinalIgnoreCase)) + : Stats.Servers; + + items.AddRange(filteredServers.Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = $"/client/{ClientId}/stats?serverId={server.Endpoint}", + Title = server.Name.StripColors(), + IsActive = Stats.ServerEndpoint == server.Endpoint && performanceBucket == null, + Meta = server.Game.ToString(), + IsCollapse = true + })); + + MenuItems = new SideContextMenuItems + { + MenuTitle = AppState.Loc("WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"), + Items = items }; } diff --git a/WebfrontCore/Components/Features/Clients/Statistics/PaceVisuals.cs b/WebfrontCore/Components/Features/Clients/Statistics/PaceVisuals.cs new file mode 100644 index 000000000..1d0c25a52 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/PaceVisuals.cs @@ -0,0 +1,137 @@ +using Data.Models.Zombie; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using WebfrontCore.Core.Services; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +/// +/// Shared rendering helpers for round-pace tinting. Used by both the post-match +/// round table and the live snapshot so a "12% slower" round looks identical +/// across both surfaces. +/// +public static class PaceVisuals +{ + /// + /// Tailwind text-colour class for the 5-band pace scale. Symmetric around + /// neutral; vivid shades for the >15% extremes, pale shades for the inner + /// 5–15% bands. Pale-not-dark on dark theme — opacity-dimmed greens/reds + /// blend into the background instead of reading as "lighter than vivid". + /// + public static string ColorClass(PaceBand? band) => band switch + { + PaceBand.MuchFaster => "text-emerald-400", + PaceBand.Faster => "text-emerald-200", + PaceBand.Slower => "text-rose-200", + PaceBand.MuchSlower => "text-rose-400", + _ => "text-subtle", + }; + + /// + /// Classify a live elapsed duration against an EMA target using the same + /// thresholds as ZombieRoundEmaService.Classify. Returns null when + /// the EMA target is not yet known. Lets the live banner shift the + /// elapsed text colour every second the round runs without round-tripping + /// to the server. + /// + public static (PaceBand Band, double Ratio)? ClassifyLive(double elapsedSeconds, double? emaSeconds) + { + if (emaSeconds is not { } typical || typical <= 0 || elapsedSeconds <= 0) return null; + var ratio = (elapsedSeconds - typical) / typical; + var band = ratio switch + { + < -0.15 => PaceBand.MuchFaster, + < -0.05 => PaceBand.Faster, + <= 0.05 => PaceBand.Neutral, + <= 0.15 => PaceBand.Slower, + _ => PaceBand.MuchSlower, + }; + return (band, ratio); + } + + /// + /// Format mm:ss / h:mm:ss for a duration in seconds. Mirrors the display + /// format used by the post-match round table. + /// + public static string FormatRoundTime(double seconds) + { + var ts = TimeSpan.FromSeconds(seconds); + return ts.TotalHours >= 1 + ? ts.ToString(@"h\:mm\:ss") + : ts.ToString(@"m\:ss"); + } + + /// + /// Player-count label suitable for the pace tooltip ("solo" / "2P" etc). + /// Reuses the same translation keys broadcast messages use. + /// + public static string PlayerLabel(int? playerCount, AppState appState) => playerCount switch + { + 1 => appState.Loc("PLUGINS_ZOMBIE_STATS_LABEL_SOLO"), + { } n and > 1 => appState.Loc("PLUGINS_ZOMBIE_STATS_LABEL_PLAYER_SHORT").FormatExt(n), + _ => string.Empty, + }; + + /// + /// Tooltip text for a completed round's pace. Returns null when the round + /// has no EMA data — caller renders the row without a tooltip. + /// + public static string? RoundTooltip(ZombieMatchHistoryRound round, AppState appState) + { + if (round.PaceBand is not { } band || round.PaceRatio is not { } ratio + || round.PaceTypicalSeconds is not { } typical) + { + return null; + } + + var playerLabel = PlayerLabel(round.PlayerCountAtRoundStart, appState); + var typicalFormatted = FormatRoundTime(typical); + var absPercent = Math.Abs(ratio) * 100; + + return band switch + { + PaceBand.Neutral => appState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_NEUTRAL") + .FormatExt(round.RoundNumber, playerLabel, typicalFormatted), + PaceBand.Faster or PaceBand.MuchFaster => appState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_FASTER") + .FormatExt(absPercent.ToString("F0"), round.RoundNumber, playerLabel, typicalFormatted), + _ => appState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_SLOWER") + .FormatExt(absPercent.ToString("F0"), round.RoundNumber, playerLabel, typicalFormatted), + }; + } + + /// + /// Visuals + localized labels for the special-round chip rendered next to the + /// round number on both the post-match Round Breakdown and the live banner. + /// Returns null on normal rounds. Tooltip explains why pace tinting / SPH are + /// suppressed for the round. + /// + public static SpecialRoundBadge? SpecialBadge(ZombieSpecialRoundType? specialType, AppState appState) + { + if (specialType is not { } st) return null; + + var (label, icon, colorClass) = st switch + { + ZombieSpecialRoundType.Dog => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_DOG"), "ph-dog", "bg-amber-500/15 text-amber-300 border border-amber-500/40"), + ZombieSpecialRoundType.Monkey => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_MONKEY"), "ph-paw-print", "bg-rose-500/15 text-rose-300 border border-rose-500/40"), + ZombieSpecialRoundType.Leaper => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_LEAPER"), "ph-arrow-up", "bg-emerald-500/15 text-emerald-300 border border-emerald-500/40"), + ZombieSpecialRoundType.Thief => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_THIEF"), "ph-mask-sad", "bg-cyan-500/15 text-cyan-300 border border-cyan-500/40"), + ZombieSpecialRoundType.Wasp => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_WASP"), "ph-bug", "bg-purple-500/15 text-purple-300 border border-purple-500/40"), + ZombieSpecialRoundType.Spider => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_SPIDER"), "ph-spiral", "bg-lime-500/15 text-lime-300 border border-lime-500/40"), + ZombieSpecialRoundType.Robot => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_ROBOT"), "ph-robot", "bg-zinc-500/15 text-zinc-300 border border-zinc-500/40"), + ZombieSpecialRoundType.Quad => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_QUAD"), "ph-squares-four", "bg-orange-500/15 text-orange-300 border border-orange-500/40"), + ZombieSpecialRoundType.Boss => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_BOSS"), "ph-skull", "bg-red-500/15 text-red-300 border border-red-500/40"), + ZombieSpecialRoundType.Ee => (appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_EE"), "ph-star-four", "bg-fuchsia-500/15 text-fuchsia-300 border border-fuchsia-500/40"), + _ => (st.ToString(), "ph-question", "bg-surface-alt text-subtle border border-line"), + }; + + var tooltip = label + " — " + appState.Loc("WEBFRONT_ZOMBIE_ROUND_SPECIAL_TOOLTIP"); + return new SpecialRoundBadge(label, icon, colorClass, tooltip); + } +} + +/// +/// Render-side payload for a special-round chip — label, icon class, color class, +/// pre-built tooltip. returns null on +/// normal rounds; callers gate rendering on the optional being set. +/// +public sealed record SpecialRoundBadge(string Label, string Icon, string ColorClass, string Tooltip); diff --git a/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor b/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor index 19c8fc517..d3834c71d 100644 --- a/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor +++ b/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor @@ -33,7 +33,7 @@
+ Value="@(PerformanceBucket != null ? System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(PerformanceBucket.ToLower()) : SelectedServer?.Name ?? AppState.Loc("WEBFRONT_STATS_INDEX_ALL_SERVERS"))"/> @TotalRankedClients.ToString("#,##0") @(AppState.Loc("WEBFRONT_TOP_PLAYERS_SUBTITLE").ToString().ToLower()) @@ -43,7 +43,35 @@
- @if (_hasLoaded && TotalRankedClients == 0) + @if (_showBucketSelector && _availableBuckets != null) + { +

Select a Category

+ + } + else if (_hasLoaded && TotalRankedClients == 0) {
@@ -56,10 +84,8 @@
} - - - @{ + @foreach (var stat in TopPlayers ?? new()) + { var isGold = stat.Ranking == 1; var isSilver = stat.Ranking == 2; var isBronze = stat.Ranking == 3; @@ -74,7 +100,6 @@ isSilver ? "text-zinc-300 drop-shadow-[0_2px_10px_rgba(203,213,225,0.5)]" : isBronze ? "text-amber-500 drop-shadow-[0_2px_10px_rgba(217,119,6,0.5)]" : "text-subtle group-hover:text-primary transition-colors"; - }
@@ -125,34 +150,15 @@
- -
- -
- @stat.Kills.ToNumericalString() - @AppState.Loc("WEBFRONT_STATS_KILLS") -
- -
- @stat.Deaths.ToNumericalString() - @AppState.Loc("WEBFRONT_STATS_DEATHS") -
- -
- = 1.0 ? "text-foreground" : "text-error") font-mono font-bold"> - @stat.KDR.ToString("F2") - - @AppState.Loc("WEBFRONT_STATS_KDR") -
- -
- @stat.TimePlayedValue.TotalHours.ToString("F0")h - @AppState.Loc("WEBFRONT_STATS_PLAYED") -
+ +
+ @foreach (var metric in stat.Metrics) + { +
+ @metric.Value + @metric.Key +
+ }
@@ -170,23 +176,30 @@ alt="Rank" loading="lazy"/>
- - -
-
-
-
-
-
-
-
-
- - - + + + }
diff --git a/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor.cs index ed3bd0e32..037eb971c 100644 --- a/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor.cs +++ b/WebfrontCore/Components/Features/Clients/Statistics/StatsOverview.razor.cs @@ -1,6 +1,6 @@ using IW4MAdmin.Plugins.Stats.Web.Dtos; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web.Virtualization; +using Microsoft.AspNetCore.Http; using SharedLibraryCore; using SharedLibraryCore.Dtos; using WebfrontCore.Core.Services; @@ -13,18 +13,26 @@ public partial class StatsOverview : IAsyncDisposable [Inject] public required AppState AppState { get; set; } [Inject] public required NavigationManager NavManager { get; set; } [Inject] public required ILogger Logger { get; set; } + [Inject] public required IHttpContextAccessor HttpContextAccessor { get; set; } [SupplyParameterFromQuery(Name = "serverId")] public string? ServerId { get; set; } - [PersistentState(AllowUpdates = true)] - public StatsOverviewState? State { get; set; } + [SupplyParameterFromQuery(Name = "category")] + public string? PerformanceBucket { get; set; } + + [PersistentState(AllowUpdates = true)] public StatsOverviewState? State { get; set; } private bool _hasLoaded; private string? _previousServerId; + private string? _previousBucket; + private List? _availableBuckets; + private bool _showBucketSelector; private bool _firstLoad = true; - private Virtualize? _virtualizeComponent; - private readonly Dictionary _statsCache = new(); + private bool _isLoadingMore; + + private const int InitialBatchSize = 25; + private const int LoadMoreBatchSize = 25; protected override async Task OnParametersSetAsync() { @@ -34,27 +42,14 @@ protected override async Task OnParametersSetAsync() State = new StatsOverviewState(); } // Check if we have restored state that matches the current request - else if (_firstLoad && State.ServerId == ServerId && State.TopPlayers.Count > 0) + else if (_firstLoad && State.ServerId == ServerId && State.PerformanceBucket == PerformanceBucket && State.TopPlayers.Count > 0) { _firstLoad = false; _previousServerId = ServerId; + _previousBucket = PerformanceBucket; _hasLoaded = true; - - // Restore cache from state - _statsCache.Clear(); - for (var i = 0; i < State.TopPlayers.Count; i++) - { - _statsCache[i] = State.TopPlayers[i]; - } - - if (State.MenuItems != null) - { - // We manually set the backing field of the wrapper (which proxies to state, so actually we just need to Ensure state is set, which it is) - // Actually MenuItems property SETTER writes to State.MenuItems. - // So we don't need to do anything if it's already there. - // But the property getter handles null coalescence. - } - else + + if (State.MenuItems is null) { await GenerateMenu(); } @@ -62,26 +57,67 @@ protected override async Task OnParametersSetAsync() return; } - // Refresh grid when ServerId changes - if (_firstLoad || _previousServerId != ServerId) + // Refresh grid when ServerId or bucket changes + if (_firstLoad || _previousServerId != ServerId || _previousBucket != PerformanceBucket) { _firstLoad = false; _previousServerId = ServerId; + _previousBucket = PerformanceBucket; _hasLoaded = false; + _showBucketSelector = false; - // Clear cache when server changes - _statsCache.Clear(); + // Clear loaded items on filter change. State.TopPlayers.Clear(); + State.HasMore = true; + State.NextOffset = 0; State.ServerId = ServerId; + State.PerformanceBucket = PerformanceBucket; - if (_virtualizeComponent != null) + var allServers = await DataService.GetServersAsync(); + var buckets = allServers + .Where(s => !string.IsNullOrEmpty(s.PerformanceBucket)) + .GroupBy(s => s.PerformanceBucket, StringComparer.OrdinalIgnoreCase) + .Select(g => new BucketInfo + { + Code = g.Key, + Games = g.Select(s => s.Game.ToString()).Distinct().ToList(), + ServerCount = g.Count() + }) + .OrderBy(b => b.Code, StringComparer.OrdinalIgnoreCase) + .ToList(); + + // Validate category if provided (case-insensitive) + if (PerformanceBucket != null) { - await _virtualizeComponent.RefreshDataAsync(); + var matchedBucket = buckets.FirstOrDefault(b => + string.Equals(b.Code, PerformanceBucket, StringComparison.OrdinalIgnoreCase)); + if (matchedBucket == null) + { + if (HttpContextAccessor.HttpContext is { } httpContext) + { + httpContext.Response.StatusCode = 404; + } + + NavManager.NavigateTo("/NotFound", replace: true); + return; + } + + // Normalize to the canonical case + PerformanceBucket = matchedBucket.Code; } - await GenerateMenu(); + // When no bucket or server is selected, auto-select the first bucket alphabetically + if (ServerId == null && PerformanceBucket == null) + { + if (buckets.Count > 0) + { + PerformanceBucket = buckets[0].Code; + State.PerformanceBucket = PerformanceBucket; + } - if (ServerId != null) + State.SelectedServer = null; + } + else if (ServerId != null) { var servers = await DataService.GetServersAsync(); State.SelectedServer = servers.FirstOrDefault(s => s.Endpoint == ServerId); @@ -91,113 +127,61 @@ protected override async Task OnParametersSetAsync() State.SelectedServer = null; } - var topResponse = await DataService.GetTopStatsAsync(new WebfrontCore.Controllers.API.Models.TopStatsRequest - { - Count = 3, - Offset = 0, - ServerId = ServerId - }); - - State.TopPlayers = topResponse.Players; - State.TotalRankedClients = topResponse.TotalRankedClients; + await GenerateMenu(); + await LoadBatch(InitialBatchSize); _hasLoaded = true; } } - private const int BatchSize = 50; - - private async ValueTask> LoadPlayerStats(ItemsProviderRequest request) + private async Task LoadBatch(int count) { - // Ensure state is initialized (should be by OnParametersSet) - if (State == null) return new ItemsProviderResult(new List(), 0); - - var startIndex = request.StartIndex; - var requestedCount = request.Count; - - // Try to fulfill entirely from cache first - if (State.TotalRankedClients > 0) - { - var cachedItems = new List(); - var allCached = true; - - var actualEnd = Math.Min(startIndex + requestedCount, (int)State.TotalRankedClients); - for (var i = startIndex; i < actualEnd; i++) - { - if (_statsCache.TryGetValue(i, out var item)) - { - cachedItems.Add(item); - } - else - { - allCached = false; - break; - } - } - - if (allCached && cachedItems.Count > 0) - { - return new ItemsProviderResult(cachedItems, (int)State.TotalRankedClients); - } - } + if (State is null) return; try { - // Fetch a batch starting from a position that covers the request - var fetchCount = Math.Max(BatchSize, requestedCount); - var response = await DataService.GetTopStatsAsync(new WebfrontCore.Controllers.API.Models.TopStatsRequest { - Count = fetchCount, - Offset = startIndex, - ServerId = ServerId + Count = count, + // Server-side ranking history may be filtered against the stats join, + // so a single page can consume more underlying rows than it returns. + // Advance offset by the rows the server actually consumed (NextOffset), + // not by accumulated `TopPlayers.Count` — otherwise rejected rows get + // re-walked on every page and pagination livelocks. + Offset = State.NextOffset, + ServerId = ServerId, + PerformanceBucketCode = PerformanceBucket }); - // Update total count State.TotalRankedClients = response.TotalRankedClients; - if (!_hasLoaded) - { - _hasLoaded = true; - StateHasChanged(); - } - - // Cache all fetched items - var playersList = response.Players.ToList(); - for (var i = 0; i < playersList.Count; i++) - { - var absoluteIndex = startIndex + i; - _statsCache[absoluteIndex] = playersList[i]; - - // Persist the first batch (approx) to State for restoration - if (absoluteIndex < BatchSize) - { - if (State.TopPlayers.Count <= absoluteIndex) - { - State.TopPlayers.Add(playersList[i]); - } - else - { - State.TopPlayers[absoluteIndex] = playersList[i]; - } - } - } - - // Return items for the requested range - var result = new List(); - var resultEnd = Math.Min(startIndex + requestedCount, (int)response.TotalRankedClients); - for (var i = startIndex; i < resultEnd; i++) - { - if (_statsCache.TryGetValue(i, out var item)) - { - result.Add(item); - } - } - - return new ItemsProviderResult(result, (int)response.TotalRankedClients); + State.TopPlayers.AddRange(response.Players); + State.NextOffset = response.NextOffset; + + // Stop when we've drained the source: either the server reports we've + // consumed every ranked row, or it returned an empty page (defense + // against a server-side filter rejecting everything in the next slice). + State.HasMore = response.Players.Count > 0 + && State.NextOffset < (int)response.TotalRankedClients; } catch (Exception ex) { - Logger.LogError(ex, "Error loading stats for virtualized list"); - return new ItemsProviderResult(new List(), (int)State.TotalRankedClients); + Logger.LogError(ex, "Error loading stats batch"); + State.HasMore = false; + } + } + + private async Task LoadMore() + { + if (_isLoadingMore || State is null || !State.HasMore) return; + _isLoadingMore = true; + StateHasChanged(); + try + { + await LoadBatch(LoadMoreBatchSize); + } + finally + { + _isLoadingMore = false; + StateHasChanged(); } } @@ -206,38 +190,63 @@ private async Task GenerateMenu() if (State == null) return; var servers = await DataService.GetServersAsync(); - State.MenuItems = new SideContextMenuItems + var items = new List(); + + // "Category" section header + bucket items (alphabetical) + var bucketGroups = servers + .Where(s => !string.IsNullOrEmpty(s.PerformanceBucket)) + .GroupBy(s => s.PerformanceBucket, StringComparer.OrdinalIgnoreCase) + .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (bucketGroups.Count > 0) { - MenuTitle = AppState.Loc("WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"), - Items = servers.Select(server => new SideContextMenuItem + items.Add(new SideContextMenuItem { IsSectionHeader = true, Title = "Category" }); + items.AddRange(bucketGroups.Select(group => new SideContextMenuItem { IsLink = true, - Reference = $"/stats/top?serverId={server.Endpoint}", - Title = server.Name.StripColors(), - IsActive = ServerId == server.Endpoint, - Meta = server.Game.ToString(), - IsCollapse = true - }).Prepend(new SideContextMenuItem - { - IsLink = true, - Reference = "/stats/top", - Title = AppState.Loc("WEBFRONT_STATS_INDEX_ALL_SERVERS"), - IsActive = ServerId == null - }).ToList() + Reference = $"/stats/top?category={group.Key}", + Title = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(group.Key.ToLower()), + IsActive = string.Equals(PerformanceBucket, group.Key, StringComparison.OrdinalIgnoreCase) && ServerId == null, + Meta = group.First().Game.ToString(), + IsCollapse = false + })); + } + + // Individual servers filtered by selected bucket (collapsible, grouped by game) + var filteredServers = PerformanceBucket != null + ? servers.Where(s => string.Equals(s.PerformanceBucket, PerformanceBucket, StringComparison.OrdinalIgnoreCase)) + : servers; + + items.AddRange(filteredServers.Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = $"/stats/top?serverId={server.Endpoint}", + Title = server.Name.StripColors(), + IsActive = ServerId == server.Endpoint, + Meta = server.Game.ToString(), + IsCollapse = true + })); + + State.MenuItems = new SideContextMenuItems + { + MenuTitle = AppState.Loc("WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"), + Items = items }; } - + // Properties that proxy to State public long TotalRankedClients => State?.TotalRankedClients ?? 0; public ServerInfo? SelectedServer => State?.SelectedServer; - public List? TopPlayers => State?.TopPlayers; - public SideContextMenuItems MenuItems - { - get => State?.MenuItems ?? new SideContextMenuItems(); + public List? TopPlayers => State?.TopPlayers; + + public SideContextMenuItems MenuItems + { + get => State?.MenuItems ?? new SideContextMenuItems(); set { - if (State != null) State.MenuItems = value; - } + if (State != null) State.MenuItems = value; + } } public class StatsOverviewState @@ -246,9 +255,19 @@ public class StatsOverviewState public ServerInfo? SelectedServer { get; set; } public List TopPlayers { get; set; } = []; public string? ServerId { get; set; } + public string? PerformanceBucket { get; set; } public SideContextMenuItems? MenuItems { get; set; } + public bool HasMore { get; set; } = true; + + /// + /// Ranking-history rows consumed so far for the current filter. Server-side + /// chunk-fill means this advances faster than .Count + /// when the stats join filters rejects rows. Pass as the next request's + /// offset to skip already-walked rows. + /// + public int NextOffset { get; set; } } - + // Existing helper methods... private static int GetRankIconIndex(double? zScore) { @@ -318,4 +337,11 @@ public async ValueTask DisposeAsync() { await Task.CompletedTask; } + + public class BucketInfo + { + public string Code { get; set; } + public List Games { get; set; } = []; + public int ServerCount { get; set; } + } } diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieAchievementBadges.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieAchievementBadges.razor new file mode 100644 index 000000000..a15e7b24c --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieAchievementBadges.razor @@ -0,0 +1,313 @@ +@inject AppState AppState +@inject IJSRuntime JS +@implements IAsyncDisposable +@using SharedLibraryCore.Interfaces +@* + Compact achievement-badge strip for match titlebars (leaderboard row + per-client + history + live snapshot). Surfaces match-level milestones that would otherwise + require expanding the row to discover. + + Badges (in render order, all conditionally rendered): + • EE chips — per-quest by default (one chip per quest with name + round). Songs + render purple, main quest yellow/amber. When the strip would wrap + (titlebar narrowed by viewport / extra chips / long player names), + JS (zombieBadgeStrip in blazor_lib.js) toggles .zm-badge-collapsed + and the strip switches to a single "EE X/Y" aggregate chip — modal + opens on click. Maps with many quests collapse first, plenty of + space stays expanded. + • All Built (X/Y) — green, ph-wrench. Only when X == Y && Y > 0. + • Flawless — pink, ph-heart. Match-wide when TotalDowns is provided + (zero across all qualified players); per-player when + PlayerDowns is provided (zero for that player). Same + label, scope-aware tooltip. Floored at HighestRound ≥ 5. + + Pass null/empty for any signal you don't have. Component never renders an empty + container — returns nothing when no badges qualify. +*@ + +@{ + var allBuilt = BuildablesTotal is { } total && total > 0 && BuildablesBuilt >= total; + var showFlawless = HighestRound >= 5 + && (TotalDowns is 0 || (TotalDowns is null && PlayerDowns is 0)); + var flawlessIsPersonal = TotalDowns is null && PlayerDowns is 0; + + var allQuests = EasterEggQuests ?? []; + var completedQuests = allQuests.Count(q => q.IsComplete); + var partialQuests = allQuests.Count(q => !q.IsComplete && q.Completed > 0); + var totalQuests = allQuests.Count; + // Per-quest chip filter mirrors the old visible-quests rule: completed quests + // always show; partials show only on live consumers (MatchEnded=false) since a + // historical partial can never advance further and is just stale noise. + var visibleQuests = allQuests + .Where(q => q.IsComplete || (!MatchEnded && q.Completed > 0)) + .ToList(); + var hasEeProgress = visibleQuests.Count > 0; + var allEeComplete = totalQuests > 0 && completedQuests == totalQuests; + var hasAny = hasEeProgress || allBuilt || showFlawless; +} + +@if (hasAny) +{ +
+ + @if (hasEeProgress) + { + @* Expanded view (default): one chip per visible quest. JS collapses to + the aggregate chip below if this group can't fit on one row. *@ +
+ @foreach (var quest in visibleQuests) + { + var questTitle = AppState.Loc(quest.LocKey); + var shortLabel = !string.IsNullOrEmpty(quest.ShortLocKey) + ? AppState.Loc(quest.ShortLocKey) + : questTitle; + var isSong = quest.Id.StartsWith("song", StringComparison.Ordinal); + // Songs render purple to separate music EEs from main-quest visually. + // Same shape across complete/partial: bright shade complete, dim partial. + var (completeClasses, partialClasses) = isSong + ? ("bg-purple-400/10 border-purple-400/30 text-purple-300", + "bg-purple-500/5 border-purple-500/30 text-purple-400") + : ("bg-yellow-400/10 border-yellow-400/30 text-yellow-400", + "bg-amber-500/5 border-amber-500/30 text-amber-400"); + if (quest.IsComplete) + { + var roundDisplay = quest.CompletedRound ?? quest.LastRound; + var eeLabel = roundDisplay is { } r && r > 0 + ? $"{shortLabel} R{r}" + : shortLabel; + var fullTooltip = $"{questTitle} — {AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_TOOLTIP")}"; + + + + } + else + { + var partialDone = quest.Completed; + var partialTotal = quest.Total; + var partialLabel = quest.LastRound is { } lr && lr > 0 + ? $"{shortLabel} {partialDone}/{partialTotal} R{lr}" + : $"{shortLabel} {partialDone}/{partialTotal}"; + var partialTooltip = $"{questTitle} — {AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_PARTIAL_TOOLTIP").FormatExt(partialDone, partialTotal)}"; + + + + } + } +
+ + @* Collapsed-fallback aggregate chip — same modal trigger. CSS hides this + by default; JS shows it (and hides the expanded group) when overflow + is detected. data-ee-aggregate hook is what the CSS rule keys off. + Wrapper carries the hook so the Tooltip component (which doesn't + splat attributes) doesn't need changes. *@ + var aggBg = allEeComplete ? "bg-yellow-400/10" : "bg-amber-500/10"; + var aggBorder = allEeComplete ? "border-yellow-400/30" : "border-amber-500/30"; + var aggText = allEeComplete ? "text-yellow-400" : "text-amber-400"; + var aggTooltip = allEeComplete + ? AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_AGGREGATE_TOOLTIP_COMPLETE") + : AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_AGGREGATE_TOOLTIP_PARTIAL"); +
+ + + +
+ } + + @if (allBuilt) + { + + + + @BuildablesBuilt/@BuildablesTotal!.Value + + + } + + @* "+N extra" chip — only when all iconic are built AND there are non-iconic + extras (side-quest items, PaP-on-Tranzit, etc.). Sits next to the All-Built + badge so the achievement reads "8/8 +2 extra". Hidden if no extras logged. *@ + @if (allBuilt && ExtraBuildablesBuilt > 0) + { + + + + @ExtraBuildablesBuilt + + + } + + @if (showFlawless) + { + var flTooltip = flawlessIsPersonal + ? AppState.Loc("WEBFRONT_ZOMBIE_MATCH_FLAWLESS_PERSONAL_TOOLTIP") + : AppState.Loc("WEBFRONT_ZOMBIE_MATCH_FLAWLESS_TOOLTIP"); + + + + @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_FLAWLESS_LABEL") + + + } + +
+} + +@if (_eeModalOpen && hasEeProgress) +{ + @* Per-quest detail modal. Opens on aggregate-chip click; lists every quest with + its progress, completed round, and step inventory. Same data as the dedicated + match page — surfaced inline so users on a leaderboard row don't need to navigate + away to see what was completed. *@ + +} + +@code { + /// + /// Per-quest EE progress for this match's map. Strip renders one chip per visible + /// quest by default; collapses to a single aggregate when overflow is detected. + /// Empty / null hides the EE block entirely. + /// + [Parameter] public List? EasterEggQuests { get; set; } + + /// Distinct iconic buildables completed this match. + [Parameter] public int BuildablesBuilt { get; set; } + + /// Total iconic buildables on this map. Null = no denominator known. + [Parameter] public int? BuildablesTotal { get; set; } + + /// + /// Count of non-iconic buildables also completed (side-quest / extras). Drives the + /// "+N extra" chip alongside the All-Built badge — only renders when every iconic + /// is built AND extras > 0. + /// + [Parameter] public int ExtraBuildablesBuilt { get; set; } + + /// Match's highest round. Drives the No-Down floor. + [Parameter, EditorRequired] public int HighestRound { get; set; } + + /// + /// Match-wide downs across all qualified players. Set this on the leaderboard row + /// where we want the match-level "No-Down Match" badge. Leave null on per-player + /// views — pass instead. + /// + [Parameter] public int? TotalDowns { get; set; } + + /// + /// This player's downs across the match. Set this on per-client history rows. + /// When zero (and TotalDowns null), shows a "Personal No-Down" variant. + /// + [Parameter] public int? PlayerDowns { get; set; } + + /// + /// Set true on historical-match consumers (leaderboard, match history) so partial + /// (in-progress) quest chips are excluded — those matches can never advance, so a + /// partial is just stale noise. Live snapshot leaves this false. + /// + [Parameter] public bool MatchEnded { get; set; } + + private readonly string _stripId = $"zm-badges-{Guid.NewGuid():N}"; + private bool _eeModalOpen; + private bool _observerWired; + + private void OpenEeModal() => _eeModalOpen = true; + private void CloseEeModal() => _eeModalOpen = false; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Fire setup once when the strip is first in the DOM. The JS-side guard in + // zombieBadgeStrip.setup is idempotent, but skipping the no-op call avoids + // an extra JSInterop roundtrip on every render. + if (firstRender && !_observerWired) + { + _observerWired = true; + try + { + await JS.InvokeVoidAsync("zombieBadgeStrip.setup", _stripId); + } + catch (JSDisconnectedException) { /* circuit gone */ } + catch (Microsoft.JSInterop.JSException) { /* page navigated */ } + } + } + + public async ValueTask DisposeAsync() + { + if (!_observerWired) return; + try + { + await JS.InvokeVoidAsync("zombieBadgeStrip.teardown", _stripId); + } + catch (JSDisconnectedException) { /* circuit gone */ } + catch (Microsoft.JSInterop.JSException) { /* page navigated */ } + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieAssistanceBadge.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieAssistanceBadge.razor new file mode 100644 index 000000000..d3a1d15e2 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieAssistanceBadge.razor @@ -0,0 +1,41 @@ +@inject AppState AppState +@* + Renders the "Solo from R" badge when the server-side gate (FinalizePlayerCount in + ZombieClientStateManager) flagged the player as a meaningful solo carry — assisted + rounds < half the match AND the solo tail is at least 3 rounds. The gate lives on + the server so all consumers (leaderboard, history, match detail) agree without + duplicating thresholds. + + Used on every match-record card: leaderboard player slot, per-client match-history + header, and per-player match-detail tab. +*@ + +@if (SoloFromRound is { } sfr && HighestRound > 0 && sfr <= HighestRound) +{ + var soloRounds = HighestRound - sfr + 1; + var lastTeamRound = sfr - 1; + // Tooltip template orders args as percent / lastTeamRound / soloRounds. + // FormatExt rewrites {{name}} placeholders left-to-right into {0}/{1}/{2}, + // so the FormatExt argument list must follow the template's placeholder + // order — not the variable's intuitive order. AssistedRounds may be null + // on legacy rows finalised before activity-gating shipped; skip the + // percentage when we have no data and degrade to "—%". + var assistedPercent = AssistedRounds is { } ar + ? ((int)Math.Round((double)ar / HighestRound * 100)).ToString() + : "—"; + var tooltip = AppState.Loc("WEBFRONT_ZOMBIE_ASSISTANCE_BADGE_TOOLTIP") + .FormatExt(assistedPercent, lastTeamRound, soloRounds); + + + + + @AppState.Loc("WEBFRONT_ZOMBIE_ASSISTANCE_BADGE_LABEL").FormatExt(sfr) + + +} + +@code { + [Parameter] public int? SoloFromRound { get; set; } + [Parameter, EditorRequired] public int HighestRound { get; set; } + [Parameter] public int? AssistedRounds { get; set; } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor new file mode 100644 index 000000000..2d8e7be23 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor @@ -0,0 +1,635 @@ +@page "/stats/zombies" +@rendermode InteractiveServer + +@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_TITLE") | @AppState.WebfrontBranding + +@if (!_serviceAvailable || !_hasLoaded) +{ + return; +} + +
+ + @* ================================================================ *@ + @* MAIN CONTENT AREA - Scrollable *@ + @* ================================================================ *@ +
+
+ + @* ================================================================ *@ + @* HEADER *@ + @* ================================================================ *@ +
+
+
+ +
+
+

+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_TITLE") +

+
+ + @if (_selectedGame is not null) + { + @_selectedGame.DisplayName + + } + @if (_selectedMap is not null) + { + @_selectedMap.MapName + + } + @GetPlayerCountLabel(_selectedPlayerCount) +
+
+
+ + @if (_selectedMap is not null) + { +
+ @foreach (var pc in _selectedMap.PlayerCounts) + { + var isPcSelected = _selectedPlayerCount == pc; + + @GetPlayerCountLabel(pc) + + } +
+ } +
+ + @if (_mapRecords.Count > 0) + { + +
+
+ @for (var i = 0; i < 2; i++) + { + @foreach (var record in _mapRecords) + { + +
+ @record.Label + @record.PlayerName +
+ @record.Value +
+ } + } +
+
+ } + + @if (_metadata is null || _metadata.Games.Count == 0) + { +
+
+ +
+

@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_NO_RECORDS_TITLE")

+

+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_NO_RECORDS_DESC") +

+
+ } + else if (_selectedMap is not null) + { + @* ================================================================ *@ + @* LEADERBOARD LIST *@ + @* ================================================================ *@ +
+ + @foreach (var entry in _entries) + { + var isExpanded = _expandedMatches.ContainsKey(entry.MatchId); + +
+ + @* ── HORIZONTAL LEDGER (Collapsed) ── *@ + @{ + var (badgeBg, badgeText, badgeBorder, badgeIcon, badgeLabel) = entry.Rank switch + { + 1 => ("bg-yellow-500/10", "text-yellow-500", "border-yellow-500/20", "ph-trophy", AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_RANK_FIRST")), + 2 => ("bg-slate-300/10", "text-slate-300", "border-slate-300/20", "ph-medal", AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_RANK_SECOND")), + 3 => ("bg-amber-600/10", "text-amber-500", "border-amber-600/20", "ph-medal", AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_RANK_THIRD")), + _ => ("bg-surface-alt", "text-subtle", "border-line/50", (string?)null, AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_RANK_OTHER").FormatExt(entry.Rank)) + }; + } + + @* Top bar: rank badge + round / map / duration + time + chevron *@ +
+
+
+ @if (badgeIcon is not null) + { + + } + @badgeLabel +
+
@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_ROUND").FormatExt(entry.HighestRound)
+ + + @* Surfaces matches whose listing-shown player count is smaller than the + actual concurrent participants — bucketed by the qualifier (a 2-player + match where one player joined too late lands in the 1-player bucket). + The badge tells viewers the entry isn't a true solo run; bucket+listing + invariant stays intact. *@ + @if (entry.TotalPlayerCount > entry.Players.Count) + { + var unqualified = entry.TotalPlayerCount - entry.Players.Count; + + + + @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_QUALIFIED_BADGE").FormatExt(entry.Players.Count, entry.TotalPlayerCount) + + + } +
+
+ @if (entry.Duration is not null) + { + @entry.Duration + + } + + @entry.MatchDate.Humanize() + + +
+
+ + @* Body: player layouts vary by count (1 / 2 / 3-8). + ALWAYS qualified-only — the leaderboard record card is a record + of the qualifying run, never a roster. Drop-ins surface only + inside the expanded scoreboard behind the SHOW_ALL toggle below. + Layout-count branch uses qualifiedPlayers.Count so a 3-player + bucket entry with 2 qualifiers + 1 drop-in renders as a 2-player + card (matching the bucket invariant), not as a 3+ row layout + that includes the drop-in. *@ + @{ + var qualifiedPlayers = entry.Players.Where(p => p.IsQualified).ToList(); + } +
+ @if (qualifiedPlayers.Count == 1) + { + var p = qualifiedPlayers[0]; + var soloInf = p.DamageReceived == 0; + var soloRatio = soloInf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER") +
+ + + + +
+
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_KILLS") + @p.Kills.ToString("N0") +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DMG_RATIO") + + @if (soloInf) + { + + } + else + { + @soloRatio.ToString("N1")x + } + +
+
+ } + else if (qualifiedPlayers.Count == 2) + { +
+ @foreach (var p in qualifiedPlayers) + { + var inf = p.DamageReceived == 0; + var ratio = inf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER") +
+ + + + +
+
+
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_KILLS") + @p.Kills.ToString("N0") +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DMG") + + @if (inf) + { + + } + else + { + @ratio.ToString("N1")x + } + +
+
+
+ } +
+ } + else + { + @* 3-8 players: compact list with dotted leader, two-column on md+ *@ +
+ @{ var idx = 0; } + @foreach (var p in qualifiedPlayers) + { + idx++; + var inf = p.DamageReceived == 0; + var ratio = inf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+ @(idx). + + + + + + +
+ + @p.Kills.ToString("N0")K + | + + @if (inf) + { + + } + else + { + @ratio.ToString("N1")x + } + + +
+ } +
+ } +
+ + @* ── EXPANDED SCOREBOARD ── *@ + @if (isExpanded) + { +
+ + @* Scoreboard table *@ +
+ + + + + + + + + + + + + + + + + + @{ + var showingAllPlayers = _showAllPlayers.Contains(entry.MatchId); + var visiblePlayers = showingAllPlayers + ? entry.Players + : entry.Players.Where(x => x.IsQualified).ToList(); + var hiddenUnqualifiedCount = entry.Players.Count(x => !x.IsQualified); + } + @foreach (var p in visiblePlayers) + { + var kpd = p.Downs == 0 ? p.Kills : (double)p.Kills / p.Downs; + var hsPercent = p.Kills == 0 ? 0 : (double)p.HeadshotKills / p.Kills * 100; + var netPoints = p.PointsEarned - p.PointsSpent; + var dmgRatioInfinite = p.DamageReceived == 0; + var dmgRatio = dmgRatioInfinite ? 0 : (double)p.DamageDealt / p.DamageReceived; + + + + + + + + + + + + + + + } + +
@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_SCORE")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_NET")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_SPENT")KDR@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_KPD")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_HS_PCT")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DMG_RATIO")@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PACE")
+
+ @if (_showTimeline.Contains(entry.MatchId)) + { + var isSelected = GetTimelinePlayer(entry.MatchId) == p.ClientId; + + } + + + + @if (!p.IsQualified) + { + + + @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DROPIN_LABEL") + + + } +
+
+ @p.PointsEarned.ToString("N0") + + @(netPoints > 0 ? "+" : "")@FormatK((int)netPoints) + + @FormatK((int)p.PointsSpent) + @p.Kills.ToString("N0")@p.Downs@p.Revives + @(kpd > 999 ? FormatK((int)kpd) : kpd.ToString("F1")) + + @hsPercent.ToString("F0")% + + @(dmgRatioInfinite ? "∞" : dmgRatio.ToString("N1")) + + @if (p.RoundDurationPaceRatio is { } paceRatio) + { + var paceTint = PaceVisuals.ColorClass(p.PaceBand); + var paceSign = paceRatio > 0.05 ? "+" : (paceRatio < -0.05 ? "−" : "±"); + var paceTooltip = LeaderboardPaceTooltip(paceRatio, p.PaceBand); + + @paceSign@(Math.Abs(paceRatio * 100).ToString("F0"))% + + } + else + { + + } +
+
+ + @* Footer with server name and timeline toggle *@ +
+
+ @if (entry.ServerName is not null) + { + + + } + + + #@entry.MatchId + + +
+ @if (_expandedMatches.TryGetValue(entry.MatchId, out var matchDetail) && matchDetail is not null) + { +
+ @if (entry.Players.Any(x => !x.IsQualified)) + { + var dropInCount = entry.Players.Count(x => !x.IsQualified); + var allShowing = _showAllPlayers.Contains(entry.MatchId); + + } + @* Split-button: Open (primary, ~70%) + Copy (secondary, icon-only, ~30%). + Two visually-merged controls in one rounded shell — saves header + horizontal real-estate and reads as one affordance with a primary + action and a quick-copy variant. The Open half is a real so + right-click → new tab works. *@ + @* Split-button layout notes: + - Outer container is flex+items-stretch so each half fills full height. + - Tooltip wrappers MUST also be flex+items-stretch — they sit between + the outer flex and the inner / + +
+ +
+ } + else + { +
+ + @AppState.Loc("WEBFRONT_LOADING") +
+ } +
+ + @* Timeline (existing component). ShowAllPlayers forwards this card's + SHOW_ALL toggle state through ZombieMatchDetail to the scrubber so + the timeline's visible lanes match the scoreboard's visible rows. *@ + @if (_showTimeline.Contains(entry.MatchId) && + _expandedMatches.TryGetValue(entry.MatchId, out var detail) && detail is not null) + { + + } +
+ } +
+ } + + @if (_entriesLoaded && _hasMore) + { + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } + + @if (_entriesLoaded && _totalEntries == 0) + { +
+
+ +
+

@AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_NO_MATCHES_TITLE")

+

+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_NO_MATCHES_DESC") +

+
+ } +
+ } +
+
+ + @* ================================================================ *@ + @* SIDEBAR - Fixed position *@ + @* ================================================================ *@ +
+ @if (MenuItems != null) + { +
+ +
+ } +
+ + +@code { + + private bool _marqueePaused; + private readonly HashSet _showTimeline = new(); + private readonly HashSet _showAllPlayers = new(); + private readonly Dictionary _timelinePlayerSelection = new(); + private int? _copiedMatchId; + + private void ToggleShowAllPlayers(int matchId) + { + if (!_showAllPlayers.Remove(matchId)) + { + _showAllPlayers.Add(matchId); + } + } + + private static string FormatK(int num) => + num >= 1000 ? ((double)num).ToMetric(decimals: 1) : num.ToString(); + + private void ToggleTimeline(int matchId) + { + if (!_showTimeline.Remove(matchId)) + { + _showTimeline.Add(matchId); + } + } + + private async Task CopyShareLink(int matchId) + { + var url = $"{NavManager.BaseUri.TrimEnd('/')}/zombie/match/{matchId}"; + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", url); + _copiedMatchId = matchId; + StateHasChanged(); + await Task.Delay(2000); + if (_copiedMatchId == matchId) _copiedMatchId = null; + StateHasChanged(); + } + catch + { + // Clipboard API requires HTTPS or localhost — fail silently + } + } + + private int GetTimelinePlayer(int matchId) + { + if (_timelinePlayerSelection.TryGetValue(matchId, out var clientId)) + { + return clientId; + } + + // Prefer first qualified player so the timeline renders by default — + // a high-scoring drop-in who didn't qualify would otherwise leave the + // scrubber blank (no rounds/timeline rows for unqualified players). + if (_expandedMatches.TryGetValue(matchId, out var detail) && detail?.Players.Count > 0) + { + var first = detail.Players.FirstOrDefault(p => p.IsQualified)?.ClientId + ?? detail.Players[0].ClientId; + _timelinePlayerSelection[matchId] = first; + return first; + } + + return 0; + } + + private void SetTimelinePlayer(int matchId, int clientId) + { + _timelinePlayerSelection[matchId] = clientId; + } + +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor.cs new file mode 100644 index 000000000..07eb9099b --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLeaderboard.razor.cs @@ -0,0 +1,279 @@ +using Data.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using WebfrontCore.Core.Services; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieLeaderboard +{ + [Inject] public required AppState AppState { get; set; } + [Inject] public required NavigationManager NavManager { get; set; } + [Inject] public required IJSRuntime JS { get; set; } + [Inject] public required ILogger Logger { get; set; } + [Inject] public required IHttpContextAccessor HttpContextAccessor { get; set; } + [Inject] public required IServiceProvider ServiceProvider { get; set; } + + [SupplyParameterFromQuery(Name = "game")] + public string? GameParam { get; set; } + + [SupplyParameterFromQuery(Name = "map")] + public string? MapParam { get; set; } + + [SupplyParameterFromQuery(Name = "players")] + public string? PlayersParam { get; set; } + + private IZombieLeaderboardService? _leaderboardService; + private IZombieMatchHistoryService? _matchHistoryService; + private ZombieLeaderboardMetadata? _metadata; + private ZombieLeaderboardGame? _selectedGame; + private ZombieLeaderboardMap? _selectedMap; + private int _selectedPlayerCount; + private bool _hasLoaded; + private bool _serviceAvailable; + private int _totalEntries; + private bool _entriesLoaded; + private bool _isLoadingMore; + private bool _hasMore = true; + private readonly List _entries = []; + private SharedLibraryCore.Dtos.SideContextMenuItems? MenuItems { get; set; } + private readonly Dictionary _expandedMatches = new(); + private readonly HashSet _loadingMatches = new(); + private List _mapRecords = []; + + private const int InitialBatchSize = 25; + private const int LoadMoreBatchSize = 25; + + private string? _previousGame; + private string? _previousMap; + private string? _previousPlayers; + private bool _firstLoad = true; + + protected override async Task OnParametersSetAsync() + { + _leaderboardService = ServiceProvider.GetService(); // TODO: Replace with IResourceQueryHelper ( REFERENCE!) + _matchHistoryService = ServiceProvider.GetService(); // TODO: Replace with IResourceQueryHelper ( REFERENCE!) + + if (_leaderboardService is null) + { + _serviceAvailable = false; + if (HttpContextAccessor.HttpContext is { } httpContext) + { + httpContext.Response.StatusCode = 404; + } + + NavManager.NavigateTo("/NotFound", replace: true); + return; + } + + _serviceAvailable = true; + + if (!_firstLoad && _previousGame == GameParam && _previousMap == MapParam && + _previousPlayers == PlayersParam) + { + return; + } + + var mapChanged = _firstLoad || _previousGame != GameParam || _previousMap != MapParam; + + _firstLoad = false; + _previousGame = GameParam; + _previousMap = MapParam; + _previousPlayers = PlayersParam; + _entriesLoaded = false; + + _metadata = await _leaderboardService.GetLeaderboardMetadataAsync(); + + if (_metadata.Games.Count == 0) + { + _hasLoaded = true; + return; + } + + // Resolve selected game + if (GameParam is not null && Enum.TryParse(GameParam, true, out var parsedGame)) + { + _selectedGame = _metadata.Games.FirstOrDefault(g => g.Game == parsedGame); + } + + _selectedGame ??= _metadata.Games[0]; + + // Resolve selected map + if (MapParam is not null && int.TryParse(MapParam, out var parsedMapId)) + { + _selectedMap = _selectedGame.Maps.FirstOrDefault(m => m.MapId == parsedMapId); + } + + _selectedMap ??= _selectedGame.Maps.FirstOrDefault(); + + // Populate sidebar menu + MenuItems = new SharedLibraryCore.Dtos.SideContextMenuItems + { + MenuTitle = AppState.Loc("WEBFRONT_STATS_INDEX_CATEGORIES"), + Items = _metadata.Games.SelectMany(game => + { + var list = new List + { + new() + { + IsSectionHeader = true, + Title = game.DisplayName, + Icon = "ph-game-controller" + } + }; + + list.AddRange(game.Maps.Select(map => new SharedLibraryCore.Dtos.SideContextMenuItem + { + IsLink = true, + Title = map.MapName, + Reference = BuildFilterUrl(game.Game.ToString(), map.MapId.ToString(), + map.PlayerCounts.FirstOrDefault().ToString()), + IsActive = _selectedGame.Game == game.Game && _selectedMap?.MapId == map.MapId, + Icon = "ph-map-trifold" + })); + + return list; + }).ToList() + }; + + // Resolve selected player count + if (_selectedMap is not null) + { + if (PlayersParam is not null && int.TryParse(PlayersParam, out var parsedPlayers) && + _selectedMap.PlayerCounts.Contains(parsedPlayers)) + { + _selectedPlayerCount = parsedPlayers; + } + else + { + _selectedPlayerCount = + _selectedMap.PlayerCounts.FirstOrDefault(); + } + } + + _hasLoaded = true; + + // Load map records for the marquee — only reload when game/map changes, not player count + if (_selectedMap is not null && mapChanged) + { + _mapRecords = await _leaderboardService.GetMapRecordsAsync(_selectedGame.Game, _selectedMap.MapId); + Random.Shared.Shuffle(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_mapRecords)); + } + + // Reset paging on filter change and fetch the first batch. + _entries.Clear(); + _expandedMatches.Clear(); + _loadingMatches.Clear(); + _hasMore = true; + await LoadBatch(InitialBatchSize); + } + + private async Task LoadBatch(int count) + { + if (_leaderboardService is null || _selectedGame is null || _selectedMap is null) return; + + try + { + var response = await _leaderboardService.GetLeaderboardEntriesAsync( + _selectedGame.Game, + _selectedMap.MapId, + _selectedPlayerCount, + _entries.Count, + count); + + _totalEntries = response.TotalCount; + _entries.AddRange(response.Entries); + _hasMore = response.Entries.Count >= count && _entries.Count < response.TotalCount; + _entriesLoaded = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading zombie leaderboard entries"); + _hasMore = false; + } + } + + private async Task LoadMore() + { + if (_isLoadingMore || !_hasMore) return; + _isLoadingMore = true; + StateHasChanged(); + try + { + await LoadBatch(LoadMoreBatchSize); + } + finally + { + _isLoadingMore = false; + StateHasChanged(); + } + } + + private string BuildFilterUrl(string? game = null, string? map = null, string? players = null) + { + var g = game ?? GameParam ?? _selectedGame?.Game.ToString(); + var m = map ?? MapParam ?? _selectedMap?.MapId.ToString(); + var p = players ?? PlayersParam ?? _selectedPlayerCount.ToString(); + return $"/stats/zombies?game={g}&map={m}&players={p}"; + } + + private string GetPlayerCountLabel(int count) => count switch + { + 1 => AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_SOLO"), + _ => AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER_COUNT").FormatExt(count) + }; + + private static string GetGameImagePath(Reference.Game game) => + $"/images/zombies/{game.ToString().ToLowerInvariant()}.jpg"; + + private async Task ToggleMatchDetail(int matchId) + { + if (_expandedMatches.ContainsKey(matchId)) + { + _expandedMatches.Remove(matchId); + return; + } + + if (_matchHistoryService is null || _loadingMatches.Contains(matchId)) return; + + _loadingMatches.Add(matchId); + _expandedMatches[matchId] = null; + StateHasChanged(); + + try + { + var detail = await _matchHistoryService.GetMatchDetailAsync(matchId); + _expandedMatches[matchId] = detail; + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to load match detail for {MatchId}", matchId); + _expandedMatches.Remove(matchId); + } + finally + { + _loadingMatches.Remove(matchId); + } + } + + /// + /// Tooltip text for the leaderboard's per-player Pace column. Same vocabulary + /// as per-round tooltips so the meaning is consistent across surfaces. + /// + private string LeaderboardPaceTooltip(double ratio, PaceBand? band) + { + var absPercent = Math.Abs(ratio) * 100; + return band switch + { + PaceBand.Faster or PaceBand.MuchFaster => + AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PACE_TOOLTIP_FASTER") + .FormatExt(absPercent.ToString("F0")), + PaceBand.Slower or PaceBand.MuchSlower => + AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PACE_TOOLTIP_SLOWER") + .FormatExt(absPercent.ToString("F0")), + _ => AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PACE_TOOLTIP_NEUTRAL"), + }; + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor new file mode 100644 index 000000000..eadd2e620 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor @@ -0,0 +1,590 @@ +@using SharedLibraryCore.Interfaces +@using Data.Models.Zombie + +@* + Live snapshot of an in-progress zombie match. Distinct from the post-match + ZombieMatchDetail — emphasises "happening right now" rather than "review past + performance". Refreshes via the parent ZombieLiveModalWrapper every 5s. + + Layout adapts to player count, mirroring ZombieLeaderboard's pattern: + - 1P → wide hero card with full stat strip + current-round footer + - 2P → side-by-side condensed cards + - 3-8 → compact rows with Round / Kills / Dmg columns +*@ + +
+ +
+ + @* ── BANNER: round + round timer + match elapsed + map ── *@ +
+
+
+ +
+ @{ var liveSpecial = PaceVisuals.SpecialBadge(Snapshot.CurrentRoundSpecialType, AppState); } +
+ @* Special-round badge sits inline beside the section title — keeps + the number/timer column free of the chip (which previously + sandwiched between the elapsed and zombies-left lines and read + as out-of-place). *@ +
+ @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_CURRENT_ROUND") + @if (liveSpecial is { } titleSb) + { + + + @titleSb.Label + + + } +
+ @* items-center vertically centres the elapsed/avg/sph/left column + against the big round number — items-baseline aligned first column + line to the round-number baseline, leaving a tall stack hanging + below the number that read as awkward padding. *@ +
+ @Snapshot.CurrentRound +
+ @if (_roundElapsedSeconds is { } rs) + { + // Pace tint suppressed on special rounds — EMA isn't comparable across + // round types, and the title-row badge already signals the round type. + var liveBand = liveSpecial is null + ? PaceVisuals.ClassifyLive(rs, Snapshot.CurrentRoundEmaSeconds) + : null; + var liveTint = PaceVisuals.ColorClass(liveBand?.Band); + var liveTooltip = LiveBannerTooltip(rs, liveBand); + + +@FormatElapsed(rs) + + if (liveSpecial is null) + { + if (Snapshot.CurrentRoundEmaSeconds is { } emaSeconds && emaSeconds > 0) + { + (@AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PACE_AVG_PREFIX") @FormatElapsed(emaSeconds)) + } + // Live SPH = elapsed / (cleared / 24) where cleared comes + // from engine truth (budget − remaining − alive), captured + // by the GSC ZR watcher. Gate on starting budget > 24 (one + // horde minimum) so tiny early rounds don't show jitter; + // brief div-by-zero protection until first kill lands. + if (Snapshot.CurrentRoundBudget is { } budget && budget > 24 + && Snapshot.CurrentRoundZombiesRemaining is { } remaining + && Snapshot.CurrentRoundZombiesAlive is { } alive) + { + var cleared = budget - remaining - alive; + if (cleared > 0) + { + var liveSph = rs / (cleared / 24.0); + @liveSph.ToString("F1") sph + } + } + } + } + @* Engine-truth zombies-left counter — sourced from the GSC ZR + watcher, independent of the budget formula so it works on + special rounds too. Clamped at 0 because dynamically-spawned + enemies (Origins panzer, mid-round dogs on T6) can briefly + push remaining+alive above the static initial total; we + surface "still to clear" rather than progress vs budget. *@ + @if (Snapshot.CurrentRoundZombiesRemaining is { } zr + && Snapshot.CurrentRoundZombiesAlive is { } za) + { + var zombiesLeft = Math.Max(0, zr + za); + + @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_ZOMBIES_REMAINING").FormatExt(zombiesLeft) + + } +
+
+
+ +
+ +
+ @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_MATCH_ELAPSED") +
+ @FormatElapsed(_elapsedSeconds) + + + + +
+
+ + @if (!string.IsNullOrEmpty(Snapshot.Map)) + { +
+
+ +
+ + @Snapshot.Map +
+
+ @* Power state indicator — current state derived from the most + recent transition. Mirrors the live-game red dot pattern + above for visual consistency. Hidden until at least one + transition has fired (no "OFF" baseline noise on maps where + the switch hasn't been hit yet). *@ + @if (Snapshot.PowerStateChanges.Count > 0) + { + var currentlyOn = Snapshot.PowerStateChanges[^1].IsOn; + var dotColor = currentlyOn ? "bg-success" : "bg-error"; + var stateText = currentlyOn + ? AppState.Loc("WEBFRONT_ZOMBIE_MATCH_POWER_ON") + : AppState.Loc("WEBFRONT_ZOMBIE_MATCH_POWER_OFF"); +
+ + + + @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_POWER_LABEL") · @stateText +
+ } +
+ } +
+
+ + @* ── EE PROGRESS MINI-STRIPS ── one row per in-progress quest, all rows live + in a single bordered container with horizontal dividers between rows so + multi-quest maps (Origins has 4) read as one cohesive panel rather than a + stack of separate pills. Per-row layout: + [icon + name][step dots in a row][X/Y + bar on right] + Step dots are icon-only circles (~22px) so a 10-step quest still fits on + one line; hover/tap a dot for the step name + completion round. *@ + @{ + var liveQuests = Snapshot.EasterEggQuests + .Where(q => !q.IsComplete && q.Total > 0) + .ToList(); + } + @if (liveQuests.Count > 0) + { +
+ @for (var i = 0; i < liveQuests.Count; i++) + { + var quest = liveQuests[i]; + var firedKeys = quest.Steps.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); + var stepsDone = quest.Completed; + var stepsTotal = quest.Total; + var pct = stepsTotal > 0 ? Math.Min(100, stepsDone * 100 / stepsTotal) : 0; + var dividerClass = i > 0 ? "border-t border-line/60" : ""; + var isSong = quest.Id.StartsWith("song", StringComparison.Ordinal); + // Songs render purple in the live strip too; main-quest stays amber. + // Same shape across complete/partial since live is always partial here + // (completed quests filter out at the source). + var titleColor = isSong ? "text-purple-300" : "text-amber-400"; + var dotFiredClasses = isSong + ? "bg-purple-400/20 border-purple-400/50 text-purple-200" + : "bg-amber-500/20 border-amber-500/50 text-amber-300"; + var barColor = isSong ? "bg-purple-300" : "bg-amber-400"; + var countColor = isSong ? "text-purple-300" : "text-amber-400"; +
+
+ + @AppState.Loc(quest.LocKey) +
+
+ @foreach (var step in quest.Inventory) + { + var fired = firedKeys.TryGetValue(step.Key, out var rec); + var label = AppState.Loc(step.LocKey); + var tooltip = fired && rec is not null + ? (rec.RoundNumber is { } rn && rn > 0 + ? $"{label} • R{rn}" + : label) + : $"{label} — pending"; + + + + + + } +
+
+
+
+
+ @stepsDone / @stepsTotal +
+
+ } +
+ } + + @* ── PLAYERS ── *@ + @if (Snapshot.Players.Count > 0) + { +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PLAYERS") (@Snapshot.Players.Count) +
+ + @if (Snapshot.Players.Count == 1) + { + @RenderSoloHero(Snapshot.Players[0]) + } + else if (Snapshot.Players.Count == 2) + { +
+ @foreach (var p in Snapshot.Players) + { + @RenderDuoCard(p) + } +
+ } + else + { +
+ @{ var rank = 0; } + @foreach (var p in Snapshot.Players) + { + rank++; + @RenderCompactRow(p, rank) + } +
+ } +
+ } + + @* ── RECENT ACTIVITY: vertical thread ── *@ + @if (Snapshot.RecentEvents.Count > 0) + { +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_RECENT_ACTIVITY") +
+
+ @{ var orderedEvents = Snapshot.RecentEvents.AsEnumerable().Reverse().ToList(); } + @* Timeline thread alignment: + - Row uses `items-center` so all three columns (time, icon, text) + share a vertical centre. Row height = tallest child = w-6 disc + = 24px. Text-xs and text-sm line-boxes both centre to y=12. + - mb-4 between rows (instead of pb-4 on text column) keeps vertical + spacing without breaking the items-center alignment. + - Icon column horizontal: w-16 (time) + gap-3 (12) → icon column + starts at 76px; w-6 disc centre at 76 + 12 = 88px. + - Connecting line at left-[88px], top-[24px] (icon bottom), + bottom-[-16px] (extends through the mb-4 gap to next row's + icon top). *@ + @for (var i = 0; i < orderedEvents.Count; i++) + { + var evt = orderedEvents[i]; + var visuals = GetEventVisuals(evt.Category); + var isLast = i == orderedEvents.Count - 1; + var isRound = evt.Category == "round"; + +
+ @if (!isLast) + { +
+ } + +
+ @evt.Time +
+ +
+ @if (isRound) + { +
+ } + else + { + @* Solid surface bg behind the tinted overlay so the connecting line + behind the disc doesn't bleed through the (translucent) /15 fill. *@ +
+
+ +
+
+ } +
+ +
+ + @evt.Label + + @if (isRound && evt.PaceBand is not null && evt.PaceRatio is { } ratio + && evt.PaceTypicalSeconds is { } typicalSec) + { + var paceTint = PaceVisuals.ColorClass(evt.PaceBand); + var pacePrefix = ratio > 0.05 ? "+" : (ratio < -0.05 ? "−" : ""); + var paceTooltip = LiveTimelinePaceTooltip(evt); + // Reconstruct the round's actual elapsed from typical and ratio + // (ratio = (actual - typical) / typical → actual = typical * (1 + ratio)). + // Avoids surfacing a bare "+2%" with no anchor — viewer needs to see + // the live time to make sense of the percent vs the avg. + var actualSec = typicalSec * (1 + ratio); + + @PaceVisuals.FormatRoundTime(actualSec) + @pacePrefix@(Math.Abs(ratio * 100).ToString("F0"))% + (@AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PACE_AVG_PREFIX") @PaceVisuals.FormatRoundTime(typicalSec)) + + } +
+
+ } +
+
+ } +
+
+ +@code { + + private double _elapsedSeconds; + private double? _roundElapsedSeconds; + + protected override void OnParametersSet() + { + var now = DateTimeOffset.UtcNow; + _elapsedSeconds = Math.Max(0, (now - Snapshot.MatchStartedAt).TotalSeconds); + _roundElapsedSeconds = Snapshot.CurrentRoundStartedAt is { } rs + ? Math.Max(0, (now - rs).TotalSeconds) + : null; + } + + @* ─────────── 1P: hero card ─────────── *@ + private RenderFragment RenderSoloHero(ZombieLivePlayerSnapshot p) => __builder => + { + var dmgInf = p.MatchDamageReceived == 0; + var dmgRatio = dmgInf ? 0 : (double)p.MatchDamageDealt / p.MatchDamageReceived; + var hsPct = p.MatchKills == 0 ? 0 : (double)p.MatchHeadshotKills / p.MatchKills * 100; + var kpd = p.MatchDowns == 0 ? p.MatchKills : (double)p.MatchKills / p.MatchDowns; + var net = p.MatchPointsEarned - p.MatchPointsSpent; + +
+
+
+ + + + @RenderJoinedBadge(p) +
+ @RenderStatusPill(p) +
+ +
+ @RenderHeroCell("ph-sword", AppState.Loc("WEBFRONT_ZOMBIE_LIVE_KILLS"), p.MatchKills.ToString("N0"), "text-foreground") + @RenderHeroCellInline("ph-activity", AppState.Loc("WEBFRONT_ZOMBIE_LIVE_DMG"), dmgInf, dmgRatio, "x") + @RenderHeroCellNet("ph-lightning", AppState.Loc("WEBFRONT_ZOMBIE_LIVE_NET_PTS"), net) + @RenderHeroCell("ph-target", AppState.Loc("WEBFRONT_ZOMBIE_LIVE_HS_PCT"), $"{hsPct:F0}%", "text-foreground") + @RenderHeroCell("ph-crosshair", AppState.Loc("WEBFRONT_ZOMBIE_LIVE_KPD"), kpd >= 1000 ? FormatCompactNumber((long)kpd) : kpd.ToString("F1"), "text-foreground") +
+ +
+ @RenderRoundCell(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_ROUND_KILLS"), p.CurrentRoundKills, "text-warning") + @RenderRoundCell(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_DOWNS"), p.CurrentRoundDowns, "text-orange-400") + @RenderRoundCell(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_DEATHS"), p.CurrentRoundDeaths, "text-error") +
+
+ }; + + @* ─────────── 2P: condensed card ─────────── *@ + private RenderFragment RenderDuoCard(ZombieLivePlayerSnapshot p) => __builder => + { + var dmgInf = p.MatchDamageReceived == 0; + var dmgRatio = dmgInf ? 0 : (double)p.MatchDamageDealt / p.MatchDamageReceived; + var net = p.MatchPointsEarned - p.MatchPointsSpent; + +
+
+
+ + + + @RenderJoinedBadge(p) +
+ @RenderStatusPill(p) +
+ +
+ @RenderDuoStat(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_KILLS"), p.MatchKills.ToString("N0"), "text-foreground") + @RenderDuoStatInline(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_DMG"), dmgInf, dmgRatio, "x") + @RenderDuoStat(AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PTS"), FormatCompactNumber(net), net >= 0 ? "text-foreground" : "text-error") +
+ +
+ @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_THIS_ROUND") + + @p.CurrentRoundKillsk + @p.CurrentRoundDownsd + @p.CurrentRoundDeathsx + +
+
+ }; + + @* ─────────── 3-8P: compact rows ─────────── *@ + private RenderFragment RenderCompactRow(ZombieLivePlayerSnapshot p, int rank) => __builder => + { + var dmgInf = p.MatchDamageReceived == 0; + var dmgRatio = dmgInf ? 0 : (double)p.MatchDamageDealt / p.MatchDamageReceived; + +
+ @(rank). + @{ + var (label, colorClass, _, dotClass, animate, tooltip) = StatusVisuals(p.Status, AppState); + } + + + + + @RenderJoinedBadge(p) + @if (p.Status != ZombieLivePlayerStatus.Alive) + { + @label + } + +
+ +
+
+ + +@p.CurrentRoundKills +
+
+ + @p.MatchKills.ToString("N0") +
+ +
+
+ }; + + @* ─────────── shared bits ─────────── *@ + + @* "Joined late" badge — inline next to the player name across all layouts so the + indicator never changes the host card's height (the previous footer-row variant + made duo cards uneven and hung pad off solo cards). Renders nothing when the + player was present from round 1. *@ + private RenderFragment RenderJoinedBadge(ZombieLivePlayerSnapshot p) => __builder => + { + if (p.JoinedRound is not { } jr || jr <= 1) return; + + + R@(jr) + + + }; + + private RenderFragment RenderStatusPill(ZombieLivePlayerSnapshot p) => __builder => + { + var (label, colorClass, bgClass, dotClass, animate, tooltip) = StatusVisuals(p.Status, AppState); + + @label + + }; + + private RenderFragment RenderHeroCell(string icon, string label, string value, string colorClass) => __builder => + { +
+ + @label + + @value +
+ }; + + private RenderFragment RenderHeroCellInline(string icon, string label, bool inf, double value, string suffix) => __builder => + { +
+ + @label + + + @if (inf) + { + + } + else + { + @value.ToString("N1")@suffix + } + +
+ }; + + private RenderFragment RenderHeroCellNet(string icon, string label, long value) => __builder => + { + var colorClass = value >= 0 ? "text-success" : "text-error"; +
+ + @label + + @FormatSigned(value) +
+ }; + + private RenderFragment RenderRoundCell(string label, int value, string colorClass) => __builder => + { +
+ @label + @value +
+ }; + + private RenderFragment RenderDuoStat(string label, string value, string colorClass) => __builder => + { +
+ @label + @value +
+ }; + + private RenderFragment RenderDuoStatInline(string label, bool inf, double value, string suffix) => __builder => + { +
+ @label + + @if (inf) + { + + } + else + { + @value.ToString("N1")@suffix + } + +
+ }; + + private static string FormatElapsed(double seconds) + { + var span = TimeSpan.FromSeconds(seconds); + return span.TotalHours >= 1 + ? $"{(int)span.TotalHours}h {span.Minutes}m" + : $"{span.Minutes}m {span.Seconds}s"; + } + + private static string FormatCompactNumber(long value) => value switch + { + <= -1_000_000 or >= 1_000_000 => $"{value / 1_000_000.0:F1}M", + <= -1_000 or >= 1_000 => $"{value / 1_000.0:F1}k", + _ => value.ToString("N0") + }; + + private static string FormatSigned(long value) => + (value > 0 ? "+" : "") + FormatCompactNumber(value); +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor.cs new file mode 100644 index 000000000..b993cc31e --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieLiveSnapshot.razor.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Components; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using WebfrontCore.Core.Services; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieLiveSnapshot +{ + [Parameter, EditorRequired] + public ZombieLiveMatchSnapshot Snapshot { get; set; } = default!; + + [Inject] public required AppState AppState { get; set; } + + private static (string Label, string Color, string Bg, string Dot, bool Animate, string Tooltip) + StatusVisuals(ZombieLivePlayerStatus status, AppState appState) => status switch + { + ZombieLivePlayerStatus.Alive => (appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_ALIVE"), "text-success", "bg-success/10", "bg-success", true, + appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_ALIVE_TOOLTIP")), + ZombieLivePlayerStatus.Down => (appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DOWN"), "text-orange-400", "bg-orange-400/10", "bg-orange-400", true, + appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DOWN_TOOLTIP")), + ZombieLivePlayerStatus.Dead => (appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DEAD"), "text-error", "bg-error/10", "bg-error", false, + appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DEAD_TOOLTIP")), + _ => (appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DISCONNECTED"), "text-subtle", "bg-subtle/10", "bg-subtle", false, + appState.Loc("WEBFRONT_ZOMBIE_LIVE_STATUS_DISCONNECTED_TOOLTIP")), + }; + + /// + /// Tooltip text for a round-completed event in the live timeline. Reuses the + /// same tooltip vocabulary as the round-table breakdown so users see a + /// consistent description across both surfaces. + /// + private string? LiveTimelinePaceTooltip(ZombieMatchHistoryEvent ev) + { + if (ev.PaceBand is not { } band || ev.PaceRatio is not { } ratio + || ev.PaceTypicalSeconds is not { } typical || ev.RoundNumber is not { } round) + { + return null; + } + + var playerLabel = PaceVisuals.PlayerLabel(Snapshot.CurrentRoundPlayerCount, AppState); + var typicalFormatted = PaceVisuals.FormatRoundTime(typical); + var absPercent = Math.Abs(ratio) * 100; + + return band switch + { + PaceBand.Neutral => AppState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_NEUTRAL") + .FormatExt(round, playerLabel, typicalFormatted), + PaceBand.Faster or PaceBand.MuchFaster => AppState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_FASTER") + .FormatExt(absPercent.ToString("F0"), round, playerLabel, typicalFormatted), + _ => AppState.Loc("WEBFRONT_ZOMBIE_ROUND_PACE_TOOLTIP_SLOWER") + .FormatExt(absPercent.ToString("F0"), round, playerLabel, typicalFormatted), + }; + } + + /// + /// Tooltip text for the live banner's elapsed timer. When the EMA target is + /// unknown (cold cell) we fall back to the original "round in progress" string; + /// when known we surface the current pace delta vs typical so the viewer + /// understands what the colour means. + /// + private string LiveBannerTooltip(double elapsedSeconds, (PaceBand Band, double Ratio)? band) + { + if (Snapshot.CurrentRoundEmaSeconds is not { } typical || typical <= 0 || band is not { } b) + { + return AppState.Loc("WEBFRONT_ZOMBIE_LIVE_CURRENT_ROUND_TOOLTIP"); + } + + var playerLabel = PaceVisuals.PlayerLabel(Snapshot.CurrentRoundPlayerCount, AppState); + var typicalFormatted = PaceVisuals.FormatRoundTime(typical); + var absPercent = Math.Abs(b.Ratio) * 100; + + return b.Band switch + { + PaceBand.Neutral => AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PACE_TOOLTIP_NEUTRAL") + .FormatExt(Snapshot.CurrentRound, playerLabel, typicalFormatted), + PaceBand.Faster or PaceBand.MuchFaster => AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PACE_TOOLTIP_FASTER") + .FormatExt(absPercent.ToString("F0"), Snapshot.CurrentRound, playerLabel, typicalFormatted), + _ => AppState.Loc("WEBFRONT_ZOMBIE_LIVE_PACE_TOOLTIP_SLOWER") + .FormatExt(absPercent.ToString("F0"), Snapshot.CurrentRound, playerLabel, typicalFormatted), + }; + } + + // Mirrors ZombieTimeline.GetEventVisuals — same icons/colors so the live activity + // feed matches the post-match timeline. Kept inline (rather than reusing the + // ZombieTimeline helper) because this view renders events as a list, not a strip, + // and doesn't need the ZIndex / IsTick bar-vs-icon decision logic. + private static (string Bg, string Text, string Icon, bool IsTick) GetEventVisuals(string category) => + category switch + { + "powerup" => ("bg-yellow-400", "text-yellow-400", "ph-lightning", false), + "danger" => ("bg-orange-500", "text-orange-500", "ph-warning", false), + "critical" => ("bg-error", "text-error", "ph-skull", false), + "success" => ("bg-success", "text-success", "ph-heartbeat", false), + "perk" => ("bg-purple-500", "text-purple-500", "ph-pill", false), + "weapon" => ("bg-info", "text-info", "ph-knife", false), + "box" => ("bg-blue-400", "text-blue-400", "ph-cube", false), + "box-pass" => ("bg-orange-400", "text-orange-400", "ph-cube", false), + "box-teddy" => ("bg-pink-400", "text-pink-400", "ph-spiral", false), + "door" => ("bg-amber-500", "text-amber-500", "ph-door-open", false), + "trap" => ("bg-red-400", "text-red-400", "ph-lightning", false), + "build" => ("bg-emerald-500", "text-emerald-500", "ph-wrench", false), + "session-join" => ("bg-slate-400", "text-slate-400", "ph-sign-in", false), + "session-leave" => ("bg-slate-500", "text-slate-500", "ph-sign-out", false), + // Mirrors zombie-scrubber.js easter-egg-step (amber + ph-trophy) so the + // live feed matches the post-match scrubber's EE dot visuals. + "easter-egg-step" => ("bg-amber-500", "text-amber-500", "ph-trophy", false), + "easter-egg" => ("bg-yellow-400", "text-yellow-400", "ph-trophy", false), + "power-on" => ("bg-yellow-400", "text-yellow-400", "ph-lightning", false), + "power-off" => ("bg-slate-400", "text-slate-400", "ph-lightning-slash", false), + "weapon-abandon" => ("bg-orange-400", "text-orange-400", "ph-knife", false), + "bank-deposit" => ("bg-green-500", "text-green-500", "ph-piggy-bank", false), + "bank-withdraw" => ("bg-amber-400", "text-amber-400", "ph-hand-coins", false), + "locker-store" => ("bg-blue-400", "text-blue-400", "ph-lockers", false), + "locker-retrieve" => ("bg-emerald-400", "text-emerald-400", "ph-lockers", false), + "gum-activate" => ("bg-pink-500", "text-pink-500", "ph-sparkle", false), + "gum-take" => ("bg-purple-500", "text-purple-500", "ph-gift", false), + "gum-leave" => ("bg-slate-400", "text-slate-400", "ph-heart-break", false), + _ => ("bg-muted", "text-muted", "", true) + }; +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor new file mode 100644 index 000000000..1551e4f89 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor @@ -0,0 +1,295 @@ +@using SharedLibraryCore.Interfaces +@inject AppState AppState +@* Match detail panel with per-player tabs, timeline, and round breakdown *@ + +@if (Detail.Players.Count == 0) +{ + return; +} + +
+ + @* Match-level achievement badges live on the consumer's titlebar (leaderboard + row, history row, share-page hero) via ZombieAchievementBadges; surfacing + them again inside the expanded view duplicates the same signal. *@ + + @* ── PLAYER ROSTER + TABS (hidden when selection is controlled externally) ── + Match Page only (SelectedClientId is null). Leaderboard expansion already + shows its own player roster + scoreboard, then drives Detail with a fixed + SelectedClientId for the timeline drill-down. + + Roster on top mirrors the leaderboard's adaptive 1P / 2P / 3-8 layout so + Match Page and Leaderboard read the same way. Drop-in players follow the + existing ShowAllPlayers toggle (same source of truth as the tabs below). *@ + @if (SelectedClientId is null) + { + @* Roster: leaderboard-style adaptive cards (qualified by default; + ShowAllPlayers includes drop-ins). Match the leaderboard's + rounded-md / border-line/50 / bg-surface-alt/30 weight exactly. *@ + var rosterPlayers = ShowAllPlayers + ? Detail.Players + : Detail.Players.Where(p => p.IsQualified).ToList(); + var qualifiedRoster = Detail.Players.Where(p => p.IsQualified).ToList(); + @if (rosterPlayers.Count > 0) + { +
+ @if (qualifiedRoster.Count == 1 && !ShowAllPlayers) + { + var p = qualifiedRoster[0]; + var soloInf = p.DamageReceived == 0; + var soloRatio = soloInf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER") +
+ + + + +
+
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_KILLS") + @p.Kills.ToString("N0") +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DMG_RATIO") + + @if (soloInf) + { + + } + else + { + @soloRatio.ToString("N1")x + } + +
+
+ } + else if (qualifiedRoster.Count == 2 && !ShowAllPlayers) + { +
+ @foreach (var p in qualifiedRoster) + { + var inf = p.DamageReceived == 0; + var ratio = inf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_PLAYER") +
+ + + + +
+
+
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_KILLS") + @p.Kills.ToString("N0") +
+
+ @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_DMG") + + @if (inf) + { + + } + else + { + @ratio.ToString("N1")x + } + +
+
+
+ } +
+ } + else + { + @* 3-8 players (or ShowAllPlayers=true): compact dotted-leader list, + two-column on md+. Drop-ins render with reduced opacity to match + the leaderboard scoreboard's drop-in styling. *@ +
+ @{ var rosterIdx = 0; } + @foreach (var p in rosterPlayers) + { + rosterIdx++; + var inf = p.DamageReceived == 0; + var ratio = inf ? 0 : (double)p.DamageDealt / p.DamageReceived; +
+ @(rosterIdx). + + + + + + +
+ + @p.Kills.ToString("N0")K + | + + @if (inf) + { + + } + else + { + @ratio.ToString("N1")x + } + + +
+ } +
+ } +
+ } + +
+ @foreach (var player in VisiblePlayers) + { + var isActive = player.ClientId == ActiveClientId; + + } +
+ } + + @{ + var activePlayer = SelectedPlayer; + } + + @if (activePlayer is not null) + { + // Computed-stat helpers — same math as the leaderboard scoreboard table + // (ZombieLeaderboard.razor:344-348) so the dedicated match page reads + // consistent values to the bucket scoreboard. Guards on /0 below. + var kpd = activePlayer.Downs == 0 ? activePlayer.Kills : (double)activePlayer.Kills / activePlayer.Downs; + var hsPercent = activePlayer.Kills == 0 ? 0 : (double)activePlayer.HeadshotKills / activePlayer.Kills * 100; + var netPoints = activePlayer.PointsEarned - activePlayer.PointsSpent; + var dmgRatioInfinite = activePlayer.DamageReceived == 0; + var dmgRatio = dmgRatioInfinite ? 0 : (double)activePlayer.DamageDealt / activePlayer.DamageReceived; + // Per-round denominator for pacing-style subtitles ("X/round"). Uses the + // player's actual rounds played (not the match max) so a drop-in's pace + // reflects their tenure rather than the full match — fairer pacing read. + // Falls back to 1 to avoid /0 on legacy rows missing round data. + var roundsPlayed = activePlayer.Rounds.Count > 0 ? activePlayer.Rounds.Count : 1; + var perRoundDash = "—"; + var killsPerRound = activePlayer.Kills > 0 ? $"{(double)activePlayer.Kills / roundsPlayed:F1}/round" : perRoundDash; + var deathsPerRound = activePlayer.Deaths > 0 ? $"{(double)activePlayer.Deaths / roundsPlayed:F2}/round" : perRoundDash; + var downsPerRound = activePlayer.Downs > 0 ? $"{(double)activePlayer.Downs / roundsPlayed:F2}/round" : perRoundDash; + var revsPerRound = activePlayer.Revives > 0 ? $"{(double)activePlayer.Revives / roundsPlayed:F2}/round" : perRoundDash; +
+ + @* ── HEADER ROW ── name link (right) anchored in a compact bordered + card so it doesn't read as floating text, with the assistance + badge to the left. Renders only when selection isn't externally + controlled (leaderboard already labels the row the user clicked). + Hyperlinked to the player profile so the dedicated match page is + a launchpad to the broader stats — not a dead-end view. *@ + @if (SelectedClientId is null) + { + + } + + @* ── PLAYER SUMMARY ── 4-col responsive grid of stat cards. + Mirrors the leaderboard scoreboard's column set so users get the + same numbers regardless of which surface they land on. Every card + carries a subtitle so all 8 cards share the same height — empty + subtitles caused a visible vertical-gap delta in the previous + layout. Subtitle convention: + • Derived stats: the underlying values that feed the math + (KPD = K/D, Net = Earned-Spent, HS% = HS count, DmgRatio = D/R) + • Raw counts: per-round pacing ("X/round"), with em-dash when + the value is 0 (no signal to convey, but height is preserved). *@ +
+ @RenderStatCard("ph-sword", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_KILLS"), activePlayer.Kills.ToString("N0"), killsPerRound) + @RenderStatCard("ph-skull", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_DEATHS"), activePlayer.Deaths.ToString(), deathsPerRound) + @RenderStatCard("ph-target", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_KPD"), kpd > 999 ? FormatK((int)kpd) : kpd.ToString("F1"), + $"{activePlayer.Kills:N0} / {activePlayer.Downs}") + @RenderStatCard("ph-crosshair", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_HS_PCT"), $"{hsPercent:F0}%", + $"{activePlayer.HeadshotKills:N0} HS") + @RenderStatCard("ph-warning-circle", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_DOWNS"), activePlayer.Downs.ToString(), downsPerRound) + @RenderStatCard("ph-first-aid", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_REVIVES"), activePlayer.Revives.ToString(), revsPerRound) + @RenderStatCard("ph-coins", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_NET_POINTS"), + $"{(netPoints > 0 ? "+" : "")}{FormatK((int)netPoints)}", + $"{activePlayer.PointsEarned:N0} − {activePlayer.PointsSpent:N0}") + @RenderStatCard("ph-shield-warning", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_DETAIL_DMG_RATIO"), + dmgRatioInfinite ? "∞" : dmgRatio.ToString("N1"), + $"{activePlayer.DamageDealt:N0} / {activePlayer.DamageReceived:N0}") +
+ + @* ── TIMELINE (multi-lane scrubber) ── *@ + @if (Detail.Players.Any(p => p.Events.Count > 0)) + { + + } + + @* ── ROUND TABLE ── *@ + @if (activePlayer.Rounds.Count > 0) + { + + } +
+ } +
+ +@code { + + /// + /// Stat card — small bordered tile with icon + label + value, optional + /// subtitle for the underlying values that feed a derived stat (e.g. + /// "612 / 1" under a KPD card). Drives the 4-col responsive grid. + /// + private RenderFragment RenderStatCard(string icon, string label, string value, string? subtitle = null) => __builder => + { +
+
+ + @label +
+ @value + @if (!string.IsNullOrEmpty(subtitle)) + { + @subtitle + } +
+ }; + + /// + /// Compact "K"-suffixed integer formatter (e.g. 12345 -> "12.3K"). Mirrors + /// the leaderboard scoreboard's formatter so the dedicated match page + /// renders the same compact values for high-magnitude stats (Net Points). + /// + private static string FormatK(int value) + { + if (Math.Abs(value) < 1000) return value.ToString(); + return (value / 1000.0).ToString("F1") + "K"; + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor.cs new file mode 100644 index 000000000..3d4a6904d --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchDetail.razor.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Components; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieMatchDetail +{ + [Parameter, EditorRequired] + public SharedLibraryCore.Interfaces.ZombieMatchDetail Detail { get; set; } = default!; + + /// + /// When set, the player tab bar is hidden — selection is controlled externally. + /// + [Parameter] + public int? SelectedClientId { get; set; } + + /// + /// Forwarded to so the consumer's SHOW_ALL + /// toggle (e.g. the leaderboard card) governs both scoreboard rows AND timeline + /// lane visibility from a single control. Default false (qualified-only). + /// Standalone share page (ZombieMatchPage) sets this true so a deep-link + /// to a match shows the full roster. + /// + [Parameter] + public bool ShowAllPlayers { get; set; } + + private int _internalSelectedClientId; + + // Memoized to keep reference stable across renders — scrubber treats payload + // identity change as a full reinit (which discards zoom/scroll). Recomputed + // only when Detail itself changes. + private SharedLibraryCore.Interfaces.ZombieMatchDetail? _memoizedDetailKey; + private ZombieScrubberPayload? _memoizedPayload; + + private ZombieScrubberPayload ScrubberPayload + { + get + { + if (!ReferenceEquals(_memoizedDetailKey, Detail) || _memoizedPayload is null) + { + _memoizedPayload = ZombieScrubberPayload.From(Detail, AppState.Loc); + _memoizedDetailKey = Detail; + } + return _memoizedPayload; + } + } + + private int ActiveClientId => SelectedClientId ?? _internalSelectedClientId; + + /// + /// Players to surface in the tab bar. When the consumer hides drop-ins + /// ( = false), unqualified lanes also drop from + /// the tabs so the tab bar matches the timeline lanes — same control, same + /// visible roster. The active selection auto-falls-back to the first + /// visible player if the previously-active one just hid. + /// + private IEnumerable VisiblePlayers => + ShowAllPlayers ? Detail.Players : Detail.Players.Where(p => p.IsQualified); + + protected override void OnParametersSet() + { + // Active player must always be in the visible set; if the SHOW_ALL + // toggle just hid the previously-active drop-in, snap to the first + // visible qualifier so the panel never renders an empty body. + var visible = VisiblePlayers.ToList(); + if (visible.Count > 0 && visible.All(p => p.ClientId != ActiveClientId)) + { + _internalSelectedClientId = visible[0].ClientId; + } + } + + private ZombieMatchDetailPlayer? SelectedPlayer => + VisiblePlayers.FirstOrDefault(p => p.ClientId == ActiveClientId); + + private void SelectPlayer(int clientId) => _internalSelectedClientId = clientId; +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor new file mode 100644 index 000000000..d095087df --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor @@ -0,0 +1,142 @@ +@using SharedLibraryCore.Interfaces +@inject AppState AppState +@if (Matches.Count == 0) +{ + return; +} + +
+
+ +

@AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_TITLE")

+
+ + @foreach (var match in Matches) + { + var state = GetCardState(match.MatchId); + var hasDetails = match.Rounds.Count > 0 || match.Events.Count > 0; + +
+ + @* ── CARD HEADER ── *@ +
+ +
+
+ +
+
+
+

+ @match.Map +

+ + +
+ @if (!string.IsNullOrEmpty(match.ServerName)) + { +
+ + +
+ } +
@match.Date
+
+
+ +
+ @RenderStatBadge("ph-flag-checkered", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_ROUND"), match.HighestRound.ToString(), true) + @RenderStatBadge("ph-clock", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_DURATION"), $"{match.DurationMinutes:F0}m", false) + @RenderStatBadge("ph-sword", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_KILLS"), match.Kills.ToString("N0"), true) + @RenderStatBadge("ph-skull", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_DEATHS"), match.Deaths.ToString(), false) + @RenderStatBadge("ph-coins", AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_POINTS"), match.PointsEarned.ToString("N0"), true) + + +
+
+ + @* ── EXPANDED CONTENT ── *@ + @if (state.IsExpanded && hasDetails) + { +
+ + @* "View Full Match" — this card shows the focused player's slice + only (per-client view by design). The dedicated match page + carries the full multi-player roster, timeline lanes for every + drop-in, and the share-link affordance. Anchored top-right of + the expanded panel so it's reachable without scrolling. + URL intentionally omits the client suffix — passing it would set + SelectedClientId on the destination page, which suppresses the + player tab bar there. Letting the user pick from tabs reads more + naturally than landing pinned to themselves with no visible + option to view a teammate. *@ + + + @if (match.Events.Count > 0) + { + + } + + @if (match.Rounds.Count > 0) + { + + } + +
+ } + else if (state.IsExpanded) + { +
+ @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_NO_DATA") +
+ } +
+ } +
+ +@code { + + private RenderFragment RenderStatBadge(string icon, string label, string value, bool highlight) => __builder => + { +
+
+ + +
+ + @value + +
+ }; +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor.cs new file mode 100644 index 000000000..51f66f15e --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchHistory.razor.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Components; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieMatchHistory +{ + [Parameter, EditorRequired] + public List Matches { get; set; } = []; + + [Parameter, EditorRequired] + public int ClientId { get; set; } + + private readonly Dictionary _cardStates = new(); + + private MatchCardState GetCardState(int matchId) + { + if (!_cardStates.TryGetValue(matchId, out var state)) + { + state = new MatchCardState(); + _cardStates[matchId] = state; + } + + return state; + } + + private void ToggleExpand(int matchId) => + GetCardState(matchId).IsExpanded = !GetCardState(matchId).IsExpanded; + + private class MatchCardState + { + public bool IsExpanded { get; set; } + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor new file mode 100644 index 000000000..be745b49b --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor @@ -0,0 +1,373 @@ +@page "/zombie/match/{MatchId:int}" +@page "/zombie/match/{MatchId:int}/{ClientId:int?}" +@rendermode InteractiveServer +@using SharedLibraryCore.Interfaces +@inject AppState AppState +@inject NavigationManager NavManager +@inject IServiceProvider ServiceProvider + +@* + Public, anonymous, deep-linkable share page for one zombie match. Mirrors the + Profile.razor OG-meta pattern: PersistentState carries server-rendered detail + through the SSR→interactive handoff so Discord/Twitter crawlers see the meta + tags without running JS. +*@ + +@(_ogTitle) | @AppState.WebfrontBranding + + + + + + + + + + +@if (_premiumMissing) +{ +
+ +
+} +else if (Detail is null) +{ + @if (_loading) + { +
+ + @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_LOADING") +
+ } + else + { +
+ +
+ } +} +else +{ +
+ + @* ── HEADER ── *@ +
+
+
+
+ +
+
+

@Detail.Map

+
+ + @AppState.Loc("WEBFRONT_ZOMBIE_LEADERBOARD_ROUND").FormatExt(Detail.HighestRound) + + + @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_HISTORY_DURATION"): @Detail.DurationMinutes.ToString("F0")m + + @* Match start time: rendered UTC server-side; utcLocalTime.js + (in blazor_lib.js) swaps in the browser-local value at first + paint. The Tooltip exposes the canonical UTC on hover. *@ + + + @Detail.Date.UtcDateTime.ToStandardFormat() + + + @if (!string.IsNullOrEmpty(Detail.ServerName)) + { + + + } +
+
+
+ +
+ @* SHOW_ALL toggle — only renders when the match actually has drop-ins. + Mirrors the leaderboard's button (same translation keys, same icon + choice, same hover style) but starts in the inverted state because + the share page defaults to "show everyone". *@ + @{ + var dropInCount = Detail.Players.Count(p => !p.IsQualified); + } + @if (dropInCount > 0) + { + + } + +
+
+
+ + @* Achievements banner removed: it conflated the per-quest EE sections below + ("Easter Egg Completed on R9" was misleading when only the Archangel song + had completed, not the main quest), and Marathon/Flawless/Buildables now + live on the per-row chip strip + dedicated Buildables card. The per-quest + progress sections that follow are the authoritative EE surface. *@ + + @* ── PROGRESS PANEL ── EE quests + Buildables share one bordered container, + each tracker = a row separated by a horizontal divider. Mirrors the + live-modal merged layout so the static "match record" view reads the + same way as the ongoing-match view. Per-row: header (icon + name + X/Y + + bar) on top, step/inventory grid below in the same row's content + area. Step cards show absolute fire timestamp ("R7 • 13:19:38") — + relative "ago" lost the in-match snapshot context the user actually + wants when reviewing a record. + Buildables row only renders when the map has a configured iconic + system or any built items; on EE-only maps the panel collapses to + just the quest rows; on buildable-only maps it collapses the other + way. *@ + @{ + var hasBuildablesRow = Detail.BuildablesBuilt > 0 || Detail.BuildablesTotal.HasValue; + } +
+ @if (Detail.EasterEggQuests.Count == 0) + { + @* Maps with no configured EE quests (Nacht der Untoten is the only + stock case). Render a muted placeholder rather than collapsing the + panel entirely so the section's vertical position stays stable + across maps. *@ +
+
+ +
+
+

@AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_LABEL")

+

@AppState.Loc("WEBFRONT_ZOMBIE_MATCH_EE_NONE_ON_MAP")

+
+
+ } + @for (var qi = 0; qi < Detail.EasterEggQuests.Count; qi++) + { + var quest = Detail.EasterEggQuests[qi]; + var stepsByKey = quest.Steps.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); + var qPct = quest.Total > 0 ? Math.Min(100, quest.Completed * 100 / quest.Total) : 0; + var qDone = quest.IsComplete; + var isSong = quest.Id.StartsWith("song", StringComparison.Ordinal); + var dividerClass = qi > 0 ? "border-t border-line/60" : ""; + // Songs render purple to visually separate music EEs from the main quest + // (yellow/amber). Same brightness hierarchy preserved per state. Literal + // class strings keep Tailwind's purge happy. + var headIconBg = qDone + ? (isSong ? "bg-purple-400/15 text-purple-300" : "bg-yellow-400/15 text-yellow-400") + : (isSong ? "bg-surface-alt text-purple-400" : "bg-surface-alt text-amber-400"); + var countText = qDone + ? (isSong ? "text-purple-300" : "text-yellow-400") + : "text-muted"; + var barColor = qDone + ? (isSong ? "bg-purple-300" : "bg-yellow-400") + : (isSong ? "bg-purple-400" : "bg-amber-400"); + var stepFiredBg = isSong + ? "bg-purple-400/5 border-purple-400/30" + : "bg-yellow-400/5 border-yellow-400/30"; + var stepFiredIcon = isSong ? "text-purple-300" : "text-yellow-400"; + var stepCheckIcon = isSong ? "text-purple-300" : "text-yellow-400"; +
+
+
+ +
+
+
+
+

@AppState.Loc(quest.LocKey)

+ @if (!string.IsNullOrEmpty(quest.ActiveVariantLocKey)) + { + + @AppState.Loc("WEBFRONT_ZOMBIE_EE_VIA_PATH").FormatExt(AppState.Loc(quest.ActiveVariantLocKey)) + + } + else if (!string.IsNullOrEmpty(quest.BranchGroupId)) + { + + @AppState.Loc("WEBFRONT_ZOMBIE_EE_NO_PATH_CHOSEN") + + } +
+ + @quest.Completed / @quest.Total + +
+ +
+
+
+ + @* Step grid: round + absolute clock-time on a right-aligned mono caption + ("R7 • 13:19:38"). Per-element Tooltip surfaces the full step name (in + case truncated) and the canonical UTC timestamp (because the visible + text is converted to local time by utcLocalTime.js). *@ +
+ @foreach (var step in quest.Inventory) + { + var fired = stepsByKey.TryGetValue(step.Key, out var record); + var label = AppState.Loc(step.LocKey); + var utcDisplay = fired && record is not null + ? record.OccurredAt.UtcDateTime.ToStandardFormat() + : null; +
+ + +
@label
+
+ @if (fired && record is not null) + { +
+ @if (record.RoundNumber is { } rn && rn > 0) + { + R@(rn) + + } + + + @record.OccurredAt.UtcDateTime.ToString("HH:mm:ss") + + +
+ + } +
+ } +
+
+
+
+ } + + @* ── BUILDABLES ROW ── shares the same shell as the EE rows above + so the two trackers read as one panel. Same layout primitives + (10×10 icon, text-base title, h-1.5 progress bar, 3-col chip + grid) — only the colour swaps to blue (the buildables hue). *@ + @if (hasBuildablesRow) + { + var allBuilt = Detail.BuildablesTotal is { } total && total > 0 && Detail.BuildablesBuilt >= total; + var pct = Detail.BuildablesTotal is { } t2 && t2 > 0 + ? Math.Min(100, Detail.BuildablesBuilt * 100 / t2) + : 100; + var bDividerClass = Detail.EasterEggQuests.Count > 0 ? "border-t border-line/60" : ""; + var bHeadIconBg = allBuilt + ? "bg-blue-400/15 text-blue-300" + : "bg-surface-alt text-blue-400"; + var bCountText = allBuilt ? "text-blue-300" : "text-muted"; + var bBarColor = allBuilt ? "bg-blue-300" : "bg-blue-400"; +
+
+
+ +
+
+
+

@AppState.Loc("WEBFRONT_ZOMBIE_MATCH_BUILDABLES_LABEL")

+ + @Detail.BuildablesBuilt @(Detail.BuildablesTotal.HasValue ? $"/ {Detail.BuildablesTotal.Value}" : "") + +
+ + @if (Detail.BuildablesTotal.HasValue) + { +
+
+
+ } + + @* Inventory-driven checklist — mirrors the EE step grid above. + Every iconic item the map could register renders as a chip; + filled blue when built, muted when not. Falls back to a flat + "built only" list for unconfigured maps (custom maps, T4/T5) + where Inventory is empty but built items exist. *@ + @if (Detail.BuildableInventory.Count > 0) + { + var builtKeys = new HashSet( + Detail.BuildableNames.Select(b => b.Key), + StringComparer.OrdinalIgnoreCase); +
+ @foreach (var item in Detail.BuildableInventory) + { + var built = builtKeys.Contains(item.Key); +
+ +
@item.DisplayName
+ @if (built) + { + + } +
+ } +
+ } + else if (Detail.BuildableNames.Count > 0) + { + @* Unconfigured map fallback: no inventory template, just show what was built. *@ +
+ @foreach (var entry in Detail.BuildableNames) + { + + + @entry.DisplayName + + } +
+ } + + @* Extras: side-quest / non-iconic buildables. Surfaced separately + so they don't dilute the main count; the heading flags them as + bonus rather than core progression. *@ + @if (Detail.ExtraBuildableNames.Count > 0) + { +
+
+ + @AppState.Loc("WEBFRONT_ZOMBIE_MATCH_BUILDABLES_EXTRAS_LABEL").FormatExt(Detail.ExtraBuildablesBuilt) +
+
+ @foreach (var entry in Detail.ExtraBuildableNames) + { + + + @entry.DisplayName + + } +
+
+ } +
+
+
+ } + +
+ + @* ── DETAIL (existing component, scrubber + share button inside) ── + ShowAllPlayers=true: this is the dedicated share page for one match. + Hiding drop-ins here would silently misrepresent the run (e.g. share-link + a 4-player match where 1 didn't qualify and the page shows 3 only). + Leaderboard-card consumer defaults this false to honour the qualifying- + roster framing of the bucket leaderboard; share page wants the full + picture. *@ + +
+} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor.cs new file mode 100644 index 000000000..811337f12 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchPage.razor.cs @@ -0,0 +1,97 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieMatchPage +{ + [Parameter] public int MatchId { get; set; } + [Parameter] public int? ClientId { get; set; } + + [Inject] public required IJSRuntime JS { get; set; } + + /// + /// Persisted state — survives SSR-to-interactive handoff so the OG meta tags + /// land in the initial server-rendered HTML (bots don't run JS). + /// + [PersistentState(AllowUpdates = true)] + public MatchPageState? State { get; set; } + + private SharedLibraryCore.Interfaces.ZombieMatchDetail? Detail => State?.Detail; + private bool _premiumMissing; + private bool _loading; + private bool _copied; + private int _lastLoadedId; + + // Default true — this is the share page, hiding the drop-ins by default would + // silently misrepresent the run (a 4-player match showing as 3). User can flip + // to qualified-only via the same toggle the leaderboard uses, just inverted + // starting state. State lives here (not in the scrubber) so the player tab bar + // and timeline lanes update from one source. + private bool _showAllPlayers = true; + private void ToggleShowAllPlayers() => _showAllPlayers = !_showAllPlayers; + + private string _ogTitle => Detail is null + ? AppState.Loc("WEBFRONT_ZOMBIE_MATCH_LOADING") + : AppState.Loc("WEBFRONT_ZOMBIE_MATCH_OG_TITLE").FormatExt(Detail.HighestRound, Detail.Map); + + private string _ogDescription => Detail is null + ? AppState.Loc("WEBFRONT_ZOMBIE_MATCH_LOADING") + : AppState.Loc("WEBFRONT_ZOMBIE_MATCH_OG_DESCRIPTION").FormatExt( + Detail.Players.Count, + Detail.DurationMinutes.ToString("F0"), + Detail.Date.UtcDateTime.ToStandardFormat()); + + protected override async Task OnParametersSetAsync() + { + // Skip refetch when state was hydrated from SSR for the same match. + if (Detail is not null && Detail.MatchId == MatchId) + { + _lastLoadedId = MatchId; + return; + } + + var matchHistoryService = ServiceProvider.GetService(typeof(IZombieMatchHistoryService)) as IZombieMatchHistoryService; + if (matchHistoryService is null) + { + _premiumMissing = true; + return; + } + + _loading = true; + try + { + State ??= new MatchPageState(); + State.Detail = await matchHistoryService.GetMatchDetailAsync(MatchId); + _lastLoadedId = MatchId; + } + finally + { + _loading = false; + } + } + + private async Task CopyShareLink() + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", NavManager.Uri); + _copied = true; + StateHasChanged(); + await Task.Delay(2000); + _copied = false; + StateHasChanged(); + } + catch + { + // Clipboard API requires HTTPS or localhost — fail silently + } + } + + public class MatchPageState + { + public SharedLibraryCore.Interfaces.ZombieMatchDetail? Detail { get; set; } + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor new file mode 100644 index 000000000..8ba2b3841 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor @@ -0,0 +1,105 @@ +@using SharedLibraryCore.Interfaces +@inject AppState AppState +@inject IJSRuntime JS +@implements IAsyncDisposable +@* + Multi-lane Konva-rendered match timeline scrubber. Replaces ZombieTimeline.razor. + Owner-of-state: this component holds Filter / ZoomLevel / ScrubSeconds. JS owns + rendering + interaction, and notifies the component via dotnetRef callbacks. + Single-lane payload (per-client view) renders identically — JS decides layout. +*@ + +
+ + @* ── TOOLBAR ── *@ +
+
+ + @AppState.Loc("WEBFRONT_ZOMBIE_TIMELINE_TITLE") +
+ +
+ @* Lane-mode (qualified vs all-players) used to live here as a button on + the scrubber itself. Removed: it duplicated the SHOW_ALL toggle the + consumer (leaderboard card / match-detail page) already exposes, and + two toggles for the same concept invited drift. The scrubber now + consumes ShowAllPlayers as a parameter from the consumer; lane + visibility follows whatever the consumer's single SHOW_ALL state + says. Share-link to an unqualified player still auto-promotes + internally via ResolveLaneMode so the focused lane never hides. *@ + +
+ @foreach (var (key, locKey, color) in _filters) + { + var isActive = _filter == key; + + } +
+ +
+ + @FormatZoom(_zoomLevel)x + +
+
+
+ + @* ── CANVAS + EVENT WINDOW (single bordered container, divider between) ── *@ +
+ + @* DOM scrubber mount — JS owns the inner subtree (lane names column, + horizontal scroll area, track, dots, gaps, scrub cursor). The host + div carries no internal scroll/overflow rules; the inner .zsr-scroll + handles horizontal scroll when --zoom > 1. min-height exists only as + a CLS guard so the page doesn't reflow when JS finishes building. *@ +
+ + @* Divider + events-at-scrub-time panel inside the same container *@ + @if (_payload is not null && _payload.Lanes.Count > 0) + { +
+ @if (_windowEvents.Count > 0) + { +
+ @AppState.Loc("WEBFRONT_ZOMBIE_SCRUBBER_AT_TIME").FormatExt(_scrubTimeLabel, _scrubRoundLabel) +
+
+ @foreach (var w in _windowEvents) + { + @* items-baseline keeps mixed-size text rows visually aligned. + Time column uses min-w (not w-10 fixed) — at HH:MM:SS the + text is ~56 px wide; w-10 (40 px) clipped the box but + didn't clip overflow, so the name span sat ~16 px inside + the time text. shrink-0 keeps the column width stable. *@ +
+ @w.Time + + + + @w.Label +
+ } +
+ } + else + { +
+ @AppState.Loc("WEBFRONT_ZOMBIE_SCRUBBER_NO_EVENTS_IN_WINDOW") +
+ } +
+ } +
+
diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor.cs new file mode 100644 index 000000000..ed12efd08 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieMatchScrubber.razor.cs @@ -0,0 +1,271 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieMatchScrubber : IAsyncDisposable +{ + [Parameter, EditorRequired] public ZombieScrubberPayload Payload { get; set; } = default!; + + /// + /// When set, focuses that lane on initial render (dims others). Drives the + /// share-link `?player={clientId}` deep-link case. + /// + [Parameter] public int? FocusedClientId { get; set; } + + /// + /// Externally controls whether the scrubber surfaces unqualified drop-in + /// lanes alongside the qualifying roster. Driven by the consumer's + /// SHOW_ALL toggle (e.g. the leaderboard card's WEBFRONT_ZOMBIE_LEADERBOARD_SHOW_ALL + /// button) so a single user-facing control governs both the scoreboard's + /// visible rows and the timeline's visible lanes — no two-toggle drift. + /// Default false (qualified-only) matches the leaderboard's default and the + /// "this is a record of the qualifying run" framing. + /// + [Parameter] public bool ShowAllPlayers { get; set; } + + private readonly string _elementId = $"zombie-scrubber-{Guid.NewGuid():N}"; + private DotNetObjectReference? _dotnetRef; + private bool _initialized; + + private ZombieScrubberPayload? _payload; + private int? _lastFocusedClientId; + private bool _lastShowAllPlayers; + private double _zoomLevel = 1; + private string _filter = "all"; + // Derived from the ShowAllPlayers parameter + share-link auto-promote (see + // EnsureLaneModeShowsFocused). The JS side reads "qualified" / "all" strings; + // we keep that wire format internally so existing JS doesn't need to change. + private string _laneMode = "qualified"; + private double _scrubSeconds; + // JS-computed zoom-aware hit-window; defaults to ±5s pre-first-callback. + private double _scrubHalfWindow = 5; + private string _scrubTimeLabel = string.Empty; + private int _scrubRoundLabel; + private List _windowEvents = []; + + private static readonly (string key, string locKey, string color)[] _filters = + [ + ("all", "WEBFRONT_ZOMBIE_TIMELINE_FILTER_ALL", "text-foreground"), + ("critical", "WEBFRONT_ZOMBIE_TIMELINE_FILTER_DANGER", "text-error"), + ("powerups", "WEBFRONT_ZOMBIE_TIMELINE_FILTER_DROPS", "text-yellow-400"), + ("economy", "WEBFRONT_ZOMBIE_TIMELINE_FILTER_ECONOMY", "text-primary"), + ]; + + // Container height tracks the lanes JS will actually render — when the consumer + // toggles SHOW_ALL off, the unqualified lanes are hidden client-side but the + // container would otherwise keep their reserved vertical space. Shrinking with the + // visible lane count keeps the timeline tight and avoids awkward dead space. + private int _minHeight + { + get + { + if (Payload is null) return 120; + var visibleLanes = _laneMode == "all" + ? Payload.Lanes.Count + : Payload.Lanes.Count(l => l.IsQualified); + return Math.Max(120, 60 + visibleLanes * 44); + } + } + + protected override void OnParametersSet() + { + // Payload identity change → match swap → full reinit (data shape may differ). + if (!ReferenceEquals(_payload, Payload)) + { + _payload = Payload; + ResolveLaneMode(); + _lastFocusedClientId = FocusedClientId; + _lastShowAllPlayers = ShowAllPlayers; + if (_initialized) + { + _ = ReinitializeAsync(); + } + return; + } + + // Same payload, but focused player or external ShowAllPlayers toggle + // changed → soft update (preserves zoom/scroll position). + var focusChanged = _lastFocusedClientId != FocusedClientId; + var showAllChanged = _lastShowAllPlayers != ShowAllPlayers; + if (_initialized && (focusChanged || showAllChanged)) + { + _lastFocusedClientId = FocusedClientId; + _lastShowAllPlayers = ShowAllPlayers; + var previousLaneMode = _laneMode; + ResolveLaneMode(); + if (_laneMode != previousLaneMode) + { + _ = JS.InvokeVoidAsync("zombieScrubber.setLaneMode", _elementId, _laneMode).AsTask(); + } + if (focusChanged) + { + _ = JS.InvokeVoidAsync("zombieScrubber.focusClient", _elementId, FocusedClientId).AsTask(); + } + } + } + + /// + /// Resolves from the external + /// parameter, with one override: when the focused client maps to an unqualified + /// lane, force "all" so the share-link `?player={clientId}` case never lands on + /// a hidden lane. Without that override, deep-linking to a drop-in player would + /// silently hide the lane the user explicitly asked to see. + /// + private void ResolveLaneMode() + { + _laneMode = ShowAllPlayers ? "all" : "qualified"; + + if (_laneMode == "qualified" && FocusedClientId is not null && _payload is not null) + { + var focused = _payload.Lanes.FirstOrDefault(l => l.ClientId == FocusedClientId); + if (focused is not null && !focused.IsQualified) + { + _laneMode = "all"; + } + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && _payload is not null) + { + _dotnetRef = DotNetObjectReference.Create(this); + // Pass _laneMode explicitly — JS would otherwise default to 'qualified' + // when any lane qualifies, ignoring our resolved state. That mismatch + // bit the dedicated match page (ShowAllPlayers=true → Razor _laneMode + // = "all" but JS picked "qualified" → drop-ins missing on first paint). + await JS.InvokeVoidAsync("zombieScrubber.init", _elementId, _payload, _dotnetRef, FocusedClientId, _laneMode); + _initialized = true; + } + } + + private async Task ReinitializeAsync() + { + if (_dotnetRef is null) return; + await JS.InvokeVoidAsync("zombieScrubber.dispose", _elementId); + await JS.InvokeVoidAsync("zombieScrubber.init", _elementId, _payload, _dotnetRef, FocusedClientId, _laneMode); + UpdateWindowEvents(); + StateHasChanged(); + } + + private async Task SelectFilter(string key) + { + _filter = key; + if (_initialized) + { + await JS.InvokeVoidAsync("zombieScrubber.setFilter", _elementId, key); + } + } + + private async Task MultiplyZoom(double factor) + { + _zoomLevel = Math.Clamp(_zoomLevel * factor, 1, 20); + if (_initialized) + { + await JS.InvokeVoidAsync("zombieScrubber.setZoom", _elementId, _zoomLevel); + } + } + + private static string FormatZoom(double level) => + level >= 10 ? $"{level:F0}" : $"{level:F1}"; + + /// + /// Called from JS after wheel-zoom applies, so the toolbar +/- display reflects + /// the actual JS-side zoom level. Button path also fires this (idempotent). + /// + [JSInvokable] + public void OnZoomChanged(double level) + { + if (Math.Abs(_zoomLevel - level) < 0.001) return; + _zoomLevel = level; + StateHasChanged(); + } + + /// + /// Called from JS when scrubber cursor moves. Debounced JS-side (~50ms). + /// is JS-computed so the panel's hit + /// window scales with the current zoom — dot footprint visual width maps + /// to hit-window width at any zoom level. + /// + [JSInvokable] + public void OnScrubChanged(double seconds, double halfWindowSeconds) + { + _scrubSeconds = seconds; + _scrubHalfWindow = halfWindowSeconds > 0 ? halfWindowSeconds : 5; + UpdateWindowEvents(); + StateHasChanged(); + } + + /// + /// Called from JS when an event dot is clicked. Pins the side-panel window to that + /// time across all lanes. + /// + [JSInvokable] + public void OnEventClicked(int clientId, double seconds) + { + _scrubSeconds = seconds; + UpdateWindowEvents(); + StateHasChanged(); + } + + private void UpdateWindowEvents() + { + if (_payload is null) { _windowEvents = []; return; } + + var lo = _scrubSeconds - _scrubHalfWindow; + var hi = _scrubSeconds + _scrubHalfWindow; + + _windowEvents = _payload.Lanes + .SelectMany(l => l.Events + // Round-completion markers are excluded from the side panel — + // the R{n} round-band labels on the track convey the same info, + // and "Round 3" rows for every lane within a 5s window of a + // band boundary added noise without insight. + .Where(e => !string.Equals(e.Category, "round", StringComparison.OrdinalIgnoreCase)) + .Select(e => new WindowEvent + { + ClientId = l.ClientId, + PlayerName = l.Name, + Time = e.Time, + Label = e.Label, + Seconds = e.Seconds + })) + .Where(w => w.Seconds >= lo && w.Seconds <= hi) + .OrderBy(w => w.Seconds) + .Take(20) + .ToList(); + + _scrubTimeLabel = TimeSpan.FromSeconds(Math.Max(0, _scrubSeconds - _payload.MinSeconds)) + .ToString(@"mm\:ss"); + + var bandAtScrub = _payload.RoundBands + .FirstOrDefault(b => _scrubSeconds >= b.StartSeconds && _scrubSeconds <= b.EndSeconds); + _scrubRoundLabel = bandAtScrub?.RoundNumber ?? 0; + } + + public async ValueTask DisposeAsync() + { + try + { + if (_initialized) + { + await JS.InvokeVoidAsync("zombieScrubber.dispose", _elementId); + } + } + catch (JSDisconnectedException) { /* circuit gone */ } + catch (Microsoft.JSInterop.JSException) { /* page navigated */ } + + _dotnetRef?.Dispose(); + } + + private sealed class WindowEvent + { + public int ClientId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public string Time { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public double Seconds { get; set; } + } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor b/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor new file mode 100644 index 000000000..2649cae2e --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor @@ -0,0 +1,159 @@ +@using SharedLibraryCore.Interfaces +@using Data.Models.Zombie +@inject AppState AppState +@* + Shared per-round breakdown table. Stateless — pass in the rounds list, get a sortable + visual table back. Used by both ZombieMatchHistory (per-client) and ZombieMatchDetail + (per-match, per-player tab). + + When MatchHighestRound is supplied, gaps in the player's round set are surfaced as + dashed-red rows so users don't read "rounds 8-11 are missing from my record" as a bug. + The "joined late" gap (player's first round > 1) is also rendered when applicable. +*@ + +@if (Rounds.Count == 0) +{ + return; +} + +@{ + var maxKills = Math.Max(Rounds.Max(r => r.Kills), 1); + var orderedRounds = Rounds.OrderBy(r => r.RoundNumber).ToList(); + var firstRound = orderedRounds[0].RoundNumber; + var lastRound = orderedRounds[^1].RoundNumber; + var showJoinedLateGap = firstRound > 1; + var showLeftEarlyGap = MatchHighestRound is { } highest && lastRound < highest; + // Surface a Type column only when this match actually has a special round — + // an all-regular match shouldn't pay for an empty column. colSpan on gap + // rows updates accordingly so the dashed banner spans the full width. + var hasAnySpecial = orderedRounds.Any(r => r.SpecialType is not null); + var totalCols = hasAnySpecial ? 6 : 5; +} + +
+
+ + @AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_TITLE") +
+ +
+ + + + + @if (hasAnySpecial) + { + + } + + + + + + + + + @if (showJoinedLateGap) + { + @RenderGapRow(1, firstRound - 1, AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_JOINED_LATE"), totalCols) + } + + @for (var i = 0; i < orderedRounds.Count; i++) + { + var round = orderedRounds[i]; + var specialBadge = PaceVisuals.SpecialBadge(round.SpecialType, AppState); + + + @if (hasAnySpecial) + { + + } + + + + + + + @if (i < orderedRounds.Count - 1) + { + var next = orderedRounds[i + 1]; + if (next.RoundNumber > round.RoundNumber + 1) + { + @RenderGapRow(round.RoundNumber + 1, next.RoundNumber - 1, + AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_NOT_TRACKED"), totalCols) + } + } + } + + @if (showLeftEarlyGap) + { + @RenderGapRow(lastRound + 1, MatchHighestRound!.Value, AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_LEFT_EARLY"), totalCols) + } + + +
@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_ROUND")@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_TYPE")@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_KILLS")@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_DOWNS_DEATHS")@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_POINTS")@AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_TIME")
+ + @round.RoundNumber + + + @if (specialBadge is { } sb) + { + + + @sb.Label + + + } + +
+ @round.Kills +
+
+
+
+
+
+ @round.Downs + / + @round.Deaths + @round.Points.ToString("N0") + @{ + var paceTooltip = PaceTooltip(round); + var paceClass = PaceColorClass(round.PaceBand); + } + + @FormatRoundTime(round.DurationSeconds) + +
+
+
+ +@code { + + private RenderFragment RenderGapRow(int from, int to, string tooltip, int colSpan) => __builder => + { + var label = from == to + ? AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_GAP_SINGLE").FormatExt(from) + : AppState.Loc("WEBFRONT_ZOMBIE_ROUND_TABLE_GAP_RANGE").FormatExt(from, to); + @* Tooltip wraps the inner content (a real flex div with a measurable rect) + rather than the / — those are unreliable hover targets and JS + bounding-rect can be quirky on table rows. *@ + + + +
+
+ + @label + +
+
+
+ + + }; +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor.cs new file mode 100644 index 000000000..32ee142f7 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieRoundTable.razor.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +public partial class ZombieRoundTable +{ + [Parameter, EditorRequired] + public List Rounds { get; set; } = []; + + /// + /// Optional: the match's overall HighestRound. When supplied AND the player's last + /// tracked round is below it, a "left early" gap row is rendered at the bottom. + /// Without this, the table can only show internal gaps + "joined late" gaps. + /// + [Parameter] public int? MatchHighestRound { get; set; } + + private static string FormatRoundTime(double seconds) => PaceVisuals.FormatRoundTime(seconds); + private static string PaceColorClass(PaceBand? band) => PaceVisuals.ColorClass(band); + private string? PaceTooltip(ZombieMatchHistoryRound round) => PaceVisuals.RoundTooltip(round, AppState); +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieScrubberPayload.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieScrubberPayload.cs new file mode 100644 index 000000000..3ab931187 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieScrubberPayload.cs @@ -0,0 +1,328 @@ +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +/// +/// Slim DTO sent to zombie-scrubber.js via JSInterop. Carries per-lane events and +/// shared time axis. Built from either a full +/// (multi-player match-overview) or a single-player +/// (per-client view), since the JS renderer treats them identically — single-lane render +/// is auto-detected from Lanes.Count == 1. +/// +public sealed class ZombieScrubberPayload +{ + public double MinSeconds { get; set; } + public double MaxSeconds { get; set; } + public int HighestRound { get; set; } + public List RoundBands { get; set; } = []; + public List Lanes { get; set; } = []; + + /// + /// Match-level events (no per-player attribution) — EE step markers, canonical + /// EE-complete marker. JS renders these in a tick band above the lanes rather + /// than duplicating them onto every lane. Empty on the per-client view. + /// + public List MatchLevelEvents { get; set; } = []; + + public static ZombieScrubberPayload From(SharedLibraryCore.Interfaces.ZombieMatchDetail detail, + Func? loc = null) + { + loc ??= s => s; + + // Match-level events (per-quest completions + step-progress dots) live in + // their own band rather than getting cloned onto every lane — keeps the + // visual layout clean and gives the team-milestone semantics a dedicated + // visual home. + var matchLevelEvents = new List(); + + // One marker per completed quest at its own CompletedAt/Round (not the + // match-level OccurredAt — that's the FIRST-quest-completion time, so + // a 2-quest map would render both markers stacked at the first time). + foreach (var quest in detail.EasterEggQuests.Where(q => q.IsComplete && q.CompletedAt.HasValue)) + { + var elapsed = Math.Max(0, (quest.CompletedAt!.Value - detail.Date).TotalSeconds); + var shortLabel = !string.IsNullOrEmpty(quest.ShortLocKey) ? loc(quest.ShortLocKey) : loc(quest.LocKey); + matchLevelEvents.Add(new ScrubberEvent + { + Seconds = elapsed, + Time = FormatElapsed(elapsed), + Label = quest.CompletedRound is { } cr && cr > 0 ? $"{shortLabel} (R{cr})" : shortLabel, + Category = "easter-egg", + RoundNumber = quest.CompletedRound + }); + } + + // Step records: each becomes a tick on the band, in fire order. Label uses + // the inventory's loc-key (resolved to localized text via the supplied + // resolver) so the tooltip reads as human-friendly step names rather than + // raw resource keys. Step keys are globally unique, so flattening across + // quests doesn't collide. + var inventoryLabels = detail.EasterEggQuests + .SelectMany(q => q.Inventory) + .GroupBy(s => s.Key, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First().LocKey, StringComparer.OrdinalIgnoreCase); + foreach (var step in detail.EasterEggQuests.SelectMany(q => q.Steps)) + { + var elapsed = Math.Max(0, (step.OccurredAt - detail.Date).TotalSeconds); + var rawLabel = inventoryLabels.GetValueOrDefault(step.Key, step.Key); + matchLevelEvents.Add(new ScrubberEvent + { + Seconds = elapsed, + Time = FormatElapsed(elapsed), + Label = loc(rawLabel), + Category = "easter-egg-step", + RoundNumber = step.RoundNumber + }); + } + + AddPowerEventsToTimeline(detail.PowerStateChanges, detail.Date, matchLevelEvents, loc); + + matchLevelEvents = matchLevelEvents.OrderBy(e => e.Seconds).ToList(); + + var lanes = detail.Players + .Select(p => + { + var events = p.Events.Select(ScrubberEvent.From).ToList(); + events = events.OrderBy(e => e.Seconds).ToList(); + + return new ScrubberLane + { + ClientId = p.ClientId, + Name = p.Name, + IsQualified = p.IsQualified, + Events = events, + Gaps = ZombieTimelineGaps.Compute(p.Rounds, p.Events, detail.HighestRound) + .Select(g => new ScrubberGap + { + Start = g.Start, + End = g.End, + Tooltip = g.Tooltip, + Compact = g.Compact + }) + .ToList() + }; + }) + .ToList(); + + return Build(lanes, detail.HighestRound, detail.Players.SelectMany(p => p.Events), matchLevelEvents); + } + + public static ZombieScrubberPayload From(ZombieMatchHistoryMatch match, string playerName, int clientId, + Func? loc = null) + { + loc ??= s => s; + + // Per-client view doesn't carry MatchStartDate — synthesize from Date string + // so quest CompletedAt timestamps can be projected onto the same elapsed-seconds + // axis as match.Events. Round bands derive from match.Events directly. + DateTimeOffset? matchStart = DateTimeOffset.TryParse(match.Date, out var parsed) ? parsed : null; + + // Match-level events: per-quest completion markers + step-progress dots, mirroring + // the multi-player overload. Empty when matchStart can't be parsed. + var matchLevelEvents = new List(); + if (matchStart is not null) + { + foreach (var quest in match.EasterEggQuests.Where(q => q.IsComplete && q.CompletedAt.HasValue)) + { + var elapsed = Math.Max(0, (quest.CompletedAt!.Value - matchStart.Value).TotalSeconds); + var shortLabel = !string.IsNullOrEmpty(quest.ShortLocKey) ? loc(quest.ShortLocKey) : loc(quest.LocKey); + matchLevelEvents.Add(new ScrubberEvent + { + Seconds = elapsed, + Time = FormatElapsed(elapsed), + Label = quest.CompletedRound is { } cr && cr > 0 ? $"{shortLabel} (R{cr})" : shortLabel, + Category = "easter-egg", + RoundNumber = quest.CompletedRound + }); + } + + var inventoryLabels = match.EasterEggQuests + .SelectMany(q => q.Inventory) + .GroupBy(s => s.Key, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First().LocKey, StringComparer.OrdinalIgnoreCase); + foreach (var step in match.EasterEggQuests.SelectMany(q => q.Steps)) + { + var elapsed = Math.Max(0, (step.OccurredAt - matchStart.Value).TotalSeconds); + var rawLabel = inventoryLabels.GetValueOrDefault(step.Key, step.Key); + matchLevelEvents.Add(new ScrubberEvent + { + Seconds = elapsed, + Time = FormatElapsed(elapsed), + Label = loc(rawLabel), + Category = "easter-egg-step", + RoundNumber = step.RoundNumber + }); + } + + AddPowerEventsToTimeline(match.PowerStateChanges, matchStart.Value, matchLevelEvents, loc); + + matchLevelEvents = matchLevelEvents.OrderBy(e => e.Seconds).ToList(); + } + + var laneEvents = match.Events.Select(ScrubberEvent.From).OrderBy(e => e.Seconds).ToList(); + + var lane = new ScrubberLane + { + ClientId = clientId, + Name = playerName, + // Per-client view: the row only renders when the player qualified, so the + // lane is always qualified by construction. + IsQualified = true, + Events = laneEvents, + Gaps = ZombieTimelineGaps.Compute(match.Rounds, match.Events, match.HighestRound) + .Select(g => new ScrubberGap + { + Start = g.Start, + End = g.End, + Tooltip = g.Tooltip, + Compact = g.Compact + }) + .ToList() + }; + + return Build([lane], match.HighestRound, match.Events, matchLevelEvents); + } + + private static string FormatElapsed(double seconds) + { + var ts = TimeSpan.FromSeconds(seconds); + return ts.TotalHours >= 1 + ? $"{(int)ts.TotalHours}:{ts.Minutes:D2}:{ts.Seconds:D2}" + : $"{ts.Minutes:D2}:{ts.Seconds:D2}"; + } + + /// + /// Project power-state transitions onto the match-level events band as discrete + /// markers. Player attribution (when present) goes into the label so the JS-side + /// tooltip surfaces it. Categories distinguish on/off so the renderer can use + /// different icons / colours per state. Player names are stripped of in-game + /// colour codes (^1, ^7 etc.) since the tooltip is plain text. + /// + private static void AddPowerEventsToTimeline( + List changes, + DateTimeOffset matchStart, + List matchLevelEvents, + Func loc) + { + foreach (var change in changes) + { + var elapsed = Math.Max(0, (change.OccurredAt - matchStart).TotalSeconds); + var stateLabel = change.IsOn + ? loc("WEBFRONT_ZOMBIE_MATCH_POWER_ON") + : loc("WEBFRONT_ZOMBIE_MATCH_POWER_OFF"); + var label = loc("WEBFRONT_ZOMBIE_MATCH_POWER_LABEL") + ": " + stateLabel; + if (!string.IsNullOrEmpty(change.PlayerName)) + { + label += " · " + change.PlayerName.StripColors(); + } + matchLevelEvents.Add(new ScrubberEvent + { + Seconds = elapsed, + Time = FormatElapsed(elapsed), + Label = label, + Category = change.IsOn ? "power-on" : "power-off", + RoundNumber = change.Round + }); + } + } + + private static ZombieScrubberPayload Build( + List lanes, + int highestRound, + IEnumerable allEventsForBands, + List matchLevelEvents) + { + // Round bands derive from any player's round-marker events. Group by round, take + // the earliest second-mark per round (covers the case where a late joiner has a + // later "round X" marker — the first-to-fire is the actual transition). + var roundSeconds = allEventsForBands + .Where(e => e.Category == "round" && e.RoundNumber.HasValue) + .GroupBy(e => e.RoundNumber!.Value) + .OrderBy(g => g.Key) + .Select(g => new { Round = g.Key, Seconds = g.Min(e => e.Seconds) }) + .ToList(); + + var bands = new List(); + for (var i = 0; i < roundSeconds.Count; i++) + { + var cur = roundSeconds[i]; + // End of band = start of next round, or extended to last event time on the + // final round so the band visually covers the tail of the match. + var end = i + 1 < roundSeconds.Count + ? roundSeconds[i + 1].Seconds + : Math.Max(cur.Seconds, AllEventsMaxSeconds(lanes)); + + bands.Add(new ScrubberRoundBand + { + RoundNumber = cur.Round, + StartSeconds = cur.Seconds, + EndSeconds = end + }); + } + + var allTimes = lanes.SelectMany(l => l.Events).Select(e => e.Seconds) + .Concat(lanes.SelectMany(l => l.Gaps).SelectMany(g => new[] { g.Start, g.End })) + .Concat(bands.SelectMany(b => new[] { b.StartSeconds, b.EndSeconds })) + .Concat(matchLevelEvents.Select(e => e.Seconds)) + .ToList(); + + return new ZombieScrubberPayload + { + MinSeconds = allTimes.Count > 0 ? allTimes.Min() : 0, + MaxSeconds = allTimes.Count > 0 ? allTimes.Max() : 1, + HighestRound = highestRound, + RoundBands = bands, + Lanes = lanes, + MatchLevelEvents = matchLevelEvents + }; + } + + private static double AllEventsMaxSeconds(List lanes) + { + var allEventTimes = lanes.SelectMany(l => l.Events).Select(e => e.Seconds).ToList(); + return allEventTimes.Count > 0 ? allEventTimes.Max() : 0; + } +} + +public sealed class ScrubberRoundBand +{ + public int RoundNumber { get; set; } + public double StartSeconds { get; set; } + public double EndSeconds { get; set; } +} + +public sealed class ScrubberLane +{ + public int ClientId { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsQualified { get; set; } + public List Events { get; set; } = []; + public List Gaps { get; set; } = []; +} + +public sealed class ScrubberEvent +{ + public double Seconds { get; set; } + public string Time { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public int? RoundNumber { get; set; } + + public static ScrubberEvent From(ZombieMatchHistoryEvent e) => new() + { + Seconds = e.Seconds, + Time = e.Time, + Label = e.Label, + Category = e.Category, + RoundNumber = e.RoundNumber + }; +} + +public sealed class ScrubberGap +{ + public double Start { get; set; } + public double End { get; set; } + public string Tooltip { get; set; } = string.Empty; + public bool Compact { get; set; } +} diff --git a/WebfrontCore/Components/Features/Clients/Statistics/ZombieTimelineGaps.cs b/WebfrontCore/Components/Features/Clients/Statistics/ZombieTimelineGaps.cs new file mode 100644 index 000000000..8539d24a4 --- /dev/null +++ b/WebfrontCore/Components/Features/Clients/Statistics/ZombieTimelineGaps.cs @@ -0,0 +1,114 @@ +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Clients.Statistics; + +/// +/// Helper for deriving timeline gap ranges (start/end seconds, elapsed-since-match-start) +/// from a player's round set + the match's round-marker events. Lets both +/// (per-client) and +/// (per-match-per-player) compute gap ranges identically before handing them to +/// for rendering. +/// +/// A gap is any contiguous span of rounds the player wasn't tracked in. The endpoints +/// of each gap are looked up in the round-marker events: gap (R_a..R_b) maps to +/// [seconds(R_a), seconds(R_{b+1})]. Gaps at the start (before player's first round) +/// and end (after player's last round) of the match are also surfaced. +/// +public static class ZombieTimelineGaps +{ + /// + /// A gap range. = true for mid-match interruptions that + /// should render as a slim centered marker (point-in-time event) rather than a + /// full-width band (which is reserved for joined-late / left-early — those + /// describe genuine extents of player presence). + /// + public record GapRange(double Start, double End, string Tooltip, bool Compact); + + public static List Compute( + List rounds, + List events, + int? matchHighestRound) + { + if (rounds.Count == 0) return []; + + // Map roundNumber -> seconds, from the match's round-marker events. + var roundSeconds = events + .Where(e => e.Category == "round" && e.RoundNumber.HasValue) + .GroupBy(e => e.RoundNumber!.Value) + .ToDictionary(g => g.Key, g => g.Min(e => e.Seconds)); + + var orderedRounds = rounds.OrderBy(r => r.RoundNumber).Select(r => r.RoundNumber).ToList(); + var firstPlayed = orderedRounds[0]; + var lastPlayed = orderedRounds[^1]; + + var gaps = new List(); + + // "Joined late" gap — from match start (round 1) up to the player's first round. + // Only render when we know where round 1 was AND where the player's first round was. + if (firstPlayed > 1 && roundSeconds.TryGetValue(firstPlayed, out var firstPlayedSeconds)) + { + gaps.Add(new GapRange(0, firstPlayedSeconds, "Player joined the match late", false)); + } + + // Internal gaps between consecutive played rounds. + for (var i = 0; i < orderedRounds.Count - 1; i++) + { + var current = orderedRounds[i]; + var next = orderedRounds[i + 1]; + if (next <= current + 1) continue; + + // Gap covers rounds (current+1)..(next-1). In seconds, from when round + // (current+1) started up to when round next started. + if (roundSeconds.TryGetValue(current + 1, out var gapStart) && + roundSeconds.TryGetValue(next, out var gapEnd)) + { + gaps.Add(new GapRange(gapStart, gapEnd, "Player wasn't tracked here", true)); + } + } + + // "Left early" gap — from end of player's last round to the match's last round. + if (matchHighestRound is { } highest && lastPlayed < highest) + { + // Gap starts at the round after the player's last played round. + if (roundSeconds.TryGetValue(lastPlayed + 1, out var gapStart)) + { + // End at the last round-marker we have, or fall back to max event time. + var lastEventSeconds = events.Count > 0 ? events.Max(e => e.Seconds) : gapStart; + gaps.Add(new GapRange(gapStart, lastEventSeconds, "Player left or disconnected before the match ended", false)); + } + } + + // "Round markers missing" gap — round-event continuity loss. Fires when the + // logged RoundCompleted events skip numbers AND the player did track rounds + // in the missing span (so we know they played, but the server-side markers + // didn't make it). Most likely cause: IW4MAdmin restart between rounds. + // Skipped when the player also has a gap there — the player-rounds gap above + // already covers that case with a more accurate "wasn't tracked" tooltip. + var playedRounds = orderedRounds.ToHashSet(); + var sortedRoundEvents = events + .Where(e => e.Category == "round" && e.RoundNumber.HasValue) + .OrderBy(e => e.RoundNumber!.Value) + .Select(e => (Round: e.RoundNumber!.Value, e.Seconds)) + .ToList(); + + for (var i = 0; i < sortedRoundEvents.Count - 1; i++) + { + var cur = sortedRoundEvents[i]; + var next = sortedRoundEvents[i + 1]; + if (next.Round <= cur.Round + 1) continue; + + var anyMissingWasPlayed = false; + for (var r = cur.Round + 1; r < next.Round; r++) + { + if (!playedRounds.Contains(r)) continue; + anyMissingWasPlayed = true; + break; + } + if (!anyMissingWasPlayed) continue; + + gaps.Add(new GapRange(cur.Seconds, next.Seconds, "Match data missing here", true)); + } + + return gaps; + } +} diff --git a/WebfrontCore/Components/Features/Home/Pages/Help.razor b/WebfrontCore/Components/Features/Home/Pages/Help.razor index 11e1db3d4..48df2bca8 100644 --- a/WebfrontCore/Components/Features/Home/Pages/Help.razor +++ b/WebfrontCore/Components/Features/Home/Pages/Help.razor @@ -56,7 +56,7 @@ -
+
@AppState.Loc("WEBFRONT_HELP_STAT_TOTAL") @CommandGroups.Sum(g => g.Commands.Count).ToString("#,##0") diff --git a/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor b/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor index bcb2aba94..03e3e72b5 100644 --- a/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor +++ b/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor @@ -1,6 +1,5 @@ @page "/Penalty/List" @page "/penalties" -@implements IAsyncDisposable @rendermode InteractiveServer @attribute [Authorize(Policy = "Permissions.Penalty.Read")] @@ -180,23 +179,23 @@
- - -
- @if (_isLoading && State is not null && State.Penalties.Count > 0) - { -
- } - else if (State?.HasMoreResults ?? false) - { -
- - } - else if (State?.Penalties.Count > 0) - { + @if (State?.HasMoreResults ?? false) + { + + +
+
+
+ } + else if (State?.Penalties.Count > 0) + { +
@AppState.Loc("WEBFRONT_PENALTY_NO_MORE") - } -
+
+ }
diff --git a/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor.cs b/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor.cs index b50b5e971..e4c00953f 100644 --- a/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor.cs +++ b/WebfrontCore/Components/Features/Penalties/Pages/PenaltyList.razor.cs @@ -1,6 +1,5 @@ using Data.Models; using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; using WebfrontCore.Core.Services; using PenaltyInfo = SharedLibraryCore.Dtos.PenaltyInfo; @@ -10,7 +9,6 @@ public partial class PenaltyList { [Inject] public required IWebfrontDataService DataService { get; set; } [Inject] public required AppState AppState { get; set; } - [Inject] public required IJSRuntime JS { get; set; } [Inject] public required NavigationManager NavManager { get; set; } [Inject] public required ILogger Logger { get; set; } @@ -24,7 +22,6 @@ public partial class PenaltyList private bool IgnoreAutomated { get; set; } = true; private EFPenalty.PenaltyType ShowOnly { get; set; } = EFPenalty.PenaltyType.Any; private bool _isLoading; - private DotNetObjectReference? _dotNetRef; protected override async Task OnInitializedAsync() { @@ -41,17 +38,7 @@ protected override async Task OnInitializedAsync() await LoadData(); } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - _dotNetRef = DotNetObjectReference.Create(this); - await JS.InvokeVoidAsync("window.infiniteScroll.initialize", _dotNetRef, "loadMoreTrigger"); - } - } - - [JSInvokable] - public async Task LoadMore() + private async Task LoadMore() { if (!(State?.HasMoreResults ?? false) || _isLoading) { @@ -133,20 +120,6 @@ private async Task LoadData() } } - public async ValueTask DisposeAsync() - { - try - { - await JS.InvokeVoidAsync("window.infiniteScroll.disconnect"); - } - catch (Exception ex) when (ex is InvalidOperationException or JSDisconnectedException) - { - // JS interop not available during static rendering - safe to ignore - } - - _dotNetRef?.Dispose(); - } - /// /// State class for persistent state serialization during SSR-to-interactive handoff. /// All display-relevant values are stored here to prevent flashing during enhanced navigation. diff --git a/WebfrontCore/Components/Features/Search/Pages/FindMessage.razor b/WebfrontCore/Components/Features/Search/Pages/FindMessage.razor index 3dd8643e9..649ed4f62 100644 --- a/WebfrontCore/Components/Features/Search/Pages/FindMessage.razor +++ b/WebfrontCore/Components/Features/Search/Pages/FindMessage.razor @@ -87,7 +87,7 @@ {
-
No messages found
+
@AppState.Loc("WEBFRONT_SEARCH_NO_MESSAGES")
} else @@ -117,25 +117,26 @@ - -
- @if (_isLoading && Results.Count > 0) - { -
- - @AppState.Loc("WEBFRONT_LOADING") -
- } - else if (_hasMore) - { -
- - } - else if (Results.Count > 0) - { + @if (_hasMore) + { + + +
+ + @AppState.Loc("WEBFRONT_LOADING") +
+
+
+ } + else if (Results.Count > 0) + { +
@AppState.Loc("WEBFRONT_SEARCH_NO_MORE_RESULTS")
- } -
+
+ } @@ -153,7 +154,7 @@

@AppState.Loc("WEBFRONT_CHAT_CONTEXT")

- ±5 minutes + @AppState.Loc("WEBFRONT_CHAT_CONTEXT_WINDOW")
+ } +
@@ -69,9 +79,12 @@
@@ -95,6 +108,13 @@ @Model.GameType } + @if (Model.ZombieRoundNumber is not null) + { +
+ Round + @Model.ZombieRoundNumber +
+ }
@AppState.Loc("WEBFRONT_SERVER_PLAYERS") @Model.ClientCount / @(Model.MaxClients - Model.PrivateClientSlots) diff --git a/WebfrontCore/Components/Features/Servers/Components/ServerCard.razor.cs b/WebfrontCore/Components/Features/Servers/Components/ServerCard.razor.cs index c397170ec..b741976bd 100644 --- a/WebfrontCore/Components/Features/Servers/Components/ServerCard.razor.cs +++ b/WebfrontCore/Components/Features/Servers/Components/ServerCard.razor.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using SharedLibraryCore; using SharedLibraryCore.Dtos; @@ -11,6 +12,7 @@ public partial class ServerCard : IAsyncDisposable [Inject] public required AppState AppState { get; set; } [Inject] public required IWebfrontDataService DataService { get; set; } [Inject] public required IActionService ActionService { get; set; } + [Inject] public required IToastService ToastService { get; set; } [Inject] public required IJSRuntime JS { get; set; } [Inject] public required ILogger Logger { get; set; } [Parameter, EditorRequired] public ServerInfo Model { get; set; } = default!; @@ -95,6 +97,30 @@ private async Task Refresh() } } + private async Task HandlePlayClick(MouseEventArgs e) + { + // Modifier-click copies the connect command to clipboard instead of opening + // the protocol handler. The icon swap (play -> clipboard) is driven by a + // body-level CSS class set on Ctrl/Meta keydown in blazor_lib.js, so the + // affordance is visible before the user actually clicks. + if (e.CtrlKey || e.MetaKey) + { + var connectCmd = $"connect {Model.ExternalIPAddress}:{Model.Port}"; + var ok = await JS.InvokeAsync("copyToClipboard", connectCmd); + if (ok) + { + await ToastService.ShowSuccessAsync(connectCmd, AppState.Loc("WEBFRONT_HOME_JOIN_COPIED")); + } + else + { + await ToastService.ShowErrorAsync(AppState.Loc("WEBFRONT_HOME_JOIN_COPY_FAILED")); + } + return; + } + + await JS.InvokeVoidAsync("openProtocolUrl", Model.ConnectProtocolUrl); + } + private void OpenScoreboard() { ActionService.OpenCustom(ScoreboardContent(Model.Id), Model.Name.StripColors(), "max-w-5xl"); @@ -107,6 +133,21 @@ private RenderFragment ScoreboardContent(string serverId) => builder => builder.CloseComponent(); }; + private void OpenZombieLive() + { + var serverName = Model.Name.StripColors(); + var mapName = string.IsNullOrEmpty(Model.Map) ? null : Model.Map; + var title = mapName is null ? serverName : $"{serverName} — {mapName}"; + ActionService.OpenCustom(ZombieLiveContent(Model.Id), title, "max-w-4xl"); + } + + private RenderFragment ZombieLiveContent(string serverId) => builder => + { + builder.OpenComponent(0, typeof(ZombieLiveModalWrapper)); + builder.AddAttribute(1, nameof(ZombieLiveModalWrapper.ServerId), serverId); + builder.CloseComponent(); + }; + public async ValueTask DisposeAsync() { await _cts.CancelAsync(); diff --git a/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor b/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor new file mode 100644 index 000000000..1b982f284 --- /dev/null +++ b/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor @@ -0,0 +1,26 @@ +@using SharedLibraryCore.Interfaces +@using WebfrontCore.Components.Features.Clients.Statistics + +@if (_isLoading) +{ +
+
+
+} +else if (_error != null) +{ +
+ @_error +
+} +else if (_snapshot == null) +{ +
+ + @AppState.Loc("WEBFRONT_ZOMBIE_LIVE_NO_MATCH") +
+} +else +{ + +} diff --git a/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor.cs b/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor.cs new file mode 100644 index 000000000..b25e2d8f7 --- /dev/null +++ b/WebfrontCore/Components/Features/Servers/Components/ZombieLiveModalWrapper.razor.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Components; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Components.Features.Servers.Components; + +/// +/// Modal wrapper for the zombie live-match snapshot. Mirrors +/// — same 5s PeriodicTimer cadence, same dispose pattern. The snapshot service may be null +/// (premium plugin not loaded) — we surface a friendly empty state in that case rather than +/// failing. +/// +public partial class ZombieLiveModalWrapper : IAsyncDisposable +{ + [Parameter, EditorRequired] public string ServerId { get; set; } = default!; + [Inject] public IZombieLiveMatchService? LiveMatchService { get; set; } + [Inject] public required ILogger Logger { get; set; } + [Inject] public required WebfrontCore.Core.Services.AppState AppState { get; set; } + + private bool _isLoading = true; + private string? _error; + private ZombieLiveMatchSnapshot? _snapshot; + private PeriodicTimer? _refreshTimer; + private CancellationTokenSource? _cts; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + + await LoadDataAsync(); + + _cts = new CancellationTokenSource(); + _refreshTimer = new PeriodicTimer(TimeSpan.FromSeconds(5)); + _ = RefreshLoopAsync(); + } + + private async Task LoadDataAsync() + { + try + { + if (LiveMatchService is not null) + { + _snapshot = await LiveMatchService.GetLiveMatchSnapshotAsync(ServerId); + } + _error = null; + } + catch (Exception ex) + { + _error = AppState.Loc("WEBFRONT_ZOMBIE_LIVE_LOAD_ERROR"); + Logger.LogError(ex, "Error loading live match snapshot for server {ServerId}", ServerId); + } + finally + { + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task RefreshLoopAsync() + { + if (_refreshTimer == null || _cts == null) return; + + try + { + while (await _refreshTimer.WaitForNextTickAsync(_cts.Token)) + { + try + { + if (LiveMatchService is not null) + { + _snapshot = await LiveMatchService.GetLiveMatchSnapshotAsync(ServerId); + await InvokeAsync(StateHasChanged); + } + } + catch + { + // Ignore refresh errors — keep showing the last good snapshot + } + } + } + catch (OperationCanceledException) + { + // Expected on dispose + } + } + + public async ValueTask DisposeAsync() + { + await (_cts?.CancelAsync() ?? Task.CompletedTask); + _cts?.Dispose(); + _refreshTimer?.Dispose(); + } +} diff --git a/WebfrontCore/Components/UI/Controls/InfiniteScrollSentinel.razor b/WebfrontCore/Components/UI/Controls/InfiniteScrollSentinel.razor new file mode 100644 index 000000000..a4522bf6c --- /dev/null +++ b/WebfrontCore/Components/UI/Controls/InfiniteScrollSentinel.razor @@ -0,0 +1,70 @@ +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JS + +@* + Encapsulates the sentinel + IntersectionObserver wiring so callers don't + repeat the IJSRuntime / DotNetObjectReference / OnAfterRenderAsync / + DisposeAsync boilerplate (see PenaltyList, AdvancedFind, etc. for the + pre-existing pattern this consolidates). + + Usage: caller owns the loaded list and HasMore/IsLoading state, places + this component at the bottom of the scrollable list. OnLoadMore fires + when the sentinel intersects the viewport. +*@ +
+ @if (IsLoading && LoadingTemplate is not null) + { + @LoadingTemplate + } +
+ +@code { + [Parameter, EditorRequired] public EventCallback OnLoadMore { get; set; } + [Parameter] public bool HasMore { get; set; } = true; + [Parameter] public bool IsLoading { get; set; } + [Parameter] public RenderFragment? LoadingTemplate { get; set; } + [Parameter] public string ContainerClass { get; set; } = ""; + + private string ElementId { get; } = $"infiniteScrollSentinel-{Guid.NewGuid():N}"; + private DotNetObjectReference? _dotNetRef; + private bool _observerInitialized; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotNetRef = DotNetObjectReference.Create(this); + await JS.InvokeVoidAsync( + "window.infiniteScroll.initialize", + _dotNetRef, + ElementId, + nameof(LoadMoreInternal)); + _observerInitialized = true; + } + } + + [JSInvokable] + public async Task LoadMoreInternal() + { + if (!HasMore || IsLoading) return; + await OnLoadMore.InvokeAsync(); + } + + public async ValueTask DisposeAsync() + { + if (_observerInitialized) + { + try + { + await JS.InvokeVoidAsync("window.infiniteScroll.disconnect", ElementId); + } + catch (Exception ex) when (ex is InvalidOperationException or JSDisconnectedException) + { + // JS interop unavailable during prerender / disconnect — safe to ignore. + } + } + + _dotNetRef?.Dispose(); + } +} diff --git a/WebfrontCore/Components/UI/Display/Tooltip.razor b/WebfrontCore/Components/UI/Display/Tooltip.razor index 0658df986..5dcdac81f 100644 --- a/WebfrontCore/Components/UI/Display/Tooltip.razor +++ b/WebfrontCore/Components/UI/Display/Tooltip.razor @@ -1,15 +1,18 @@ -@* Tooltip component - hover on desktop, tap on mobile *@ +@inject IJSRuntime JS + +@* Tooltip component - uses fixed positioning via JS to escape overflow containers *@ @if (!string.IsNullOrEmpty(Text)) { -
+
@ChildContent -
-
- @Text -
-
-
} else @@ -28,41 +31,57 @@ else } [Parameter] public string? Text { get; set; } - [Parameter, EditorRequired] public RenderFragment ChildContent { get; set; } = default!; + [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public TooltipDirection Direction { get; set; } = TooltipDirection.Up; [Parameter] public string? WrapperClass { get; set; } + [Parameter] public string? WrapperStyle { get; set; } + private ElementReference _triggerRef; private bool _showMobile; - private string TooltipClasses => Direction switch + private string DirectionString => Direction switch { - TooltipDirection.Up => $"absolute z-[9999] bottom-full left-1/2 -translate-x-1/2 mb-2 " + BaseVisibilityClasses, - TooltipDirection.Down => $"absolute z-[9999] top-full left-1/2 -translate-x-1/2 mt-2 " + BaseVisibilityClasses, - TooltipDirection.Left => $"absolute z-[9999] right-full top-1/2 -translate-y-1/2 mr-2 " + BaseVisibilityClasses, - TooltipDirection.Right => $"absolute z-[9999] left-full top-1/2 -translate-y-1/2 ml-2 " + BaseVisibilityClasses, - _ => $"absolute z-[9999] bottom-full left-1/2 -translate-x-1/2 mb-2 " + BaseVisibilityClasses + TooltipDirection.Down => "down", + TooltipDirection.Left => "left", + TooltipDirection.Right => "right", + _ => "up" }; - private string ArrowClasses => Direction switch + private async Task ShowTooltip() { - TooltipDirection.Up => "absolute left-1/2 -translate-x-1/2 -bottom-1 w-2 h-2 bg-surface-alt border-r border-b border-line rotate-45", - TooltipDirection.Down => "absolute left-1/2 -translate-x-1/2 -top-1 w-2 h-2 bg-surface-alt border-l border-t border-line rotate-45", - TooltipDirection.Left => "absolute top-1/2 -translate-y-1/2 -right-1 w-2 h-2 bg-surface-alt border-t border-r border-line rotate-45", - TooltipDirection.Right => "absolute top-1/2 -translate-y-1/2 -left-1 w-2 h-2 bg-surface-alt border-b border-l border-line rotate-45", - _ => "absolute left-1/2 -translate-x-1/2 -bottom-1 w-2 h-2 bg-surface-alt border-r border-b border-line rotate-45" - }; + if (string.IsNullOrEmpty(Text)) return; + try + { + await JS.InvokeVoidAsync("tooltipFixed.show", _triggerRef, Text, DirectionString); + } + catch + { + // Component may have been disposed + } + } - private string BaseVisibilityClasses => - "opacity-0 invisible group-hover/tooltip:opacity-100 group-hover/tooltip:visible " + - (_showMobile ? "!opacity-100 !visible " : "") + - "transition-all duration-200 " + - (_showMobile ? "" : "pointer-events-none"); + private async Task HideTooltip() + { + try + { + await JS.InvokeVoidAsync("tooltipFixed.hide"); + } + catch + { + // Component may have been disposed + } + } - private void ToggleTooltip() + private async Task ToggleTooltip() { - // Only toggle on touch devices (mobile) - // Clicks on tooltip content are prevented from reaching here via @onclick:stopPropagation _showMobile = !_showMobile; + if (_showMobile) + { + await ShowTooltip(); + } + else + { + await HideTooltip(); + } } - } diff --git a/WebfrontCore/Components/UI/Layout/NavMenu.razor b/WebfrontCore/Components/UI/Layout/NavMenu.razor index cfed690ab..3c834b163 100644 --- a/WebfrontCore/Components/UI/Layout/NavMenu.razor +++ b/WebfrontCore/Components/UI/Layout/NavMenu.razor @@ -97,7 +97,7 @@ { - + @pageLink.Name } diff --git a/WebfrontCore/Components/UI/Navigation/SideContextMenu.razor b/WebfrontCore/Components/UI/Navigation/SideContextMenu.razor index 91f135868..5aa0d3772 100644 --- a/WebfrontCore/Components/UI/Navigation/SideContextMenu.razor +++ b/WebfrontCore/Components/UI/Navigation/SideContextMenu.razor @@ -67,6 +67,11 @@ @foreach (var item in Model.Items.Where(item => !item.IsCollapse)) { + if (item.IsSectionHeader) + { +
@item.Title
+ continue; + } var isLong = item.Title.Length > 30; // Threshold for marquee +
@(AppState.Loc($"GAME_{group.Key}")) @@ -137,6 +142,11 @@ @foreach (var item in Model.Items.Where(item => !item.IsCollapse)) { + if (item.IsSectionHeader) + { +
@item.Title
+ continue; + }
+
@(AppState.Loc($"GAME_{group.Key}")) diff --git a/WebfrontCore/Controllers/API/Models/TopStatsRequest.cs b/WebfrontCore/Controllers/API/Models/TopStatsRequest.cs index 5570e6a82..46a50ae22 100644 --- a/WebfrontCore/Controllers/API/Models/TopStatsRequest.cs +++ b/WebfrontCore/Controllers/API/Models/TopStatsRequest.cs @@ -5,4 +5,20 @@ namespace WebfrontCore.Controllers.API.Models; public class TopStatsRequest : PaginationRequest { public string? ServerId { get; set; } + + /// + /// Performance-bucket code filter. Stored lower-cased — the DB column + /// (EFPerformanceBucket.Code) holds the canonical lower-cased form + /// (the writer in IW4MServer normalises on insert), so any user- + /// supplied value that retains the original casing (e.g. "Zombies" + /// from IW4MAdminSettings.json) would otherwise return zero rows + /// when compared with case-sensitive equality. Empty/whitespace stays null + /// so callers can distinguish "no filter" from the default bucket. + /// Mirrors SharedLibraryCore.Helpers.PerformanceBucketCodes.Normalize. + /// + public string? PerformanceBucketCode + { + get; + init => field = string.IsNullOrWhiteSpace(value) ? null : value.ToLowerInvariant(); + } } diff --git a/WebfrontCore/Controllers/API/Models/TopStatsResponse.cs b/WebfrontCore/Controllers/API/Models/TopStatsResponse.cs index a3569eebd..28932a6b4 100644 --- a/WebfrontCore/Controllers/API/Models/TopStatsResponse.cs +++ b/WebfrontCore/Controllers/API/Models/TopStatsResponse.cs @@ -6,4 +6,13 @@ public class TopStatsResponse { public List Players { get; set; } = []; public long TotalRankedClients { get; set; } + + /// + /// Pass this value as the next request's offset to walk the leaderboard + /// without re-visiting rejected rows. Server-side ranking history is filtered + /// against the stats join, so a single page may consume more underlying rows + /// than it returns; this advances by the actual rows consumed. Equal to + /// request.Offset + Players.Count only when no rows were filtered. + /// + public int NextOffset { get; set; } } diff --git a/WebfrontCore/Controllers/API/StatsController.cs b/WebfrontCore/Controllers/API/StatsController.cs index 1858f0117..181b9cc35 100644 --- a/WebfrontCore/Controllers/API/StatsController.cs +++ b/WebfrontCore/Controllers/API/StatsController.cs @@ -60,13 +60,14 @@ public async Task GetAdvancedStats(int clientId, [FromQuery] stri [HttpGet("top")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetTopPlayers([FromQuery] int count = 25, [FromQuery] int offset = 0, - [FromQuery] string? serverId = null) + [FromQuery] string? serverId = null, [FromQuery] string? performanceBucketCode = null) { var response = await dataService.GetTopStatsAsync(new Models.TopStatsRequest { Count = count, Offset = offset, - ServerId = serverId + ServerId = serverId, + PerformanceBucketCode = performanceBucketCode }); return Ok(response); } diff --git a/WebfrontCore/Controllers/API/ZombieStatsController.cs b/WebfrontCore/Controllers/API/ZombieStatsController.cs new file mode 100644 index 000000000..1c9975ee3 --- /dev/null +++ b/WebfrontCore/Controllers/API/ZombieStatsController.cs @@ -0,0 +1,193 @@ +using System.Net.Mime; +using Data.Models; +using Microsoft.AspNetCore.Mvc; +using SharedLibraryCore.Interfaces; + +namespace WebfrontCore.Controllers.API; + +/// +/// Zombie match data — leaderboards, map records, and per-player match history. +/// All endpoints require the Zombie Stats Premium plugin; without it every route returns 404. +/// +[ApiController] +[Route("api/zombie")] +[Tags("Zombie Stats")] +[Produces(MediaTypeNames.Application.Json)] +public class ZombieStatsController( + ILogger logger, + IServiceProvider serviceProvider) : ControllerBase +{ + private readonly IZombieLeaderboardService? _leaderboardService = + serviceProvider.GetService(); + + private readonly IZombieMatchHistoryService? _matchHistoryService = + serviceProvider.GetService(); + + private readonly IZombieLiveMatchService? _liveMatchService = + serviceProvider.GetService(); + + /// + /// Returns the games, maps, and player counts that can be used to filter leaderboard entries. + /// Use the values returned here to populate the game, mapId, and playerCount + /// query parameters on GET /api/zombie/leaderboard. + /// + /// Metadata returned. + /// Zombie Stats Premium plugin is not installed. + [HttpGet("leaderboard/metadata")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetLeaderboardMetadata() + { + if (_leaderboardService is null) + { + return NotFound(); + } + + var metadata = await _leaderboardService.GetLeaderboardMetadataAsync(); + return Ok(metadata); + } + + /// + /// Returns ranked entries for a game/map/player-count, sorted by highest round descending. + /// Page size is capped at 100. + /// + /// Game code (e.g. T4, T5, T6). + /// Map identifier, from /api/zombie/leaderboard/metadata. + /// Player count filter, from metadata. + /// Pagination offset. Defaults to 0. + /// Page size. Defaults to 25, capped at 100. + /// Leaderboard page returned. + /// Zombie Stats Premium plugin is not installed. + [HttpGet("leaderboard")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetLeaderboardEntries( + [FromQuery] Reference.Game game, + [FromQuery] int mapId, + [FromQuery] int playerCount, + [FromQuery] int offset = 0, + [FromQuery] int count = 25) + { + if (_leaderboardService is null) + { + return NotFound(); + } + + count = Math.Min(count, 100); + var response = await _leaderboardService.GetLeaderboardEntriesAsync(game, mapId, playerCount, offset, count); + return Ok(response); + } + + /// + /// Returns notable records for a map across all player counts (highest round, most kills, + /// best economy, etc.). + /// + /// Game code (e.g. T4, T5, T6). + /// Map identifier, from /api/zombie/leaderboard/metadata. + /// Records returned (may be empty). + /// Zombie Stats Premium plugin is not installed. + [HttpGet("leaderboard/records")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetMapRecords( + [FromQuery] Reference.Game game, + [FromQuery] int mapId) + { + if (_leaderboardService is null) + { + return NotFound(); + } + + var records = await _leaderboardService.GetMapRecordsAsync(game, mapId); + return Ok(records); + } + + /// + /// Returns all players' stats, per-round breakdowns, and the full event timeline for one match. + /// + /// Match identifier, from a leaderboard or match-history entry. + /// Match detail returned. + /// Match not found, or Zombie Stats Premium is not installed. + [HttpGet("match/{matchId:int}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetMatchDetail(int matchId) + { + if (_matchHistoryService is null) + { + return NotFound(); + } + + try + { + var detail = await _matchHistoryService.GetMatchDetailAsync(matchId); + if (detail is null) return NotFound(); + + // Completed matches are immutable — let CDNs and browsers cache them. + // In-progress matches stay uncached so the share page reflects current state. + if (detail.Completed) + { + Response.Headers.CacheControl = "public, max-age=300, immutable"; + } + + return Ok(detail); + } + catch (Exception e) + { + logger.LogWarning(e, "Could not get match detail for match {MatchId}", matchId); + return NotFound(); + } + } + + /// + /// Returns a player's recent zombie matches with round breakdowns and event timelines. + /// Page size is capped at 50. + /// + /// Player's IW4MAdmin client ID. + /// Optional server endpoint (ip:port) to filter by. + /// Pagination offset. Defaults to 0. + /// Page size. Defaults to 10, capped at 50. + /// Match history returned (may be empty). + /// Zombie Stats Premium plugin is not installed. + [HttpGet("client/{clientId:int}/history")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPlayerMatchHistory( + int clientId, + [FromQuery] string? serverEndpoint = null, + [FromQuery] int offset = 0, + [FromQuery] int count = 10) + { + if (_matchHistoryService is null) + { + return NotFound(); + } + + count = Math.Min(count, 50); + var history = await _matchHistoryService.GetPlayerMatchHistoryAsync(clientId, serverEndpoint, offset, count); + return Ok(history); + } + + /// + /// Returns a live snapshot of an in-progress zombie match for a server: per-player + /// current/cumulative stats, recent events, and rounds completed so far. Returns 404 + /// when the server has no active match. Designed for periodic polling (recommended + /// interval: 5s, matching the standard scoreboard). + /// + /// Server identifier (typically ip:port). + /// Live snapshot returned. + /// Server has no active match, or Zombie Stats Premium is not installed. + [HttpGet("server/{serverId}/live-match")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetLiveMatchSnapshot(string serverId) + { + if (_liveMatchService is null) + { + return NotFound(); + } + + var snapshot = await _liveMatchService.GetLiveMatchSnapshotAsync(serverId); + return snapshot is null ? NotFound() : Ok(snapshot); + } +} diff --git a/WebfrontCore/Core/OpenApi/ServerUrlTransformer.cs b/WebfrontCore/Core/OpenApi/ServerUrlTransformer.cs new file mode 100644 index 000000000..bd2614d6f --- /dev/null +++ b/WebfrontCore/Core/OpenApi/ServerUrlTransformer.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace WebfrontCore.Core.OpenApi; + +/// +/// Forces the OpenAPI servers entry to a relative URL. The default entry built +/// from the request scheme/host hard-codes http:// when Kestrel sits behind a +/// TLS-terminating proxy (Cloudflare/nginx), and Scalar's "Test Request" runner then +/// triggers mixed-content blocks on an HTTPS page. A relative URL is resolved by the +/// browser against the page origin, so it always matches the scheme the page loaded +/// over — same mechanism Blazor's NavigationManager relies on for relative hrefs. +/// +internal sealed class ServerUrlTransformer : IOpenApiDocumentTransformer +{ + public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + document.Servers = [new OpenApiServer { Url = "/" }]; + return Task.CompletedTask; + } +} diff --git a/WebfrontCore/Core/Services/IWebfrontDataService.cs b/WebfrontCore/Core/Services/IWebfrontDataService.cs index a2bc0ec47..2d3d762dc 100644 --- a/WebfrontCore/Core/Services/IWebfrontDataService.cs +++ b/WebfrontCore/Core/Services/IWebfrontDataService.cs @@ -34,7 +34,7 @@ public interface IWebfrontDataService Task SaveConfigurationFileAsync(string fileName, string content); Task>> GetPrivilegedClientsAsync(); Task GetTopStatsAsync(TopStatsRequest request); - Task GetClientStatisticsAsync(int clientId, string? serverId = null); + Task GetClientStatisticsAsync(int clientId, string? serverId = null, string? performanceBucketCode = null); Task> GetClientStatsAsync(int clientId); Task> GetAlertsAsync(); Task DismissAlertAsync(Guid alertId); diff --git a/WebfrontCore/Core/Services/WebfrontDataService.cs b/WebfrontCore/Core/Services/WebfrontDataService.cs index fc6464cd1..c3dc31405 100644 --- a/WebfrontCore/Core/Services/WebfrontDataService.cs +++ b/WebfrontCore/Core/Services/WebfrontDataService.cs @@ -15,6 +15,7 @@ using PenaltyInfo = SharedLibraryCore.Dtos.PenaltyInfo; using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats.Helpers; +using IW4MAdmin.Plugins.Stats.Web.Dtos; using Microsoft.EntityFrameworkCore; using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Interfaces; @@ -185,8 +186,11 @@ await _serverDataViewer.ClientHistoryAsync( server.ResolvedIpEndPoint.Address.IsInternal() ? _manager.ExternalIPAddress : server.ListenAddress, server.ListenPort), + IsZombieServer = server.IsZombieServer(), + ZombieRoundNumber = server.ZombieRoundNumber, RconRoundTripMs = server.LatencyMetrics?.RconRoundTripMs, - GameLogPipelineMs = server.LatencyMetrics?.GameLogPipelineMs + GameLogIngestMs = server.LatencyMetrics?.GameLogIngestMs, + PerformanceBucket = server.PerformanceCode }).ToList(); } @@ -253,8 +257,10 @@ await _serverDataViewer.ClientHistoryAsync( ConnectProtocolUrl = server.EventParser.URLProtocolFormat.FormatExt( server.ResolvedIpEndPoint.Address.IsInternal() ? _manager.ExternalIPAddress : server.ListenAddress, server.ListenPort), + IsZombieServer = server.IsZombieServer(), + ZombieRoundNumber = server.ZombieRoundNumber, RconRoundTripMs = server.LatencyMetrics?.RconRoundTripMs, - GameLogPipelineMs = server.LatencyMetrics?.GameLogPipelineMs + GameLogIngestMs = server.LatencyMetrics?.GameLogIngestMs }; } @@ -887,25 +893,37 @@ public async Task GetTopStatsAsync(TopStatsRequest request) var server = _manager.GetServers().FirstOrDefault(s => s.Id == request.ServerId) as IGameServer; var legacyId = server?.LegacyDatabaseId; - var stats = _statsConfig.EnableAdvancedMetrics - ? await _statManager.GetNewTopStats(request.Offset, request.Count, legacyId) - : await _statManager.GetTopStats(request.Offset, request.Count, legacyId); + List stats; + int rowsConsumed; + if (_statsConfig.EnableAdvancedMetrics) + { + (stats, rowsConsumed) = await _statManager.GetNewTopStats( + request.Offset, request.Count, legacyId, request.PerformanceBucketCode); + } + else + { + // Legacy path doesn't filter past the ranking query, so consumed == returned. + stats = await _statManager.GetTopStats(request.Offset, request.Count, legacyId); + rowsConsumed = stats.Count; + } - var totalRanked = await _serverDataViewer.RankedClientsCountAsync(legacyId); + var totalRanked = await _serverDataViewer.RankedClientsCountAsync(legacyId, request.PerformanceBucketCode); return new TopStatsResponse { Players = stats, - TotalRankedClients = totalRanked + TotalRankedClients = totalRanked, + NextOffset = request.Offset + rowsConsumed }; } - public async Task GetClientStatisticsAsync(int clientId, string? serverId = null) + public async Task GetClientStatisticsAsync(int clientId, string? serverId = null, string? performanceBucketCode = null) { var hitInfo = (await _advancedStatsHelper.QueryResource(new StatsInfoRequest { ClientId = clientId, - ServerEndpoint = serverId + ServerEndpoint = serverId, + PerformanceBucketCode = performanceBucketCode }))?.Results?.First(); if (hitInfo is null) @@ -922,6 +940,20 @@ public async Task GetTopStatsAsync(TopStatsRequest request) var matchedServerId = server?.LegacyDatabaseId; hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId); + + // Invoke custom stats metrics (e.g. zombie stats) for advanced view + var customMeta = new Dictionary> + { + { clientId, new List() } + }; + + foreach (var customMetricFunc in _manager.CustomStatsMetrics) + { + await customMetricFunc(customMeta, matchedServerId, hitInfo.PerformanceBucket, false); + } + + hitInfo.CustomMetrics = customMeta[clientId]; + return hitInfo; } diff --git a/WebfrontCore/Program.cs b/WebfrontCore/Program.cs index ab7e3fc20..47c80a703 100644 --- a/WebfrontCore/Program.cs +++ b/WebfrontCore/Program.cs @@ -234,6 +234,7 @@ private static void ConfigureServices(IServiceCollection services) services.AddOpenApi(options => { + options.AddDocumentTransformer(); options.AddDocumentTransformer(); }); @@ -255,6 +256,22 @@ private static void ConfigureMiddleware(WebApplication app) var appConfig = app.Services.GetRequiredService(); var manager = app.Services.GetRequiredService(); + // Honour X-Forwarded-* from Cloudflare / nginx / reverse proxies so Request.Scheme + // and Request.Host reflect the public URL the browser used. Without this, Scalar + // and any absolute URL generation default to the Kestrel HTTP bind address and + // trigger mixed-content blocking when the site is fronted by TLS termination. + var forwardedOptions = new Microsoft.AspNetCore.Builder.ForwardedHeadersOptions + { + ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto + | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedHost + | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor + }; + // Accept headers from any upstream — the reverse proxy is expected to be the + // only ingress. Users who expose Kestrel directly won't send these headers. + forwardedOptions.KnownNetworks.Clear(); + forwardedOptions.KnownProxies.Clear(); + app.UseForwardedHeaders(forwardedOptions); + if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/WebfrontCore/wwwroot/css/src/app.css b/WebfrontCore/wwwroot/css/src/app.css index 87d1396d2..4fde3bc3e 100644 --- a/WebfrontCore/wwwroot/css/src/app.css +++ b/WebfrontCore/wwwroot/css/src/app.css @@ -242,3 +242,293 @@ a { .marquee-enabled:focus-within .marquee-text { animation: marquee var(--marquee-duration, 5s) linear infinite alternate; } + +/* ============================================ + ZOMBIE EE BADGE STRIP + ============================================ */ +/* Default: per-quest expanded chips visible, aggregate hidden. JS adds + .zm-badge-collapsed when the strip would wrap (see zombieBadgeStrip in + blazor_lib.js); CSS then swaps which group is visible. */ +.zm-badge-strip [data-ee-aggregate] { display: none; } +.zm-badge-strip.zm-badge-collapsed [data-ee-expanded] { display: none; } +.zm-badge-strip.zm-badge-collapsed [data-ee-aggregate] { display: inline-flex; } + +/* ============================================ + ZOMBIE MATCH SCRUBBER (DOM) + ============================================ + Replaces the Konva canvas implementation. Zoom is a single CSS variable + (--zoom on .zsr-host); track width grows via calc() and the browser + compositor handles GPU-layer transforms for the percent-positioned dots. + No canvas raster, no per-frame redraw cycle. */ + +.zsr-host { + --zoom: 1; + --lane-row: 44px; + --lane-row-pad: 4px; + --tickband-h: 32px; + --top-pad: 24px; +} + +.zsr-shell { + display: grid; + grid-template-columns: 96px 1fr; + align-items: stretch; +} +.zsr-shell--single { grid-template-columns: 1fr; } + +/* ── Lane name column (left, doesn't scroll) ─────────────────────────── */ +.zsr-side { + position: relative; + /* Match .zsr-track: top-pad, plus tickband height when one is present. + :has() handles the conditional purely in CSS. */ + padding-top: var(--top-pad); +} +.zsr-host:has(.zsr-tickband) .zsr-side { padding-top: calc(var(--top-pad) + var(--tickband-h)); } +.zsr-side-cell { + height: var(--lane-row); + display: flex; + align-items: center; + padding: 0 8px; + font: bold 11px ui-sans-serif, system-ui, sans-serif; + color: rgb(203 213 225); /* slate-300 */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Scroll area (horizontally scrolls when zoomed) ──────────────────── */ +.zsr-scroll { + position: relative; + overflow-x: auto; + overflow-y: hidden; + min-width: 0; + scrollbar-width: thin; +} + +/* ── Track (the actually-zoomed surface) ─────────────────────────────── */ +.zsr-track { + position: relative; + width: calc(100% * var(--zoom)); + height: var(--track-height, 120px); + /* contain layout/paint to keep zoom-induced reflow scoped — child + repositioning won't invalidate the page's layout above this point. */ + contain: layout style paint; +} + +/* ── Round-band stripes ──────────────────────────────────────────────── */ +.zsr-bands { + position: absolute; + inset: var(--top-pad) 0 0 0; + pointer-events: none; +} +.zsr-band { + position: absolute; + top: 0; + bottom: 0; +} +.zsr-band--even { background: rgba(255, 255, 255, 0.02); } +.zsr-band--odd { background: rgba(255, 255, 255, 0.05); } +.zsr-band-label { + position: absolute; + top: -20px; + left: 4px; + font: 10px ui-monospace, monospace; + color: rgb(100 116 139); /* slate-500 */ +} + +/* ── Tickband (top, match-level events) ──────────────────────────────── */ +.zsr-tickband { + position: absolute; + top: var(--top-pad); + left: 0; + right: 0; + height: var(--tickband-h); + background: rgba(251, 191, 36, 0.04); + border-radius: 4px; +} +.zsr-tickband-watermark { + /* Pins to the left edge of the viewport during horizontal scroll. The prior + canvas implementation centred this watermark — replicating that with + pure CSS inside an absolutely-positioned ancestor is awkward; left-pin + conveys the same "this is the EE band" semantic without scroll-sync JS. */ + position: sticky; + left: 8px; + display: inline-flex; + align-items: center; + height: 100%; + font: bold 12px ui-sans-serif, system-ui, sans-serif; + color: rgba(251, 191, 36, 0.5); + letter-spacing: 0.18em; + text-transform: uppercase; + pointer-events: none; + z-index: 5; +} + +/* ── Lane rows ───────────────────────────────────────────────────────── */ +.zsr-lanes { + position: absolute; + left: 0; + right: 0; + top: var(--top-pad); +} +/* When a tickband is rendered, push lanes down by tickband-h. :has handles + the conditional purely in CSS; no JS layout calc needed. */ +.zsr-host:has(.zsr-tickband) .zsr-lanes { top: calc(var(--top-pad) + var(--tickband-h)); } +.zsr-lane { + position: relative; + height: var(--lane-row); + border-bottom: 1px solid rgba(148, 163, 184, 0.10); +} + +/* ── Gaps (red striped areas between rounds where the player was absent) ─ */ +.zsr-gap { + position: absolute; + top: 4px; + bottom: 4px; + background: rgba(248, 113, 113, 0.18); + border: 1px dashed rgba(248, 113, 113, 0.7); + border-radius: 2px; + pointer-events: auto; +} +.zsr-gap--compact { + background: rgba(248, 113, 113, 0.35); + border: none; +} + +/* ── Event dots ──────────────────────────────────────────────────────── */ +/* Plain solid fill + dark border. No drop-shadow filter (that promoted every + dot to its own compositor layer — fine for 50 dots, expensive for the + 1500-6000 dots a typical match renders). No hover transform (would also + promote a layer). Hover feedback is brightness instead. */ +.zsr-dot { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + z-index: var(--dot-z, 10); + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--dot-fill, #94a3b8); + border: 2px solid #0f172a; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} +.zsr-dot:hover { filter: brightness(1.25); } +.zsr-dot:focus-visible { outline: 2px solid #22d3ee; outline-offset: 2px; } +.zsr-dot-icon { + font-size: 10px; + color: #0f172a; + line-height: 1; +} +.zsr-dot--match { /* match-level: positioned in tickband, smaller */ + width: 12px; + height: 12px; + border-width: 1.5px; +} +.zsr-dot--match .zsr-dot-icon { font-size: 8px; } + +/* ── Tick (round markers, no glyph) ──────────────────────────────────── */ +.zsr-tick { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 18px; + background: var(--tick-fill, #94a3b8); + opacity: 0.6; + border-radius: 1px; + z-index: var(--dot-z, 60); + pointer-events: auto; +} + +/* Match-level dots sit in the tickband, vertically centred there. */ +.zsr-tickband .zsr-dot--match { + top: 50%; +} + +/* ── Filter (CSS-only via root data-filter attribute) ────────────────── */ +/* Default: all visible. */ +.zsr-host[data-filter="critical"] .zsr-dot:not([data-cat="critical"]):not([data-cat="danger"]):not([data-cat="round"]):not([data-cat="easter-egg"]):not([data-cat="easter-egg-step"]), +.zsr-host[data-filter="critical"] .zsr-tick:not([data-cat="round"]) { + opacity: 0.08; + pointer-events: none; +} +.zsr-host[data-filter="powerups"] .zsr-dot:not([data-cat="powerup"]):not([data-cat="round"]):not([data-cat="easter-egg"]):not([data-cat="easter-egg-step"]), +.zsr-host[data-filter="powerups"] .zsr-tick:not([data-cat="round"]) { + opacity: 0.08; + pointer-events: none; +} +.zsr-host[data-filter="economy"] .zsr-dot:not([data-cat="weapon"]):not([data-cat="weapon-abandon"]):not([data-cat="box"]):not([data-cat="box-pass"]):not([data-cat="door"]):not([data-cat="trap"]):not([data-cat="build"]):not([data-cat="perk"]):not([data-cat="round"]):not([data-cat="easter-egg"]):not([data-cat="easter-egg-step"]), +.zsr-host[data-filter="economy"] .zsr-tick:not([data-cat="round"]) { + opacity: 0.08; + pointer-events: none; +} + +/* JS-applied dim class (kept for parity with FILTER_RULES; the data-filter + selectors above handle the common case but a class flip is the fallback). */ +.zsr-dim { opacity: 0.08 !important; pointer-events: none; } + +/* ── Focus dim (deep-link to a specific player) ──────────────────────── */ +/* JS mirrors the host's data-focus-client value into a .zsr-focus-self class + on the matching lane — CSS has no attr-comparison so we can't do it purely + declaratively. Match-level (tickband) events live outside .zsr-lane so the + rule below leaves them at full opacity automatically. */ +.zsr-host[data-focus-client] .zsr-lane .zsr-dot { opacity: 0.25; } +.zsr-host[data-focus-client] .zsr-lane.zsr-focus-self .zsr-dot { opacity: 1; } + +/* ── Scrub cursor ────────────────────────────────────────────────────── */ +.zsr-scrub { + --scrub-pct: 0%; + position: absolute; + top: 0; + bottom: 0; + left: var(--scrub-pct); + pointer-events: none; + z-index: 90; +} +.zsr-scrub-line { + position: absolute; + top: 8px; + bottom: 4px; + left: -1px; + width: 2px; + background: repeating-linear-gradient( + to bottom, + #22d3ee 0 4px, + transparent 4px 7px + ); +} +.zsr-scrub-handle { + position: absolute; + top: 0; + left: -7px; + width: 14px; + height: 14px; + background: #22d3ee; + border: none; + border-radius: 2px; + cursor: ew-resize; + pointer-events: auto; + padding: 0; +} +.zsr-scrub-handle:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } + +/* ── Scrollbar styling (subtle, matches existing app aesthetic) ──────── */ +.zsr-scroll::-webkit-scrollbar { height: 8px; } +.zsr-scroll::-webkit-scrollbar-track { background: rgba(15, 23, 42, 0.3); } +.zsr-scroll::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.3); + border-radius: 4px; +} +.zsr-scroll::-webkit-scrollbar-thumb:hover { background: rgba(148, 163, 184, 0.5); } + +/* ── Server-card connect link: icon swap on Ctrl/Meta hold ──────────── */ +/* Driven by `mod-key-down` body class, set by the global key listener in + blazor_lib.js. Hidden copy-icon swaps in for the play-icon while held; + click handler in ServerCard intercepts modifier-click for clipboard copy. */ +.server-play-link .play-copy-icon { display: none; } +body.mod-key-down .server-play-link .play-icon { display: none; } +body.mod-key-down .server-play-link .play-copy-icon { display: inline-block; } diff --git a/WebfrontCore/wwwroot/js/blazor_lib.js b/WebfrontCore/wwwroot/js/blazor_lib.js index ddd11af28..ebf9ef690 100644 --- a/WebfrontCore/wwwroot/js/blazor_lib.js +++ b/WebfrontCore/wwwroot/js/blazor_lib.js @@ -1,3 +1,92 @@ +// ============================================ +// UTC -> Local Time Renderer +// ============================================ +// Server-rendered Blazor cannot know the browser's timezone, so dates served +// from C# always go out as UTC. This helper post-processes elements bearing +// `data-utc-time=""` and replaces their text with the user's +// local-formatted equivalent, moving the original UTC string into the title +// attribute so a hover always reveals the canonical UTC value. +// +// Attributes: +// data-utc-time ISO 8601 UTC timestamp (required) +// data-utc-fmt 'time' (HH:mm:ss) | 'datetime' (full) — default 'datetime' +// +// Helper only swaps the inner text — UTC-on-hover is the caller's +// responsibility (use the Tooltip component, don't set title here, since the +// app's standard hover affordance is the custom Tooltip and we shouldn't have +// a title shadowing it). +// +// A MutationObserver picks up elements added by future Blazor renders, so +// per-render hand-wiring isn't required. +window.utcLocalTime = (function () { + function format(date, fmt) { + if (fmt === 'time') return date.toLocaleTimeString(); + return date.toLocaleString(); + } + + function convert(el) { + if (!el || !el.getAttribute) return; + const iso = el.getAttribute('data-utc-time'); + if (!iso) return; + if (el.getAttribute('data-utc-converted') === '1') return; + const date = new Date(iso); + if (isNaN(date.getTime())) return; + const fmt = el.getAttribute('data-utc-fmt') || 'datetime'; + el.textContent = format(date, fmt); + el.setAttribute('data-utc-converted', '1'); + } + + function applyAll(root) { + const scope = root || document; + const els = scope.querySelectorAll('[data-utc-time]:not([data-utc-converted="1"])'); + for (let i = 0; i < els.length; i++) convert(els[i]); + } + + function start() { + applyAll(); + if (typeof MutationObserver === 'undefined') return; + const obs = new MutationObserver(function (muts) { + for (let i = 0; i < muts.length; i++) { + const m = muts[i]; + if (m.type === 'attributes') { + if (m.target && m.target.getAttribute('data-utc-time')) { + // Re-convert if the timestamp changed under us (Blazor + // re-render with a new value). + m.target.removeAttribute('data-utc-converted'); + convert(m.target); + } + continue; + } + if (m.addedNodes) { + for (let j = 0; j < m.addedNodes.length; j++) { + const n = m.addedNodes[j]; + if (n.nodeType !== 1) continue; + if (n.matches && n.matches('[data-utc-time]')) convert(n); + if (n.querySelectorAll) { + const inner = n.querySelectorAll('[data-utc-time]:not([data-utc-converted="1"])'); + for (let k = 0; k < inner.length; k++) convert(inner[k]); + } + } + } + } + }); + obs.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-utc-time'] + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start); + } else { + start(); + } + + return { applyAll: applyAll, convert: convert }; +})(); + // ============================================ // Visibility Observer for Component Virtualization // ============================================ @@ -24,6 +113,233 @@ window.visibilityObserver = { } }; +// ============================================ +// Fixed Tooltip Positioning +// ============================================ +// ============================================ +// Zombie EE Badge Strip — overflow-aware collapse +// ============================================ +// Per-quest EE chips render expanded by default. When the strip can't fit them +// without wrapping (titlebar narrowed by viewport, by long player names, by +// extra trophy chips, etc.), we collapse to a single "EE X/Y" aggregate chip +// that opens the modal on click. CSS-only can't detect "would wrap"; we measure +// after layout and toggle a class. ResizeObserver watches both the strip and +// its parent (parent width changes don't always re-fire on the strip itself). +window.zombieBadgeStrip = { + _observers: new Map(), + + setup(elementId) { + const el = document.getElementById(elementId); + if (!el) return; + // Avoid duplicate setup if Blazor re-invokes after a soft re-render. + if (this._observers.has(elementId)) return; + + const measure = () => { + // Optimistically un-collapse so we can measure the natural width of + // the expanded chips. Without this, a strip that previously collapsed + // would stay collapsed forever (the aggregate chip is narrower than + // the expanded set, so scrollWidth never exceeds clientWidth). + el.classList.remove('zm-badge-collapsed'); + // rAF so layout settles before we measure. + requestAnimationFrame(() => { + if (!el.isConnected) return; + // Two overflow tests because scrollWidth alone misses cases where + // flex-wrap kicked in and laid items out on a second row (no + // horizontal overflow but vertical wrap occurred). + const horizontalOverflow = el.scrollWidth > el.clientWidth + 1; + let wrapped = false; + if (!horizontalOverflow) { + // Compare offsetTop across direct children of the expanded + // group. If any sits below the first, flex wrapped them. + const expanded = el.querySelector('[data-ee-expanded]'); + if (expanded) { + const kids = Array.from(expanded.children); + if (kids.length > 1) { + const baseTop = kids[0].offsetTop; + wrapped = kids.some(k => k.offsetTop > baseTop + 1); + } + } + } + if (horizontalOverflow || wrapped) { + el.classList.add('zm-badge-collapsed'); + } + }); + }; + + measure(); + const obs = new ResizeObserver(measure); + obs.observe(el); + if (el.parentElement) obs.observe(el.parentElement); + this._observers.set(elementId, obs); + }, + + teardown(elementId) { + const obs = this._observers.get(elementId); + if (obs) { + obs.disconnect(); + this._observers.delete(elementId); + } + } +}; + +window.tooltipFixed = { + _el: null, + _currentTrigger: null, + _watchdog: null, + _globalsBound: false, + // Coordinates of the trigger's bounding rect at show time. Used by the global + // mousemove watchdog to detect when the cursor leaves the trigger area without + // a mouseleave event having fired (which happens when Blazor re-renders the + // trigger element out from under the cursor — the new DOM node never receives + // the in-flight mouseleave so without this safety net the tooltip orbits + // forever, snapping to whatever tooltip-wrapper the cursor next enters). + _triggerRect: null, + + _getEl: function () { + if (!this._el) { + this._el = document.getElementById('fixed-tooltip'); + if (!this._el) { + this._el = document.createElement('div'); + this._el.id = 'fixed-tooltip'; + this._el.className = 'fixed z-[9999] pointer-events-none opacity-0 transition-opacity duration-150'; + document.body.appendChild(this._el); + } + } + return this._el; + }, + + // Lazily wire the global safety nets — only once per page load. We attach to + // window scroll (capture phase, so nested scroll containers also fire) plus a + // throttled mousemove. Both call `_evictIfStale` which hides the tooltip when: + // 1. the trigger element is no longer in the DOM (Blazor swap), OR + // 2. the cursor has left the trigger's last-known bounding box. + // Either condition means the tooltip is "orphaned" and should disappear. + _bindGlobals: function () { + if (this._globalsBound) return; + this._globalsBound = true; + + // Capture-phase scroll listener catches any scrolling ancestor — nested + // scrollable cards, the document, modal backdrops, anything. The trigger + // moves on the page during scroll so its rect is stale; just hide. + window.addEventListener('scroll', () => { + if (this._currentTrigger) this.hide(); + }, true); + + // Cheap mousemove guard — only does work if a tooltip is currently shown. + // We check the trigger's rect against cursor position, NOT element-from- + // point, because the trigger may be obscured by inner content (an icon + // child swallowing the hit) and elementFromPoint would lie about it. + window.addEventListener('mousemove', (e) => { + if (!this._currentTrigger || !this._triggerRect) return; + const r = this._triggerRect; + // 4px slack handles sub-pixel rendering and tiny mouse tracking gaps + // that would otherwise flicker the tooltip on the trigger boundary. + if (e.clientX < r.left - 4 || e.clientX > r.right + 4 || + e.clientY < r.top - 4 || e.clientY > r.bottom + 4) { + this.hide(); + } + }, { passive: true }); + }, + + // Periodic isConnected check — catches the case where the trigger element is + // removed from the DOM while the cursor is stationary (no mousemove to fire + // the bounds check). Fires every ~200ms while a tooltip is visible. + _startWatchdog: function () { + this._stopWatchdog(); + this._watchdog = setInterval(() => { + if (!this._currentTrigger) { this._stopWatchdog(); return; } + if (!this._currentTrigger.isConnected) this.hide(); + }, 200); + }, + + _stopWatchdog: function () { + if (this._watchdog) { + clearInterval(this._watchdog); + this._watchdog = null; + } + }, + + show: function (triggerElement, text, direction) { + this._bindGlobals(); + const el = this._getEl(); + const rect = triggerElement.getBoundingClientRect(); + + // Render content + el.innerHTML = + '
' + + this._escapeHtml(text) + + '
' + + '
'; + + el.style.opacity = '0'; + el.style.display = 'block'; + + // Measure tooltip size after rendering content + const tipRect = el.getBoundingClientRect(); + let top, left; + + switch (direction || 'up') { + case 'down': + top = rect.bottom + 8; + left = rect.left + rect.width / 2 - tipRect.width / 2; + break; + case 'left': + top = rect.top + rect.height / 2 - tipRect.height / 2; + left = rect.left - tipRect.width - 8; + break; + case 'right': + top = rect.top + rect.height / 2 - tipRect.height / 2; + left = rect.right + 8; + break; + default: // up + top = rect.top - tipRect.height - 8; + left = rect.left + rect.width / 2 - tipRect.width / 2; + break; + } + + // Clamp to viewport + left = Math.max(4, Math.min(left, window.innerWidth - tipRect.width - 4)); + top = Math.max(4, top); + + el.style.left = left + 'px'; + el.style.top = top + 'px'; + el.style.opacity = '1'; + + // Track the active trigger + its rect for the global guards. + this._currentTrigger = triggerElement; + this._triggerRect = rect; + this._startWatchdog(); + }, + + hide: function () { + const el = this._getEl(); + el.style.opacity = '0'; + this._currentTrigger = null; + this._triggerRect = null; + this._stopWatchdog(); + }, + + _escapeHtml: function (text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + _arrowClass: function (direction) { + const base = 'absolute w-2 h-2 bg-surface-alt border-line rotate-45 '; + switch (direction || 'up') { + case 'down': + return base + 'left-1/2 -translate-x-1/2 -top-1 border-l border-t'; + case 'left': + return base + 'top-1/2 -translate-y-1/2 -right-1 border-t border-r'; + case 'right': + return base + 'top-1/2 -translate-y-1/2 -left-1 border-b border-l'; + default: + return base + 'left-1/2 -translate-x-1/2 -bottom-1 border-r border-b'; + } + } +}; + // ============================================ // Global Navigation Loading Bar // ============================================ @@ -829,3 +1145,39 @@ window.setupDynamicActionHandlers = function (dotNetRef) { document.addEventListener('click', handler, true); console.log('[DynamicAction] Handler attached'); }; + +// ============================================ +// Modifier-key state tracking (Ctrl / Cmd) +// ============================================ +// Toggles `mod-key-down` on while either modifier is held. Used to +// swap the play-icon for a copy-icon on server-card connect links so the +// "Ctrl+click to copy connect command" affordance is discoverable. +// Window blur clears the class so a tab-out doesn't leave it stuck. +(function () { + const setModState = (down) => { + document.body.classList.toggle('mod-key-down', down); + }; + document.addEventListener('keydown', e => { + if (e.key === 'Control' || e.key === 'Meta') setModState(true); + }); + document.addEventListener('keyup', e => { + if (e.key === 'Control' || e.key === 'Meta') setModState(false); + }); + window.addEventListener('blur', () => setModState(false)); +})(); + +window.copyToClipboard = async function (text) { + if (!text) return false; + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.error('[copyToClipboard] Clipboard write failed:', err); + return false; + } +}; + +window.openProtocolUrl = function (url) { + if (!url) return; + window.location.href = url; +}; diff --git a/WebfrontCore/wwwroot/js/infinite-scroll.js b/WebfrontCore/wwwroot/js/infinite-scroll.js index 65f431da9..0bf41a87b 100644 --- a/WebfrontCore/wwwroot/js/infinite-scroll.js +++ b/WebfrontCore/wwwroot/js/infinite-scroll.js @@ -1,36 +1,47 @@ window.infiniteScroll = { - observer: null, + observers: {}, - initialize: function (dotNetHelper, elementId) { + // Initializes an IntersectionObserver against the given element. + // methodName defaults to "LoadMore" for backward compatibility. + // Multiple observers may exist concurrently (keyed by elementId). + initialize: function (dotNetHelper, elementId, methodName) { const element = document.getElementById(elementId); if (!element) return; - // Cleanup existing observer if any - if (this.observer) { - this.disconnect(); - } + // Replace any existing observer for this element (e.g., re-init after reset). + this.disconnect(elementId); const options = { root: null, - rootMargin: '100px', // Preload before reaching bottom - threshold: 0.1 + rootMargin: '200px', // Preload before reaching bottom. + threshold: 0 }; - this.observer = new IntersectionObserver(async (entries) => { + const callbackName = methodName || 'LoadMore'; + const observer = new IntersectionObserver(async (entries) => { for (const entry of entries) { if (entry.isIntersecting) { - await dotNetHelper.invokeMethodAsync('LoadMore'); + await dotNetHelper.invokeMethodAsync(callbackName); } } }, options); - this.observer.observe(element); + observer.observe(element); + this.observers[elementId] = observer; }, - disconnect: function () { - if (this.observer) { - this.observer.disconnect(); - this.observer = null; + // Single-arg form disconnects a specific observer; no-arg form disconnects + // all (legacy callers that initialized with the singleton pattern). + disconnect: function (elementId) { + if (elementId) { + const obs = this.observers[elementId]; + if (obs) { + obs.disconnect(); + delete this.observers[elementId]; + } + return; } + Object.values(this.observers).forEach(o => o.disconnect()); + this.observers = {}; } }; diff --git a/WebfrontCore/wwwroot/js/stats.js b/WebfrontCore/wwwroot/js/stats.js index 1570c31bc..2c1f30da2 100644 --- a/WebfrontCore/wwwroot/js/stats.js +++ b/WebfrontCore/wwwroot/js/stats.js @@ -27,7 +27,6 @@ function getStatsChart(id, rankingText, data) { // Get theme colors from shared utility const theme = window.chartTheme.getChartColors(); - const tooltipConfig = window.chartTheme.getTooltipConfig(); const labels = []; const values = []; @@ -68,10 +67,51 @@ function getStatsChart(id, rankingText, data) { maintainAspectRatio: false, legend: false, tooltips: { - ...tooltipConfig, - callbacks: { - label: context => moment.utc(context.label).local().calendar(), - title: items => Math.round(items[0].yLabel) + ' ' + rankingText + enabled: false, + custom: function (tooltipModel) { + let tooltipEl = document.getElementById('chartjs-stats-tooltip'); + + const styles = getComputedStyle(document.documentElement); + const surfaceColor = styles.getPropertyValue('--color-surface-alt').trim() || styles.getPropertyValue('--color-surface').trim() || 'hsl(0 0% 13%)'; + const lineColor = styles.getPropertyValue('--color-line').trim() || 'hsl(0 0% 25%)'; + const foregroundColor = styles.getPropertyValue('--color-foreground').trim() || 'hsl(0 0% 98%)'; + const subtleColor = styles.getPropertyValue('--color-subtle').trim() || 'hsl(0 0% 75%)'; + + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.id = 'chartjs-stats-tooltip'; + tooltipEl.style.position = 'absolute'; + tooltipEl.style.borderRadius = '6px'; + tooltipEl.style.pointerEvents = 'none'; + tooltipEl.style.zIndex = '9999'; + tooltipEl.style.transition = 'all .1s ease'; + tooltipEl.style.transform = 'translate(-50%, 0)'; + tooltipEl.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)'; + document.body.appendChild(tooltipEl); + } + + tooltipEl.style.background = surfaceColor; + tooltipEl.style.border = '1px solid ' + lineColor; + + if (tooltipModel.opacity === 0) { + tooltipEl.style.opacity = '0'; + return; + } + + if (tooltipModel.body) { + const value = Math.round(tooltipModel.dataPoints[0].yLabel); + const dateStr = moment.utc(tooltipModel.dataPoints[0].label).local().calendar(); + tooltipEl.innerHTML = + '
' + + '
' + value + ' ' + rankingText + '
' + + '
' + dateStr + '
' + + '
'; + } + + const position = this._chart.canvas.getBoundingClientRect(); + tooltipEl.style.opacity = '1'; + tooltipEl.style.left = position.left + window.scrollX + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + window.scrollY + tooltipModel.caretY - tooltipEl.clientHeight - 10 + 'px'; } }, hover: { diff --git a/WebfrontCore/wwwroot/js/zombie-scrubber.js b/WebfrontCore/wwwroot/js/zombie-scrubber.js new file mode 100644 index 000000000..a180606bd --- /dev/null +++ b/WebfrontCore/wwwroot/js/zombie-scrubber.js @@ -0,0 +1,666 @@ +// zombie-scrubber.js +// DOM-based multi-lane match timeline scrubber. Replaces the prior Konva canvas +// implementation — see git history for the canvas version. Owned by +// ZombieMatchScrubber.razor; state (filter, zoom, scrub time) lives in C# via +// JSInterop. This module renders + handles interaction. +// +// Why DOM over canvas: zoom in the canvas implementation triggered a full +// destroy+rebuild of ~6000 Konva nodes per rAF tick plus a stage-wide bitmap +// re-cache, producing 180-200ms frames (5 fps) on iGPU/Mac/VM hardware. DOM +// rendering offloads zoom to the browser compositor — `--zoom` CSS variable +// updates a parent width via calc(); GPU layer transforms reposition the +// absolutely-positioned children with zero JavaScript on the hot path. +// +// Public API (window.zombieScrubber) — preserved verbatim from the canvas +// implementation so the Razor caller is unchanged: +// init(elementId, payload, dotnetRef, focusClientId?, initialLaneMode?) +// dispose(elementId) +// setFilter(elementId, filter) +// setZoom(elementId, level) +// setLaneMode(elementId, mode) 'qualified' | 'all' +// setScrubTime(elementId, seconds) +// focusClient(elementId, clientId | null) + +(function () { + 'use strict'; + + const instances = new Map(); + + // Visual config — preserved verbatim from the prior CATEGORY_VISUALS table. + // Maps an event category to a Phosphor glyph class + tailwind colour token. + // The fill column drives both the dot background and the dot's drop-shadow + // colour (CSS filter, GPU-composited — far cheaper than canvas shadowBlur). + const CATEGORY_VISUALS = { + 'powerup': { fill: '#facc15', icon: 'ph-lightning', z: 30, tick: false }, + 'danger': { fill: '#f97316', icon: 'ph-warning', z: 40, tick: false }, + 'critical': { fill: '#ef4444', icon: 'ph-skull', z: 50, tick: false }, + 'success': { fill: '#22c55e', icon: 'ph-heartbeat', z: 30, tick: false }, + 'perk': { fill: '#a855f7', icon: 'ph-pill', z: 25, tick: false }, + 'weapon': { fill: '#3b82f6', icon: 'ph-knife', z: 20, tick: false }, + 'weapon-abandon': { fill: '#fb923c', icon: 'ph-knife', z: 20, tick: false }, + 'box': { fill: '#60a5fa', icon: 'ph-cube', z: 20, tick: false }, + 'box-pass': { fill: '#fb923c', icon: 'ph-cube', z: 20, tick: false }, + 'box-teddy': { fill: '#f472b6', icon: 'ph-spiral', z: 25, tick: false }, + 'door': { fill: '#f59e0b', icon: 'ph-door-open', z: 15, tick: false }, + 'trap': { fill: '#f87171', icon: 'ph-lightning', z: 25, tick: false }, + 'build': { fill: '#10b981', icon: 'ph-wrench', z: 20, tick: false }, + 'session-join': { fill: '#94a3b8', icon: 'ph-sign-in', z: 15, tick: false }, + 'session-leave': { fill: '#64748b', icon: 'ph-sign-out', z: 15, tick: false }, + 'round': { fill: '#94a3b8', icon: '', z: 60, tick: true }, + 'easter-egg': { fill: '#fbbf24', icon: 'ph-trophy', z: 55, tick: false }, + 'easter-egg-step': { fill: '#f59e0b', icon: 'ph-trophy', z: 50, tick: false }, + 'power-on': { fill: '#facc15', icon: 'ph-lightning', z: 56, tick: false }, + 'power-off': { fill: '#94a3b8', icon: 'ph-lightning-slash', z: 56, tick: false }, + 'bank-deposit': { fill: '#22c55e', icon: 'ph-piggy-bank', z: 20, tick: false }, + 'bank-withdraw': { fill: '#fbbf24', icon: 'ph-hand-coins', z: 20, tick: false }, + 'locker-store': { fill: '#60a5fa', icon: 'ph-lockers', z: 20, tick: false }, + 'locker-retrieve': { fill: '#34d399', icon: 'ph-lockers', z: 20, tick: false }, + 'gum-activate': { fill: '#ec4899', icon: 'ph-sparkle', z: 30, tick: false }, + 'gum-take': { fill: '#a855f7', icon: 'ph-gift', z: 20, tick: false }, + 'gum-leave': { fill: '#94a3b8', icon: 'ph-heart-break', z: 20, tick: false }, + 'default': { fill: '#94a3b8', icon: '', z: 10, tick: true } + }; + + // Filter rules — same set + semantics as the prior implementation. Dots that + // fail the active rule get a `.zsr-dim` class (pure-CSS opacity drop), so + // toggling filters is a single class flip on each dot, not a re-render. + const FILTER_RULES = { + 'all': () => true, + 'critical': c => c === 'danger' || c === 'critical' || c === 'round' || c === 'easter-egg' || c === 'easter-egg-step' || c === 'power-on' || c === 'power-off', + 'powerups': c => c === 'powerup' || c === 'round' || c === 'easter-egg' || c === 'easter-egg-step' || c === 'power-on' || c === 'power-off', + 'economy': c => ['weapon','weapon-abandon','box','box-pass','door','trap','build','perk','round','easter-egg','easter-egg-step','power-on','power-off'].includes(c) + }; + + const SCRUB_DEBOUNCE_MS = 50; + const ZOOM_DEBOUNCE_MS = 150; + // Zoom range matches the toolbar +/- nominal cap. No canvas-cap clamp needed + // (no canvas), but keep the same bound so the toolbar UX is identical. + const ZOOM_MIN = 1; + const ZOOM_MAX = 20; + + class ScrubberInstance { + constructor(elementId, payload, dotnetRef, focusClientId, initialLaneMode) { + this.elementId = elementId; + this.payload = payload; + this.dotnetRef = dotnetRef; + this.focusClientId = focusClientId ?? null; + this.filter = 'all'; + // Lane-mode resolution priority — same rule as the prior implementation: + // 1. Razor-supplied `initialLaneMode` (authoritative when consumer + // has resolved it, e.g. dedicated match page = 'all', leaderboard + // card = 'qualified'). + // 2. Default to 'qualified' when any lane qualifies, else 'all' + // (legacy pre-qualifier match — empty stage otherwise). + if (initialLaneMode === 'qualified' || initialLaneMode === 'all') { + this.laneMode = initialLaneMode; + } else { + const anyQualified = (payload.lanes || []).some(l => l.isQualified); + this.laneMode = anyQualified ? 'qualified' : 'all'; + } + this.zoom = 1; + this.scrubSeconds = payload.minSeconds; + this._scrubDebounce = null; + this._zoomDebounce = null; + this._scrubDragging = false; + + // Cached span for percent math; never zero so % calcs don't NaN. + this.span = Math.max(payload.maxSeconds - payload.minSeconds, 1); + + this._build(); + + // Initial scrub time → first event of any visible lane (parity with + // the prior implementation's behaviour). Falls back to minSeconds. + const firstSec = this._firstEventSeconds(); + if (firstSec != null) this.setScrubTime(firstSec); + } + + _firstEventSeconds() { + for (const lane of this._visibleLanes()) { + if (lane.events && lane.events.length > 0) return lane.events[0].seconds; + } + return null; + } + + _visibleLanes() { + if (this.laneMode === 'all') return this.payload.lanes; + return this.payload.lanes.filter(l => l.isQualified); + } + + _container() { + return document.getElementById(this.elementId); + } + + _hasMatchLevelEvents() { + return (this.payload.matchLevelEvents || []).length > 0; + } + + _pct(seconds) { + // Percentage of the time axis the given second falls at. + return ((seconds - this.payload.minSeconds) / this.span) * 100; + } + + _stripColors(s) { + return (s || '').replace(/\^[0-9]/g, ''); + } + + // ── DOM construction ─────────────────────────────────────────────────── + // We build the full subtree once via DocumentFragment and replace the host + // contents in a single mutation. Subsequent zoom / filter / focus / scrub + // operations are CSS-variable or class-toggle updates — no rebuilds. + _build() { + const host = this._container(); + if (!host) return; + + // Reset host: drop any prior render (e.g. lane-mode rebuild) and seed + // baseline state. We own the inline style. + host.innerHTML = ''; + host.classList.add('zsr-host'); + host.style.setProperty('--zoom', String(this.zoom)); + host.dataset.filter = this.filter; + if (this.focusClientId != null) { + host.dataset.focusClient = String(this.focusClientId); + } else { + delete host.dataset.focusClient; + } + + const visibleLanes = this._visibleLanes(); + const showLaneLabels = visibleLanes.length > 1; + + const root = document.createElement('div'); + root.className = 'zsr-shell' + (showLaneLabels ? '' : ' zsr-shell--single'); + + // ── Lane name column (left, doesn't scroll) ── + // Position: outside the scroll-area entirely, in a sibling grid column. + // No sticky tricks, no scroll-sync handlers. + if (showLaneLabels) { + const side = document.createElement('div'); + side.className = 'zsr-side'; + visibleLanes.forEach((lane, idx) => { + const cell = document.createElement('div'); + cell.className = 'zsr-side-cell'; + cell.style.setProperty('--lane-idx', String(idx)); + cell.title = this._stripColors(lane.name); + cell.textContent = this._stripColors(lane.name); + side.appendChild(cell); + }); + root.appendChild(side); + } + + // ── Scroll area (right column, horizontally scrolls when zoomed) ── + const scrollArea = document.createElement('div'); + scrollArea.className = 'zsr-scroll'; + this._scrollEl = scrollArea; + + // ── Track (the actually-zoomed surface) ── + // Width = 100% × var(--zoom). All children use percent-based positioning + // so the browser repositions everything via GPU compositor on zoom. + const track = document.createElement('div'); + track.className = 'zsr-track'; + const trackHeight = this._trackHeight(visibleLanes.length); + track.style.setProperty('--track-height', trackHeight + 'px'); + track.style.setProperty('--lane-count', String(visibleLanes.length)); + this._trackEl = track; + + // ── Round-band stripes (full-height, alternating tint) ── + // Rendered first so they sit behind everything else in the track. + const bands = document.createElement('div'); + bands.className = 'zsr-bands'; + (this.payload.roundBands || []).forEach((band, i) => { + const left = this._pct(band.startSeconds); + const width = Math.max(this._pct(band.endSeconds) - left, 0.05); + const el = document.createElement('div'); + el.className = 'zsr-band ' + (i % 2 === 0 ? 'zsr-band--even' : 'zsr-band--odd'); + el.style.left = left + '%'; + el.style.width = width + '%'; + if (width > 1.5) { + const lbl = document.createElement('span'); + lbl.className = 'zsr-band-label'; + lbl.textContent = 'R' + band.roundNumber; + el.appendChild(lbl); + } + bands.appendChild(el); + }); + track.appendChild(bands); + + // ── Match-level event tickband (top, EE quest markers) ── + if (this._hasMatchLevelEvents()) { + const tickband = document.createElement('div'); + tickband.className = 'zsr-tickband'; + const watermark = document.createElement('div'); + watermark.className = 'zsr-tickband-watermark'; + watermark.textContent = 'Easter Eggs'; + tickband.appendChild(watermark); + this.payload.matchLevelEvents.forEach(evt => { + tickband.appendChild(this._buildDot(evt, /*isMatchLevel=*/true, null)); + }); + track.appendChild(tickband); + } + + // ── Lanes (one row per visible player) ── + const lanesEl = document.createElement('div'); + lanesEl.className = 'zsr-lanes'; + visibleLanes.forEach((lane, idx) => { + const laneEl = document.createElement('div'); + laneEl.className = 'zsr-lane'; + laneEl.style.setProperty('--lane-idx', String(idx)); + laneEl.dataset.client = String(lane.clientId); + + // Gaps render behind events — single absolute-positioned rect each. + (lane.gaps || []).forEach(gap => { + const gx = this._pct(gap.start); + const gw = Math.max(this._pct(gap.end) - gx, 0.05); + const g = document.createElement('div'); + g.className = 'zsr-gap' + (gap.compact ? ' zsr-gap--compact' : ''); + if (gap.compact) { + // Compact gaps render as a thin vertical strip at the gap + // midpoint — visual hint, not a span. Width clamped tight. + const midPct = (gx + this._pct(gap.end)) / 2; + const compactW = Math.max(Math.min(gw * 0.15, 0.6), 0.18); + g.style.left = (midPct - compactW / 2) + '%'; + g.style.width = compactW + '%'; + } else { + g.style.left = gx + '%'; + g.style.width = gw + '%'; + } + g.dataset.tooltip = gap.tooltip || ''; + laneEl.appendChild(g); + }); + + // Events. Round-completion markers (category 'round') are skipped + // here — the R{n} round-band labels at the top of the track + // convey the same information visually, so the per-lane round + // ticks were redundant clutter. Round events still ship in the + // payload because they drive RoundBand derivation server-side + // and the first-event-as-initial-scrub-time defaulting. + (lane.events || []).forEach(evt => { + if ((evt.category || 'default') === 'round') return; + laneEl.appendChild(this._buildDot(evt, /*isMatchLevel=*/false, lane)); + }); + + lanesEl.appendChild(laneEl); + }); + track.appendChild(lanesEl); + + // ── Scrub cursor (vertical line + draggable handle) ── + // Positioned via --scrub-pct CSS var so cursor moves are a single + // var write — no layout, no JS per pixel. + const scrub = document.createElement('div'); + scrub.className = 'zsr-scrub'; + scrub.style.setProperty('--scrub-pct', this._pct(this.scrubSeconds) + '%'); + const scrubLine = document.createElement('div'); + scrubLine.className = 'zsr-scrub-line'; + const scrubHandle = document.createElement('button'); + scrubHandle.className = 'zsr-scrub-handle'; + scrubHandle.type = 'button'; + scrubHandle.setAttribute('aria-label', 'Scrub'); + scrub.appendChild(scrubLine); + scrub.appendChild(scrubHandle); + track.appendChild(scrub); + this._scrubEl = scrub; + this._scrubHandleEl = scrubHandle; + + scrollArea.appendChild(track); + root.appendChild(scrollArea); + host.appendChild(root); + + this._wireHandlers(); + this._applyFilter(); + this._applyFocus(); + } + + _trackHeight(laneCount) { + const TOP_PAD = 24; + const TICKBAND_HEIGHT = this._hasMatchLevelEvents() ? 32 : 0; + const LANE_ROW = 44; // matches CSS lane-row height + const BOTTOM_PAD = 12; + return Math.max(120, TOP_PAD + TICKBAND_HEIGHT + laneCount * LANE_ROW + BOTTOM_PAD); + } + + _buildDot(evt, isMatchLevel, lane) { + const cat = evt.category || 'default'; + const v = CATEGORY_VISUALS[cat] || CATEGORY_VISUALS.default; + const left = this._pct(evt.seconds); + + if (v.tick) { + // Round markers + unknown categories render as a thin vertical + // tick — no glyph, no shadow. Cheaper to paint at scale. + const el = document.createElement('div'); + el.className = 'zsr-tick'; + el.style.left = left + '%'; + el.style.setProperty('--tick-fill', v.fill); + el.dataset.cat = cat; + el.dataset.tooltip = evt.time + ' • ' + evt.label; + if (lane) el.dataset.client = String(lane.clientId); + if (isMatchLevel) el.dataset.matchLevel = 'true'; + return el; + } + + // Event dot — button so it's keyboard-focusable + accessible by default. + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'zsr-dot' + (isMatchLevel ? ' zsr-dot--match' : ''); + btn.style.left = left + '%'; + btn.style.setProperty('--dot-fill', v.fill); + btn.style.setProperty('--dot-z', String(v.z)); + btn.dataset.cat = cat; + btn.dataset.seconds = String(evt.seconds); + btn.dataset.tooltip = evt.time + ' • ' + evt.label; + if (lane) btn.dataset.client = String(lane.clientId); + if (isMatchLevel) btn.dataset.matchLevel = 'true'; + btn.setAttribute('aria-label', evt.label); + + if (v.icon) { + const ico = document.createElement('i'); + ico.className = 'ph ' + v.icon + ' zsr-dot-icon'; + btn.appendChild(ico); + } + return btn; + } + + // ── Interaction wiring ──────────────────────────────────────────────── + _wireHandlers() { + const track = this._trackEl; + const scroll = this._scrollEl; + const handle = this._scrubHandleEl; + + // Wheel-zoom (cursor-anchored). Updates `--zoom` CSS variable on the + // host element; browser compositor handles the layer transform. Same + // anchor math as the prior canvas implementation. + this._wheelHandler = (e) => { + e.preventDefault(); + const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15; + const newZoom = Math.min(Math.max(this.zoom * factor, ZOOM_MIN), ZOOM_MAX); + if (Math.abs(newZoom - this.zoom) < 0.01) return; + + const trackRect = track.getBoundingClientRect(); + // Position of cursor in current track-coordinate space. + const stageX = e.clientX - trackRect.left + scroll.scrollLeft; + const ratio = newZoom / this.zoom; + + this.zoom = newZoom; + this._container().style.setProperty('--zoom', String(this.zoom)); + + // Reposition scroll so the time at the cursor stays under the cursor. + const newStageX = stageX * ratio; + const viewportX = e.clientX - trackRect.left; + scroll.scrollLeft = Math.max(0, newStageX - viewportX); + + this._notifyZoom(); + }; + scroll.addEventListener('wheel', this._wheelHandler, { passive: false }); + + // Scrub-cursor drag — pointer events for unified mouse/touch. + this._scrubPointerDown = (e) => { + e.preventDefault(); + this._scrubDragging = true; + handle.setPointerCapture && handle.setPointerCapture(e.pointerId); + this._updateScrubFromClientX(e.clientX); + }; + this._scrubPointerMove = (e) => { + if (!this._scrubDragging) return; + this._updateScrubFromClientX(e.clientX); + }; + this._scrubPointerUp = (e) => { + if (!this._scrubDragging) return; + this._scrubDragging = false; + handle.releasePointerCapture && handle.releasePointerCapture(e.pointerId); + }; + handle.addEventListener('pointerdown', this._scrubPointerDown); + handle.addEventListener('pointermove', this._scrubPointerMove); + handle.addEventListener('pointerup', this._scrubPointerUp); + handle.addEventListener('pointercancel', this._scrubPointerUp); + + // Click-on-track to jump scrub cursor (excluding clicks on event dots — + // those have their own handler that ALSO sets the scrub time, but + // emits a clientId so the side panel can pin to that player's event). + this._trackClick = (e) => { + const dot = e.target.closest('.zsr-dot'); + if (dot) { + const seconds = Number(dot.dataset.seconds); + const clientId = dot.dataset.client ? Number(dot.dataset.client) : 0; + if (this.dotnetRef) { + this.dotnetRef.invokeMethodAsync('OnEventClicked', clientId, seconds); + } + this.setScrubTime(seconds); + return; + } + // Click on bare track moves the cursor. + if (e.target.closest('.zsr-scrub') || e.target.closest('.zsr-side')) return; + this._updateScrubFromClientX(e.clientX); + }; + track.addEventListener('click', this._trackClick); + + // Tooltip — single delegated handler reusing the existing + // window.tooltipFixed shared element. Replaces the prior + // _showTooltip/_hideTooltip/_removeTooltipMouseMove machinery. + // _lastHoverEl gates re-shows: mouseover fires on every parent-> + // child transition (e.g. dot -> inner icon), and re-calling show() + // each time causes a perceptible tooltip flicker. We only fire on + // a genuine target change. + this._lastHoverEl = null; + this._mouseOver = (e) => { + const el = e.target.closest('[data-tooltip]'); + if (!el || !el.dataset.tooltip) return; + if (el === this._lastHoverEl) return; + this._lastHoverEl = el; + if (window.tooltipFixed && window.tooltipFixed.show) { + window.tooltipFixed.show(el, el.dataset.tooltip, 'up'); + } + }; + this._mouseOut = (e) => { + // mouseout bubbles; only act when leaving the element entirely + // (relatedTarget outside the same tooltip-bearing ancestor). + const fromEl = e.target.closest('[data-tooltip]'); + const toEl = e.relatedTarget && e.relatedTarget.closest + ? e.relatedTarget.closest('[data-tooltip]') + : null; + if (fromEl && fromEl !== toEl) { + this._lastHoverEl = null; + if (window.tooltipFixed && window.tooltipFixed.hide) { + window.tooltipFixed.hide(); + } + } + }; + track.addEventListener('mouseover', this._mouseOver); + track.addEventListener('mouseout', this._mouseOut); + } + + _updateScrubFromClientX(clientX) { + const trackRect = this._trackEl.getBoundingClientRect(); + const x = clientX - trackRect.left; + const pct = Math.min(Math.max(x / trackRect.width, 0), 1); + const seconds = this.payload.minSeconds + pct * this.span; + this.setScrubTime(seconds); + } + + _notifyScrub() { + if (this._scrubDebounce) clearTimeout(this._scrubDebounce); + this._scrubDebounce = setTimeout(() => { + if (this.dotnetRef) { + this.dotnetRef.invokeMethodAsync('OnScrubChanged', this.scrubSeconds, this._computeHalfWindowSeconds()); + } + }, SCRUB_DEBOUNCE_MS); + } + + // Half-width (in seconds) of the side-panel hit window around the scrub + // cursor. Sized so the visual dot footprint matches the hit window: at + // any zoom level, brushing the cursor across a dot's pixel width should + // surface that dot in the panel. Without this, the fixed ±5s window + // demanded near-perfect cursor-on-centre alignment at 1× zoom (where + // 5 seconds was ~2 px while a dot is 16 px wide) and was overly lenient + // at high zoom. + _computeHalfWindowSeconds() { + if (!this._scrollEl) return 5; + const trackPx = this._scrollEl.clientWidth * this.zoom; + if (trackPx <= 0) return 5; + const pxPerSec = trackPx / this.span; + // 8 px = half a regular dot, +4 px slack so edge-of-dot hovers + // still register cleanly. Floor at 0.5s so very-high-zoom doesn't + // collapse the window to nothing on tickband (smaller) dots. + const slopPx = 12; + return Math.max(0.5, slopPx / pxPerSec); + } + + // Debounce zoom-changed Blazor roundtrips — wheel-zoom fires many times + // during a fast spin, but the consumer only cares about the settled + // level (toolbar +/- display). + _notifyZoom() { + if (this._zoomDebounce) clearTimeout(this._zoomDebounce); + this._zoomDebounce = setTimeout(() => { + if (this.dotnetRef) { + this.dotnetRef.invokeMethodAsync('OnZoomChanged', this.zoom); + } + }, ZOOM_DEBOUNCE_MS); + } + + _applyFilter() { + const rule = FILTER_RULES[this.filter] || FILTER_RULES.all; + // Walk dots once; toggle .zsr-dim. Cheap (~6000 class flips, runs + // only on filter change, not per-frame). + const dots = this._trackEl.querySelectorAll('.zsr-dot, .zsr-tick'); + dots.forEach(d => { + if (d.dataset.matchLevel === 'true') { + // Match-level events stay full opacity regardless of filter + // unless the filter explicitly excludes their category. + } + const cat = d.dataset.cat || 'default'; + if (rule(cat)) d.classList.remove('zsr-dim'); + else d.classList.add('zsr-dim'); + }); + } + + _applyFocus() { + // CSS handles the dim via [data-focus-client] selector on host, but + // can't compare the host's data-focus-client attr value to each + // lane's data-client attr value (no attr-comparison in CSS). So we + // mirror the focused-lane state into a `.zsr-focus-self` class which + // the CSS can target directly. ~6 class flips per focus change — + // negligible. + const host = this._container(); + const lanes = this._trackEl ? this._trackEl.querySelectorAll('.zsr-lane') : []; + if (this.focusClientId == null) { + delete host.dataset.focusClient; + lanes.forEach(l => l.classList.remove('zsr-focus-self')); + } else { + host.dataset.focusClient = String(this.focusClientId); + const target = String(this.focusClientId); + lanes.forEach(l => { + if (l.dataset.client === target) l.classList.add('zsr-focus-self'); + else l.classList.remove('zsr-focus-self'); + }); + } + } + + // ── public API ───────────────────────────────────────────────────────── + + setFilter(filter) { + if (this.filter === filter) return; + this.filter = filter; + this._container().dataset.filter = filter; + this._applyFilter(); + } + + setZoom(level) { + const clamped = Math.min(Math.max(level, ZOOM_MIN), ZOOM_MAX); + if (Math.abs(clamped - this.zoom) < 0.01) return; + + // Anchor at viewport-centre so toolbar +/- doesn't lose the user's + // position. Same math as wheel handler but uses centre as anchor. + const trackRect = this._trackEl.getBoundingClientRect(); + const scroll = this._scrollEl; + const containerRect = scroll.getBoundingClientRect(); + const viewportX = containerRect.width / 2; + const stageX = viewportX - trackRect.left + containerRect.left + scroll.scrollLeft; + + const ratio = clamped / this.zoom; + this.zoom = clamped; + this._container().style.setProperty('--zoom', String(this.zoom)); + const newStageX = stageX * ratio; + scroll.scrollLeft = Math.max(0, newStageX - viewportX); + } + + setScrubTime(seconds) { + this.scrubSeconds = Math.min(Math.max(seconds, this.payload.minSeconds), this.payload.maxSeconds); + const pct = this._pct(this.scrubSeconds); + if (this._scrubEl) this._scrubEl.style.setProperty('--scrub-pct', pct + '%'); + this._notifyScrub(); + } + + focusClient(clientId) { + this.focusClientId = clientId; + this._applyFocus(); + } + + setLaneMode(mode) { + if (mode !== 'qualified' && mode !== 'all') return; + if (this.laneMode === mode) return; + this.laneMode = mode; + // Lane-mode change = different lane set = full subtree rebuild. Still + // cheap (single docFragment swap, no canvas raster). Preserves zoom + + // filter + focus + scrub time. + const prevZoom = this.zoom; + const prevFilter = this.filter; + const prevScrub = this.scrubSeconds; + this._build(); + this.zoom = prevZoom; + this._container().style.setProperty('--zoom', String(this.zoom)); + this.filter = prevFilter; + this._container().dataset.filter = prevFilter; + this._applyFilter(); + this._applyFocus(); + this.setScrubTime(prevScrub); + } + + dispose() { + if (this._scrollEl && this._wheelHandler) { + this._scrollEl.removeEventListener('wheel', this._wheelHandler); + } + if (this._scrubHandleEl) { + this._scrubHandleEl.removeEventListener('pointerdown', this._scrubPointerDown); + this._scrubHandleEl.removeEventListener('pointermove', this._scrubPointerMove); + this._scrubHandleEl.removeEventListener('pointerup', this._scrubPointerUp); + this._scrubHandleEl.removeEventListener('pointercancel', this._scrubPointerUp); + } + if (this._trackEl) { + this._trackEl.removeEventListener('click', this._trackClick); + this._trackEl.removeEventListener('mouseover', this._mouseOver); + this._trackEl.removeEventListener('mouseout', this._mouseOut); + } + if (this._scrubDebounce) clearTimeout(this._scrubDebounce); + if (this._zoomDebounce) clearTimeout(this._zoomDebounce); + + const host = this._container(); + if (host) { + host.innerHTML = ''; + host.classList.remove('zsr-host'); + host.removeAttribute('data-filter'); + host.removeAttribute('data-focus-client'); + host.style.removeProperty('--zoom'); + } + // Caller-owned dotnetRef; do not dispose here. + this.dotnetRef = null; + } + } + + window.zombieScrubber = { + init(elementId, payload, dotnetRef, focusClientId, initialLaneMode) { + if (instances.has(elementId)) { + instances.get(elementId).dispose(); + instances.delete(elementId); + } + instances.set(elementId, new ScrubberInstance(elementId, payload, dotnetRef, focusClientId, initialLaneMode)); + }, + dispose(elementId) { + const inst = instances.get(elementId); + if (inst) { + inst.dispose(); + instances.delete(elementId); + } + }, + setFilter(elementId, filter) { const i = instances.get(elementId); if (i) i.setFilter(filter); }, + setZoom(elementId, level) { const i = instances.get(elementId); if (i) i.setZoom(level); }, + setLaneMode(elementId, mode) { const i = instances.get(elementId); if (i) i.setLaneMode(mode); }, + setScrubTime(elementId, sec) { const i = instances.get(elementId); if (i) i.setScrubTime(sec); }, + focusClient(elementId, cid) { const i = instances.get(elementId); if (i) i.focusClient(cid); } + }; +})(); diff --git a/entrypoint.sh b/entrypoint.sh index d8bd39583..c6c2dd381 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -70,6 +70,14 @@ if [ ! -f "$CONFIG_DIR/LoggingConfiguration.json" ]; then cp -n /app_defaults/Configuration/* "$CONFIG_DIR/" fi +# DefaultSettings.json ships system defaults (maps, game strings, quick messages, +# etc.) — not user config. Always refresh it from the image so updates land on +# the host-mounted Configuration volume. IW4MAdminSettings.json stays untouched. +if [ -f "/app_defaults/Configuration/DefaultSettings.json" ]; then + echo "Refreshing DefaultSettings.json from image defaults..." + cp -f /app_defaults/Configuration/DefaultSettings.json "$CONFIG_DIR/DefaultSettings.json" +fi + # Sync core plugins — only update if the image version is newer than the mounted version if [ -d "/app_defaults/Plugins" ]; then for ref_file in /app_defaults/Plugins/*; do