Zombie Stats - WIP#400
Open
Ayymoss wants to merge 104 commits into
Open
Conversation
ddc3daf to
f7d74d8
Compare
This commit switches our target framework from .NET 6.0 to .NET 8.0 and replaces Newtonsoft.Json with System.Text.Json for serialization. The JsonConverter classes have been updated to support the new JSON model and some enhancements were applied to the codebase such as fixing a command property and updating various package references. A
The version of the SharedLibraryCore package reference has been updated across multiple projects from '2024.2.4.85' to '2024.2.5.9'. Meanwhile, version within SharedLibraryCore.csproj has been changed from '2024.02.04.085' to '2024.01.01.1'. Changes also include removal of .NET 8 requirement notice and reenabling of status upload to master communicator.
…er` for ranking queries and improve error handling. Updated ZombieStats logic for round state finalization and event validation.
… and advanced stats filtering. - Implement per-category filtering and grouping for stats. - Add bucket selection UI in `StatsOverview` and `AdvancedStats`. - Enhance server and bucket metadata handling across components. - Extend data models and service queries to support bucket codes. - Refactor side context menus with section headers for buckets. - Optimize performance bucket normalization and validation logic.
- Added `IZombieStatsEnhancer` interface for advanced analytics, including skill scoring and rolling averages. - Refactored core logic in `ZombieEventProcessor` to support external skill calculators. - Replaced `CalculateAveragesAndTotals` with `CalculateBasicTotals` and delegated advanced metrics to the enhancer. - Updated `ZombieStats` plugin initialization to dynamically register premium or fallback handlers. - Implemented premium upsell messaging in stat metrics for non-premium servers.
…tats plugin. - Completely delete obsolete `ZombieClientStateManager` and `ZombieEventProcessor` classes. - Streamline plugin to focus on updated core functionality.
…ization support - Introduced `ZombieLeaderboard` Razor component for displaying zombie match leaderboards. - Implemented dynamic filtering by game, map, and player count. - Added server-side data loading and virtualization for efficient rendering of leaderboard entries. - Updated solution file to include the new component and its `.cs` logic file. - Included support for match detail expansion and player statistics rendering.
…iguration logic. - Introduced detailed debug and warning logs for kill events, Z-score calculations, and ranking queries. - Enhanced error handling for key-not-found exceptions and null stats scenarios. - Refactored bucket configuration logic to support fallback and dynamic defaults. - Improved PvE and PvP stats handling by crediting kills and maintaining advanced metrics calculations. - Included Z-score persistence in EFClientStatistics updates for distribution cache synchronization.
…ased dependency injection in several classes. - Eliminated excessive logging in kill events, hit calculations, and ranking updates. - Refactored classes (`ServerDistributionCalculator`, `StatManager`, etc.) to use constructor-based dependency injection shorthand. - Removed unused imports and replaced some direct references with injected configurations or helpers for cleaner design. - Improved code readability and efficient resource management.
…ugins' csproj files.
…citly set `<Private>false>`; update PostBuild script to dynamically detect plugin DLLs based on source `.csproj` files.
…undPlayerCount to track player count at round start.
Badges: aggregate "EE X/Y" chip + per-quest expanded chips render
side-by-side; JS overflow detector (zombieBadgeStrip) collapses to
aggregate-only when the strip would wrap. Click any chip opens a modal
listing all quests with progress bars + completed rounds. Songs render
purple to visually separate music EEs from main quests (yellow/amber);
detected via quest.Id.StartsWith("song_"). Historical matches suppress
partial-progress chips (can't advance, just stale).
Match page: removed the Achievements banner — it conflated EE quests
("Easter Egg Completed on R9" was misleading when only the song quest
finished). Per-quest sections now share one merged container with
horizontal dividers between rows (matches the live-modal layout). Step
cards denser, padding reduced, absolute clock-time replaces relative
"X ago" (snapshot-in-time semantics for historical records). Full UTC
moved to row title tooltip.
Live snapshot: per-quest strips merged into one container with dividers.
Step pills replaced with icon-only ~22px circles so 8-step LLG fits one
line. EE icons added to live timeline category dispatcher (was rendering
empty `class="ph "` for easter-egg-step / easter-egg).
Leaderboard: Open + Copy split-button (one rounded shell, vertical
divider) replaces two stacked buttons. MatchEnded=true passed on
historical consumers.
CSS: zm-badge-strip rules govern the expanded ↔ aggregate swap. Compiled
app.css regenerated separately by the Tailwind pipeline.
…e height tooltipFixed (blazor_lib): three-layer safety net for the singleton tooltip — capture-phase scroll listener (any ancestor scroll → hide), mousemove watchdog comparing cursor against last-shown trigger rect ±4px, periodic isConnected check (200ms). Catches the orphan case where Blazor re-renders the trigger out from under the cursor, mouseleave never fires on the now-gone element, and the tooltip "follows" the cursor by snapping between subsequent tooltip-wrappers. zombieBadgeStrip (blazor_lib): ResizeObserver-driven overflow detector for the EE badge strip. Toggles .zm-badge-collapsed when scrollWidth > clientWidth or any expanded child wraps to row 2 — CSS swaps which group is display:none. Scrubber tooltip: cleanup-then-attach in _showTooltip (was leaking a fresh mousemove listener on every consecutive hover; _mouseMove was overwritten so _hideTooltip only removed the latest, leaving older listeners stacked and repositioning the tooltip on every move). DOM-level mouseleave on the scroll container — reliable hide trigger when Konva's mouseout doesn't fire (lane redraw destroys source shape mid-hover). _hideTooltip() called at the top of both redraw paths (_redrawAll and _redrawLaneAndBandLayers). Scrubber zoom canvas-cap: Chrome silently degrades canvases past ~16384px width (text glyphs vanish, software fallback for compositing). At 20× on a 1200px container we hit 24000px; user reports icons disappearing + severe lag past ~13×. Dynamic cap MAX_SAFE_CANVAS_WIDTH / innerWidth — 800px gets 20×, 1200px gets ~13×, 1600px gets 10×. Toolbar + button still disables only at nominal 20× (cosmetic; effective max isn't wired back to Razor). Full virtualization deferred — see memory reference_konva_canvas_width_cap. Scrubber lane-mode height: _minHeight tracks visible lanes count when _laneMode == "qualified" so the container shrinks alongside the JS-side lane hiding instead of reserving vertical space for unqualified lanes.
Phosphor icons use weight modifiers as a separate class (ph-fill ph-foo), not as a suffix on the name. ph-check-circle-fill silently rendered as nothing. Replace with ph-fill ph-check-circle.
Maps GSC dev names (e.g. usrpg_zm, tesla_gun_zm) to display names so
the GameStringConfiguration lookup hits instead of falling through to
TitleCase ("Usrpg Zm" -> "RPG"). Sourced from each game's english
localize tables cross-referenced with add_zombie_weapon() calls.
T4 +46, T5 +130, T6 +170 entries. Per-game canonical names preserved
where they diverge across titles (e.g. T6 m16_zm = "Colt M16A1" vs
T5 m16_zm = "M16"; T6 tar21_zm = "X95L" vs MP tar21 = "MTAR").
GSC verified: *_zm suffix is the weapon registration; unsuffixed names
like "tesla" are separate powerup registrations - both kept, no
conflict. Per-game-block duplicate-key check passes.
JS sets `container.style.position = 'relative'` once at init so the absolute-positioned DOM overlays (lane names + EE band watermark) parent off the scrubber container. But Blazor re-emits the entire `style` attribute whenever _minHeight changes (e.g. on SHOW_ALL toggle, which flips the visible lane count), wiping that inline rule. With it gone, the overlays re-parent to <body> and the `translateX(scrollLeft)` pin math drifts them right exponentially as zoom grows scrollLeft - lane names eventually scroll past the container's right edge. The bug persisted even after toggling SHOW_ALL back off because nothing re-applied the inline position. Declaring `relative` in the class list anchors it as a CSS rule Blazor never touches, so the inline-style replacement is harmless. Also tightens zoom +/- button padding (p-1 -> px-1) for vertical fit.
…res from `quest.Id.StartsWith` check.
The match-page Buildables card used to list ONLY items that were
built — a 0/4 progress bar told you what was missing in number, not
in name, and the chip list disappeared entirely on no-build runs.
Hard to tell at a glance which items make up the denominator.
Now mirrors the Easter-Egg step grid: every iconic item the map
could register renders as a chip; built items get a filled blue
treatment with a check icon, unbuilt items get a muted shell. Same
3-col grid, same hover-tooltip pattern, same "complete vs partial"
visual hierarchy — just blue (the buildables colour) instead of the
EE amber/purple. Falls back to the old flat built-only list for
unconfigured maps (custom maps, T4/T5) where there's no inventory
template to drive the checklist.
Architecture: premium plugin owns the iconic + display-name configs
and resolves both server-side. New BuildableEntry { Key, DisplayName }
record on ZombieMatchDetail carries the resolved display string
through the API so the webfront has zero premium refs and the old
FormatBuildableName helper goes away — the snake_case → Title Case
formatter that produced labels like "Springpad Zm" / "Sq Common" is
no longer needed because every name now has a real display.
…es into EE shell JS scrubber now accepts initialLaneMode so dedicated page can render with all drop-ins visible on first paint (was silently defaulting to 'qualified' even when Razor resolved 'all'). Pair with new SHOW_ALL toggle in the page header that flips one flag for both the player tab bar and the scrubber lane filter. Buildables row now lives inside the EE container with a divider so the two related signals read as one card instead of two stacked shells.
…mpact card URL no longer pins SelectedClientId on the destination page — that was suppressing the player tab bar, leaving users no way to view a teammate's slice. Letting them pick from tabs reads more naturally. Button restyled to match the dedicated page's name card so the visual language is consistent.
gramophone_crafted only fires when all 6 vinyls have been collected and placed (rare), so most matches that built the gramophone never surfaced it. Watch the gramophone_placed flag instead — fires the moment the device is placed on the stand, regardless of vinyl count. Origins EE depends on the gramophone, so missing detection meant no EE attribution.
Player name moves to top-right inside a compact bordered card with a profile hyperlink so the dedicated match page is a launchpad to broader stats, not a dead-end view. Stats redesigned as a responsive 4-col grid (8 cards) mirroring the leaderboard scoreboard's column set: adds Net Points, KPD, HS%, Damage Ratio. Every card carries a subtitle (derived: underlying values; raw counts: per-round pacing) so all 8 share the same height — empty subtitles caused a visible vertical gap in the previous layout. DTO gains PointsSpent (long) + HeadshotKills (int) to feed Net Points and HS% — same metrics the leaderboard surfaces.
Live g_gametype lags during map change / RCon flake / cold start — clients authing in that window bypassed ZombieStats override hookup, producing 0-stat top-leaderboard entries with inflated standard Skill. Cache one positive detection per server; expose MarkServerAsZombies for callers with definitive zombies-only signals (e.g. our GSC dvars).
Tooltip template uses {{percent}}/{{lastTeamRound}}/{{soloRounds}}
but badge only forwarded soloRounds + sfr — FormatExt left literal
{{percent}} in the rendered string. Wire AssistedRounds through the
3 callsites (history, match-detail, leaderboard scoreboard rows);
legacy rows where AssistedRounds is null degrade to "—%".
Konva canvas rebuilt ~6000 nodes per zoom rAF tick + cached a stage-wide
bitmap; produced 180-200 ms frames (5 fps) on iGPU/Mac/VM hardware per
DevTools Performance trace. Top hot ops: drawImage (cache rasterisation),
shadowBlur (per-shape software blur), Konva setAttr churn, _innerWidth
forced reflow. ~70 % of zoom time was native canvas raster — JS-side
optimisations couldn't move that floor.
Rewrite as DOM: zoom is a single --zoom CSS variable, browser compositor
GPU-transforms the percent-positioned dots. No canvas raster, no per-tick
rebuild, no shadowBlur. Sticky CSS positioning replaces the lane-name and
watermark scroll-sync overlay hacks. Tooltip uses the existing
window.tooltipFixed via a single delegated handler. JSInterop contract
preserved verbatim - consumers (leaderboard, match-detail, share page)
unchanged.
Same change strips the Konva script tag from App.razor, the libman entry,
and the CI artefact copy step. zombie-scrubber.js stays standalone (kept
out of app.min.js for cache isolation).
Refinements bundled:
- No drop-shadow filter on dots - promoted a compositor layer per dot;
hover brightness instead.
- Round-completion markers no longer drawn on lanes or surfaced in the
side panel; the R{n} round-band labels at the top of the track convey
the same information without the clutter.
- Side-panel hit window scales with current zoom + container width so a
cursor brushing any visual dot footprint registers, regardless of zoom.
- Side-panel rows use items-baseline (10 px time column was floating
above 12 px name baseline under items-center).
Server-side timestamps stay UTC (Blazor Server has no native access to the browser's timezone). New utcLocalTime helper in blazor_lib.js picks up [data-utc-time] attributes and swaps the inner text for the user's locale-formatted equivalent on first paint; a MutationObserver covers elements added by later Blazor renders. Non-JS clients still see UTC. Match Time and EE step timestamps on the dedicated match page wrap the local-time span with the existing <Tooltip> component to surface the canonical UTC string on hover. Drops a couple of native title="..." attributes in favour of the app's standard hover affordance. Zombie leaderboard page intentionally untouched - all times there are already in the right contextual format.
Previous sticky-positive IsZombieServer cache (0d6087a) didn't fix HGM's inflated raw cs.Skill: 24h post-deploy 50+ zombie clients still running the standard MP formula, max 471k. Sticky only helps repeat auths — once the override misses at OnClientAuthorized for a session, no later signal re-attaches it, and StatManager.UpdateStats keeps writing std-formula Skill the whole session. Revert the cache (kept the Gametype null-guard separately, that part was independently defensive). Add three one-shot WARN log sites so we can prove the leak hypothesis before shipping a fix: - StatManager.UpdateStats: log "ZombieSkillLeak" when SkillFunction is null and CurrentServer.IsZombieServer() — direct evidence that std formula ran on a zombie server. - ZombieStats.OnClientAuthorized early-return: log "ZombieAuthRace" when GameCode is T4/T5/T6 but IsZombieServer returned false — i.e. Gametype was stale at auth, override won't attach this session. - ZombieStats.OnClientAuthorized success path: log "ZombieSkillFunctionAttached" — without this we can't distinguish "no leaks because no zombie play" from "no leaks because nobody reached the affected codepath". Each site dedups via a per-(client, server) additional-property flag, so volume is bounded by unique connects, not by event count.
EFPerformanceBucket.Code is stored as lower-case canonical. A
capitalised caller (`?performanceBucketCode=Zombies` from a typo
in HGM ops, or any future API consumer) silently filtered to zero
rows on the equality comparisons sprinkled across StatManager,
HitCalculator, AdvancedClientStatsResourceQueryHelper, and the
ranking-prune path — leaderboard would look empty, ranking history
would never prune.
Two layers of defence:
(B) Boundary normalisation — DTO setters in
`WebfrontCore/Controllers/API/Models/TopStatsRequest.cs` and
`Plugins/Stats/Dtos/StatsInfoRequest.cs` lower-case on init
using the C# 13 `field` keyword. Whitespace stays null so
"no filter" remains distinguishable from "default bucket".
(A) Defence-in-depth at every comparison site. `GetBucketConfig`
is the usual chokepoint and already calls Normalize, but
direct callers (HitCalculator.GetOrAddClientHit,
StatManager.GetNewRankingFunc, StatManager.PruneOldRankings,
AdvancedClientStatsResourceQueryHelper for both hit-stats
and rating-history paths) re-Normalize locally. Cheap; the
one comment per site documents why.
Plus an xmldoc on `EFPerformanceBucket.Code` flagging the
canonical-lower-case rule and pointing readers at
`PerformanceBucketCodes.Normalize`.
…onnection attempts
… for accurate GSC detection. Fix legacy flag pre-gate false negatives.
Previous measurement captured T1 before SetDvarAsync, so the reported "log pipeline" included C#-side semaphore wait, flood-protect throttle, and any RCon retry timeouts — overhead a natural game-side log event never experiences. On busy servers this inflated readings to 1.5-8s versus a true ~250-500ms pipeline. Plumb an optional onPacketSent callback through IRConConnection / IRConParser / SetDvarAsync, invoked synchronously from CodRConConnection.SendPayloadAsync the moment SendToAsync succeeds. ServerLatencyMonitoringService.SendProbeAsync now uses TryAdd in the callback so retries don't overwrite the first-send timestamp. OnScriptEventTriggered subtracts estimated one-way RCon delivery (rtt/2) from the measured probe-to-parse window. Result is the same path a chat / kill / connect event traverses: game writes log line -> GLS poll -> GLS forward -> IW4MAdmin parse. Drops the sample if RCon RTT isn't yet established rather than recording inflated data. Renames GameLogPipelineMs -> GameLogIngestMs (property, DTO, JSON) to match the new semantic; "pipeline" was directionally ambiguous, "ingest" pins down game -> IW4MAdmin. Drops EstimatedLogOverheadMs which was the previous "raw - rtt/2" derivation -- redundant now that the primary metric is that value.
Per-sample log ingest variance is dominated by where the probe lands in the GameLogReader 300ms poll cycle (uniform 0-300ms wait, std dev ~87ms). EMA smoothed but never escaped the noise — every sample tugged the reported value, and the fixed 30s probe interval being an exact multiple of the 300ms poll interval caused phase-locking, where samples consistently landed at the same poll-cycle offset and produced an unrepresentative steady-state reading. Replace EMA with a 60-sample sliding window (30 minutes at 30s/probe); expose its median rather than mean. Median rejects outliers structurally — a single 300ms spike is one of N votes, not a tug on the average. For long-running servers the slower reaction to genuine pipeline change (half-window past = ~15 min) is acceptable; stability while stable is the priority. Add ±20% uniform jitter to the probe timer (one-shot re-armed each cycle). Decorrelates probe phase from poll phase so successive samples are independent draws across the full 0-300ms distribution rather than clustering at one offset. Median now converges to the true ~150ms mean of the underlying distribution rather than to whichever phase the clocks happened to lock onto. RCON RTT path unchanged — that signal is low-variance and EMA fits it fine. MinSamplesRequired split: 3 for RCon, 10 for log median.
ZombieEventParser.ParseScriptEvent indiscriminately processed every GameScriptEvent received by the plugin's OnScriptEvent handler on zombie servers. LatencyProbe;<id> lines split to ["LatencyProbe", "<id>"] -> eventArgs[1] is the probeId, which never matches the parser's known-types dictionary -> WRN per probe (one every ~30s per zombie server). Add a prefix guard: if eventArgs[0] != "GSE", return null silently. Real "GSE;<unknown>;..." events still warn as intended. Subtype-based filter (typeof(GameScriptEvent) check) was considered but couples the parser to the class hierarchy; prefix-based is anchored to the wire format the parser actually owns.
Some game clients don't register the iw4x:// protocol handler, leaving users to type 'connect IP:PORT' in the in-game console manually. The play icon on the home server cards now intercepts modifier+click and copies the connect command to clipboard instead of triggering the protocol handler. Discoverability: a global keydown/keyup listener in blazor_lib.js toggles a `mod-key-down` body class while Ctrl or Meta is held; CSS swaps the play icon for a clipboard icon on .server-play-link while the class is set, so the affordance is visible before the click. Window blur clears the class to avoid getting stuck. Click handler in ServerCard always calls preventDefault and dispatches based on MouseEventArgs.CtrlKey/MetaKey -- copy via copyToClipboard JS helper + IToastService.ShowSuccessAsync when modified, otherwise opens the protocol URL via openProtocolUrl. Address uses ExternalIPAddress to match the same resolution logic ConnectProtocolUrl already uses.
Adds map power activation/deactivation as a first-class match-level event
across T4/T5/T6 zombie modes. Top-level GSE category alongside RC and
EE — power is a world property (not player-action), so forcing it into
ZE's always-attributed shape would break that contract; the existing
"world-state events get their own top-level category" precedent fits.
Wire format:
GSE;PWR;{state};{source};[{guid;cnum;team;name}]
- state = on | off
- source = world | player (discriminator)
- player block present iff source == player
Optional attribution: switch-flip events get the player who pressed the
trigger; TranZit world-driven events (bus power loss, scripted) emit
world-source. ZE's contract stays unchanged.
GSC detection per game:
- T5/T6: flag_wait("power_on") loop with on/off transition handling.
TranZit's flag_clear in zm_transit_power.gsc fires power-off through
the same loop; other maps spin in the inner poll until end_game.
- T4: piggy-backs on specialty_quickrevive_power_on perk notify (fires
on every T4 zombie map with power — Verrückt, Shi No Numa, Der Riese
— since QR is universal). One-shot dedup. Nacht has no perks so the
notify never fires there. T4 has no power-off mechanic.
- All games: parallel WatchPowerSwitches scans common use-trigger
targetnames (use_power_switch et al.), records last activator on
level._iw4m_power_activator with timestamp. EmitPowerOn checks
recency (<5s) and attributes player if fresh, world otherwise.
C# side:
- PowerStateChangeGameEvent inherits GameEventV2 (not ClientGameEvent
since Origin is nullable). Parser dispatches on source discriminator.
- EventLogType: PowerOn=25, PowerOff=26 — SourceClientId nullable
carries optional attribution into the event log.
UI presentation:
- Match-detail page panel: power section removed (was overkill — stock
maps yield 1 chip, not a section's worth of content). EE panel now
always-rendered with a "no Easter Egg quests on this map" placeholder
for Nacht (the single unconfigured stock case) so layout is stable.
- Live modal: power state shown below the map name as a subtle
green/red dot indicator ("Power · ON" / "Power · OFF"), mirroring the
live-game red-dot pattern. Hidden until at least one transition has
fired so there's no false OFF baseline. Activity feed rows for
power-on/power-off render with lightning / lightning-slash icons,
yellow / slate styling.
- Scrubber timeline: power-on (yellow, ph-lightning) and power-off
(slate, ph-lightning-slash) markers on the match-level events band,
wired into the critical/powerups/economy filter rules. Tooltips
include attribution with colour codes stripped.
DTO additions: PowerStateChanges on ZombieMatchDetail,
ZombieMatchHistoryMatch (per-client), and ZombieLiveMatchSnapshot.
Helper deletions in GameFiles/ZombieStats are pre-existing and left
unstaged for separate handling.
Skill formula (zombie path only, MP unaffected): - AvgRoundReached × RPD^0.602 × speedFactor^0.3 × soloFactor^0.5 - Per-(map, round, playerCount) EMA reference table for speed normalization - Solo tier bonus 1.50/1.25/1.10/1.00 stacked after EMA - Top-stats UI swaps in 4 new rows, suppresses broken KDR on zombies bucket Match boundary integrity fixes: - EFZombieMatch.Completed flag (true on observed ExitLevel/ShutdownGame) - GameMatchId-change crash detection in TrackClient (orphan ends Completed=false) - Per-server lock in CreateMatch (closes duplicate-matchId race, see match 524) - Tentative carryover (no DB row / lifetime bump until first per-action event) - GSC RC;1 spurious-emission guard (T4/T5/T6 PrintPlayerRoundData early-return when level.round_number undef) - Timeline radio defaults to highest-scoring qualified player (was overall top) Schema: EFZombieRoundDurationEmas table, +AverageRelativeSpeed/+AverageSoloFactor on aggregates, +Completed on EFZombieMatches, EFPerformanceBuckets prefix fix. Migrations × 3 providers.
Pull Live Snapshot and Match Page surfaces down to the leaderboard's container weight so the three views read as one design language. Live Snapshot: - All sub-containers rounded-xl → rounded-md (banner, EE strip, player cards, recent activity). Round-now stats / LIVE pulse / status pills preserved — they're live-context-specific, not redundant. Match Page: - Hero kept distinct (rounded-2xl shadow-xl) — deep-link share page warrants its own visual weight. - Progress panel (EE quests + buildables) rounded-2xl shadow-xl → rounded-md, drop shadow. Match Detail (hybrid): - Adaptive 1P/2P/3-8 player roster cards above tabs, mirroring the leaderboard's adaptive layout. Honors ShowAllPlayers for drop-ins. - Existing tabs + 8-card stat grid + timeline + round table preserved as the per-player drill-down below.
Surfaces per-(map, round, playerCount) population EMA comparisons as a 5-band
colour scale (vivid green → pale green → grey → pale red → vivid red) across
three places:
- Match-detail round table — Time cell tinted, tooltip shows %-delta + EMA
- Leaderboard scoreboard — new Pace column, aggregated over rounds whose
live count matches the bucket size (apples-to-apples within bucket)
- Live modal — banner shows "+5m23s · avg 4m12s" with elapsed tinted by the
current pace ratio; recent activity feed annotates round-completed events
Persists PlayerCountAtRoundStart on EFZombieRoundClientStats so the EMA cell
key for any historical round can be reconstructed at read time without
approximating from match-level PlayerCount.
Replace the hardcoded "zombies" bucket-code literal with a live-server ratio classifier (>75% IsZombieServer => zombies bucket). Lets admins name buckets freely (e.g. "T4 - Zombies", "Comp Zombies") without losing the zombie-specific top-stats columns or KDR suppression. Single source of truth in SharedLibraryCore so base Stats and the premium plugin both call the same helper. Trim the zombie-bucket scoreboard to the metrics that actually matter for zombies: drop Kills/Deaths/KDR (round^2 scaling makes K/D meaningless), drop LastSeen for both buckets, and filter the EFClientStatistics sum to zombie-flagged servers only when the bucket classifies as zombies (so mixed-bucket MP play doesn't pollute displayed Played totals). Move PerformanceBucketCodes from Plugins/Stats/Config to SharedLibraryCore.Helpers so the new classifier can reuse Normalize/ IsDefault instead of duplicating them across the layer boundary.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For posterity's sake -- Maybe?