Skip to content

Zombie Stats - WIP#400

Open
Ayymoss wants to merge 152 commits into
developfrom
feature/zombie-stats
Open

Zombie Stats - WIP#400
Ayymoss wants to merge 152 commits into
developfrom
feature/zombie-stats

Conversation

@Ayymoss

@Ayymoss Ayymoss commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

For posterity's sake -- Maybe?

@Ayymoss
Ayymoss force-pushed the feature/zombie-stats branch 5 times, most recently from ddc3daf to f7d74d8 Compare April 17, 2026 09:38
@Ayymoss
Ayymoss force-pushed the feature/zombie-stats branch from 1e5f135 to 03e2bbf Compare April 26, 2026 16:40
Ayymoss and others added 24 commits June 4, 2026 20:46
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.
…citly set `<Private>false>`; update PostBuild script to dynamically detect plugin DLLs based on source `.csproj` files.
Ayymoss added 5 commits June 8, 2026 12:55
The .zsr-* scrubber styling is consumed only by the ZombieStatsPremium bundle's
scrubber JS/component, so it belongs with the bundle, not the host. Deleting the
plugin .zip now leaves no zombie scrubber CSS in the core.
Mirrors MigrateJsToCsPlugins: when a plugin moves to its .zip bundle form (e.g.
LiveRadar), an older loose DLL left in Plugins/ from a prior install would double-load
against the in-memory bundle assembly. MigrateDllToBundlePlugins reads each bundle
manifest's entryAssembly and renames a matching loose DLL to .dll.disabled at startup.
Best-effort; manifest-driven so it stays correct if the .zip and .dll stems differ.
The zombie extraction moved zombie-scrubber.js out of the host wwwroot into the
ZombieStatsPremium bundle, but the workflow still cp'd it unconditionally from
WebfrontCore/wwwroot/js. Under 'bash -e' the missing-file cp failed the publish step.
Drop the stale copy; the asset is served from the bundle at /_content.
Pomelo hadn't shipped EF Core 10, pinning the whole EF stack to 9. Swap it for the
Microting fork (Microting.EntityFrameworkCore.MySql 10.0.8), a drop-in of Pomelo that keeps
the same UseMySql/ServerVersion.AutoDetect API and MySql:* migration annotations — so the code
and the existing 165 MySQL migrations compile unchanged — and move the whole EF stack to 10:

- Microsoft.EntityFrameworkCore + Sqlite + Relational + Design + Tools + InMemory 9.0.0 -> 10.0.8
- Npgsql 9.0.0 -> 10.0.3, Npgsql.EntityFrameworkCore.PostgreSQL 9.0.0 -> 10.0.2

General package refresh in the same pass:
- Refit 9.0.2 -> 11.0.1 (MasterCommunication: ApiException.Content -> Message)
- MaxMind.GeoIP2 5.4.1 -> 6.0.0, Humanizer.Core 3.0.1 -> 3.0.10
- Microsoft.Extensions.* 10.0.1 -> 10.0.8, AspNetCore ConcurrencyLimiter/OpenApi 10.0.1 -> 10.0.8
- Scalar.AspNetCore 2.14.0 -> 2.14.14, Microsoft.CodeAnalysis.CSharp 5.0.0 -> 5.3.0
- Jint held at 3.1.3
The provider package changed from Pomelo to the Microting fork, which renames the provider
assembly — so EF Core's MigrationBuilder.ActiveProvider is now 'Microting.EntityFrameworkCore.MySql'.
Update the 12 provider-gated migration branches (case-sensitive collation, column renames, unique
indexes) to match; left as 'Pomelo...' they'd silently never run on MySQL/MariaDB. Also refresh the
query-helper comment and the CLAUDE.md provider note (and the now-stale EF Core 9 -> 10 line).
@Ayymoss
Ayymoss force-pushed the feature/zombie-stats branch from 7d43640 to d2f715c Compare June 8, 2026 14:56
Ayymoss added 24 commits June 8, 2026 16:24
Diagnose where loaded plugins come from when the same plugin appears from multiple sources.

PluginImporter now tracks each discovered plugin assembly with its ingest method and source
(loose dll path / disk bundle zip|dir path / remote-store bundle|binary with content type, byte
size, bundle id and manifest version — no URLs) and logs every candidate plus the dedup winner,
listing all competing copies when more than one is found.

PluginBundleLoader gains an optional logger (wired up by PluginImporter) and logs fresh loads vs
cache hits — the first loader of a bundle id wins the assembly identity, so a stale source being
silently ignored (the likely cause of an old version sticking) is now visible. All at Debug level.
A plugin bundle and the host webfront are two independent CSS builds sharing
one global class namespace. Cascade layers alone can't reconcile them: putting
a plugin's utilities below the host's keeps the chrome safe but stops the plugin
overriding the host's base utilities on its own pages (e.g. sm:flex-row can't
beat flex-col, so responsive layouts collapse); putting them above fixes the
plugin but its bare .hidden/.flex clobber the host's responsive chrome and the
sidebar/top bar disappear. Only scoping satisfies both.

The host now serves each plugin's CSS wrapped in @scope ([data-iw4m-plugin=
"<id>"]) and stamps that marker around everywhere it renders bundle content, so
a plugin's styles have full power inside its own subtree and zero reach outside
it. Plugins may use any classes (even ones the host uses), any CSS provider.

- PluginCssScoper: rewrites a stylesheet to @scope, hoisting @import/@property/
  @keyframes/@font-face and :root/:host token blocks back out to stay global.
  String/comment/escape-aware segment splitter.
- PluginBundleLoader: scopes every .css asset at registration; adds
  PluginIdForAssembly for the host to resolve a bundle's marker.
- Routes.razor cascades the routed page's bundle id; MainLayout wraps @Body in
  the marker (chrome stays outside). PluginRenderSlot wraps each slot widget.
  Wrappers use display:contents to stay layout-transparent. Imperatively-
  rendered content (e.g. an IModalService modal) is marked by the plugin itself.
- Bundle Tailwind helper now scaffolds unlayered output and inverts IW4M1001 to
  warn against layer(plugins); LiveRadar's input updated. Docs rewritten.

Requires the native CSS @scope at-rule (Chrome/Edge 118+, Safari 17.4+,
Firefox 128+); unsupported browsers leave plugin pages unstyled but chrome intact.
The scoping in ad7e186 isolates a plugin's CSS, but a plugin's Tailwind build
still didn't know the host's custom design tokens (surface, line, subtle, …), so
it silently skipped utilities built on them (bg-surface, border-line/80) and
leaned on the host's purged stylesheet — which supplies some but not the opacity
variants, leaving white borders and a broken live modal.

Fix it the standard way — publish the host's tokens and have plugins build
against them, so each plugin's stylesheet is self-contained (generates every
class its components use) rather than depending on the host's:

- iw4madmin-theme.css: the host's semantic design tokens, published for plugin
  builds. Each maps to the host's runtime var(--color-*), so utilities resolve
  the user's active theme. Mirror of WebfrontCore theme.css @theme.
- IW4MAdminTailwind helper: injects the token file ahead of the plugin's input
  (combined entry in obj/ with two absolute @imports; the author's @source still
  resolves relative to the author file). Automatic — no plugin opt-in.
- PluginCssScoper: when hoisting :root/:host out of @scope, strip self-
  referential declarations (--x: var(--x)) emitted by the injected token theme.
  Left in a global :root loaded after the host's, they'd override the host's
  real token values with a circular reference and break them everywhere.

A plugin author writes normal Tailwind and gets a complete, theme-matching,
self-contained, auto-scoped stylesheet with no host changes.
…scope

Two delivery gaps left bundle content broken outside its own pages.

Plugin CSS was injected per-page via <HeadContent>, so bundle content
rendered on HOST pages (render-slot widgets, IModalService modals) had no
stylesheet at all — the premium live-match modal rendered bare. HeadContent
is also last-render-wins, not additive, so injectors silently clobbered the
hosting page's own head content. Now:

- App.razor links every loaded bundle's stylesheets on every page. Scoped
  sheets are inert outside their plugin's marker, so global loading is safe
  and plugin styles work wherever the host renders plugin content.
- Plugin JS is imported as an ES module on demand by the component that
  uses it, never via head script tags (LiveRadar converted).
- ActionModal: modalClass is the complete size set (width + max-height);
  the shell no longer bakes in max-h-[90vh] that callers could only
  !-override. The modal sizing vocabulary is safelisted in app.css because
  plugin callers cannot style the host shell from their scoped CSS.

Host chrome rendered INSIDE a plugin's scoped subtree lost the cascade to
the plugin's unlayered utilities (a plugin's .hidden beat the host's
layered .xl:block, vanishing SideContextMenu; .flex-col beat .xl:flex-row,
collapsing PluginPageShell). Plugin CSS is now scoped
`to ([data-iw4m-host])`, and by containment:

- Leaf chrome (no plugin markup inside): SideContextMenu stamps
  data-iw4m-host on its roots — a scope hole plugin CSS cannot reach.
- Container chrome (wraps plugin content, so a hole would orphan it):
  PluginPageShell structure moved to host-reserved wc-page-shell* classes
  a plugin Tailwind build never emits.
Two follow-ups to the global scoped-CSS delivery, closing the gaps that
bit Credify:

- window.iw4m.scopedOverlay(pluginId): plugin JS that appends DOM outside
  the host-stamped markers (document.body overlays) needs a marker wrapper
  for its scoped CSS to apply — and stamping the element itself does NOT
  work, because inside @scope, :scope is implicitly prepended to every
  rule, so bare selectors match only descendants of the scope root. The
  helper centralizes that subtlety in the host.

- IW4M1002: the bundle targets now scan a bundle's hand-written wwwroot
  CSS and warn when a class is defined in more than one file. All of a
  bundle's sheets load on every page in one shared scope, so same-named
  classes silently override each other (plinko.css vs poker.css both
  chose pk-). Comment mentions and dots in declaration values are ignored
  — only selector preludes count; the generated Tailwind output is
  excluded.
Deprecates the standalone RaidMax.IW4MAdmin.WebCommon NuGet: plugins get
the shared web UI kit (PluginPageShell, SideContextMenu, render slots,
IModalService) from the one package they already reference.

WebCommon references SharedLibraryCore, so it cannot be a ProjectReference
in the other direction (circular) — the prebuilt WebCommon.dll (+ xml
docs) is packed into lib/net10.0 instead. The inclusion is
Exists-conditioned so a clean local first build stays green (SLC builds
before WebCommon in dependency order); the NuGet workflow therefore
builds WebCommon before packing and asserts the dll landed in the nupkg,
so an ordering regression fails the build instead of shipping silently.

Lossless: WebCommon is pure components (no static web assets), and SLC is
Sdk.Web so its nuspec already flows the Microsoft.AspNetCore.App framework
reference the components need. The bundle targets keep the legacy
WebCommon PrivateAssets update for older csprojs that still reference the
deprecated package.

Out-of-tree plugins drop their RaidMax.IW4MAdmin.WebCommon reference once
the first SLC package containing WebCommon is published — not before.
Toast, GenericTable, ToggleSwitch move to WebCommon so plugins get
them through the SharedLibraryCore package instead of copy-pasting.
ModalShell extracts the shell chrome ActionModal and custom modals
share. OpenCustom's two optional string params (modalClass/bodyClass)
collapse into a ModalRequest record — named init properties can't be
transposed the way positional strings could.
The recurring duplicate-aggregate class (concurrent check-then-insert
with no DB uniqueness) gets its permanent backstop. TPT splits the
natural key across tables — ClientId on the base, ServerId on the
child — so a plain UNIQUE(ClientId, ServerId) is impossible. Instead
two shadow columns (DedupeClientId, DedupeServerId; -1 sentinel for
lifetime rows since unique indexes treat NULLs as distinct on every
provider) are stamped automatically in SaveChanges and carry a unique
index on the child table.

Migration backfills the key from the base table and deletes existing
twin rows (lowest id survives; zmrebuild reconciles values) before the
index lands. Verified end-to-end on SQLite: twins removed, backfill
correct, constraint rejects a twin insert.
Parsers index into the split payload and Convert.ToInt32 freely; a
truncated or corrupt line threw out of the parser into event dispatch
(the caller has no catch). One catch at the dispatch site logs the raw
payload and drops the event — dropping beats coercing fields to 0,
which would silently record wrong stats.
Navs now mirror their nullable FKs (match-level and world-triggered
events legitimately have no client). Annotation-only — verified zero
model change via ef has-pending-model-changes.
PlayerGrabbedPowerupameEvent.cs renamed to match its class name.
A client ranked on a server with no performance bucket has an overall
(ServerId null) ranking-history row whose PerformanceBucket navigation
is null. The GroupBy yields a non-null result with a null Key, so
maxPerformance.Key.Code threw NullReferenceException and broke the
advanced stats page. Guard the Key, not just the result.
The premium career page now owns advanced-stats presentation, so the
free/premium boundary no longer injects per-metric rows. Free and
premium plugins ship in lockstep on this contract change.
Shell owns the p-4/md:p-6/lg:p-8 page padding so plugin pages match the
host pages (profile, leaderboard, stats) instead of double-padding.
Convert all 20 RCon/event parsers and the VPNDetection and ServerBanner
plugins from Jint JavaScript to Roslyn-compiled C# script plugins, using
C#-first idioms (enums over magic numbers, typed config, HttpClient,
Command classes, inlined EF queries).

Add IManager.AddOrReplace{RCon,Event}Parser helpers in SharedLibraryCore
so parser scripts register idempotently. Legacy Jint infrastructure is
retained (deprecated, not removed).
A CreateContext(false) context disables change detection, so loading an
entity, mutating it, and calling SaveChanges silently persists nothing.
Note this at the factory and on the SaveChanges override, and point
writers at Update/UpdateRange/ExecuteUpdate or a tracking context.
A slot widget that throws while being instantiated or rendered — e.g.
its injected services resolved against a tearing-down circuit scope —
otherwise escapes as an unobserved framework exception that names no
component. Wrap each slot widget in a logging error boundary recording
the slot, component type, and plugin id, and key the loop to keep the
render-tree diff stable.
…self-registration

Address PR review: parsers no longer subscribe to the Load event and call
AddOrReplace on the manager themselves. Introduce IParserDefinition in
SharedLibraryCore - a declarative contract exposing only Name and
Configure(IRConParser, IEventParser), with no build, DI, or manager logic.

CsPluginServiceHost now discovers parser definitions during plugin load,
materializes the dynamic RCon/event parsers via the manager, applies
Configure, and registers them (dedup/replace/remove by name on
load/reload/unload). A .cs file may contribute a plugin, parser
definitions, or both; parser-only files are not tracked as plugins.

Removes the now-unused ParserRegistrationExtensions (AddOrReplace) helper.

This makes the plugin/parser distinction explicit - parsers have no
lifecycle - and keeps host materialization out of the parser scripts.
Address PR review: invert parser ownership instead of having the plugin
host mutate the manager's parser lists.

- CsPluginServiceHost materializes .cs IParserDefinition types into
  DynamicRCon/EventParser instances it owns, tracked by name; exposes
  LoadedRConParsers/LoadedEventParsers (mirrors LoadedPlugins).
- ApplicationManager projects AdditionalRCon/EventParsers as base parser
  + host-loaded parsers; resolves the host lazily to avoid a DI cycle
  (manager -> host -> CsPluginCommandRegistrar -> IManager). IManager
  parser properties narrowed to IReadOnlyList.
- Remove the host's AddOrReplace/RemoveByName manipulation of manager
  state and the contradictory "no lifecycle" comment.
- Drop unsupported JS parser registration in ScriptPlugin; fail loudly
  to prompt migration to a .cs IParserDefinition.
- Loading a .cs file that implements neither contract now warns and
  skips instead of throwing (file may be a work in progress).

(cherry picked from commit f8aec5c)
The global TaskScheduler.UnobservedTaskException handler added in #402
logs every unobserved task fault at Error. Blazor Server's circuit-
teardown race (dotnet/aspnetcore#62718) surfaces as unobserved
ObjectDisposed/NullReference faults on every mid-render disconnect, so
the handler floods the log with thousands of benign Error entries a day.

Classify those faults (all inners ObjectDisposed/NullReference
originating in Microsoft.AspNetCore.Components) and route them to Debug
plus a periodic Information rollup, still marked observed. Genuine
plugin fire-and-forget faults continue to log at Error.

Refs dotnet/aspnetcore#62718
The render-teardown spam it was added to investigate is resolved at the
source (Application/Main.cs). Refs dotnet/aspnetcore#62718
* feat(plugins): per-plugin data directories for config, db, and files

Give each plugin an isolated Plugins/<key>/ folder (Paper-inspired), with a
host harness that routes its config, databases, and raw files into it instead
of the shared Configuration/ and Database/ folders.

- IPluginDataDirectory<TPlugin> + PluginDataDirectory<TPlugin>: injectable
  per-plugin folder handle (RootPath / GetPath / EnsureAsset)
- PluginDirectoryResolver: identity from typeof(T).Assembly, host-vs-plugin
  routing by elimination, sandbox-constrained path builder
- AddDatabase<TContext>: private-options IDbContextFactory (dodges the
  non-generic DbContextOptions collision) + auto-migrate + WAL, run before the
  management Load event; soft-fail per database
- BaseConfigurationHandlerV2: route V2 configs into the plugin folder, with a
  one-time auto-move of legacy Configuration/<name>.json
- docs/plugin-data-directories.md: plugin author guide

Additive and opt-in: a plugin that calls neither new API, and the host's own
configs, behave exactly as before. Format-agnostic across compiled .dll, .cs
scripts, .zip bundles, and remote streamed-in-memory plugins.

* feat(plugins): extract bundle resources and GSC to per-plugin sandbox

Add a 'resources/' bundle folder alongside 'gsc/'; on load the host
extracts both into the plugin's own sandbox (Plugins/<id>/Resources/
and Plugins/<id>/gsc/) instead of a shared, operator-configured path.
A deployed bundle is now self-contained: a plugin can ship a database
or data file and read it from its sandbox with no operator setup.

Drops the ApplicationConfiguration.PluginGscExtractPath setting.
WriteBundleFiles skips unchanged files (same byte length) to avoid
rewriting large data files on every load.

* feat(webfront): per-page navbar icons via IPageList.AddPage

Add IPageList.AddPage(name, location, icon) and a PageIcons map so a
plugin can declare its own Phosphor navbar icon instead of relying on
NavMenu's hardcoded location-suffix heuristic. Page.IconId carries it
to the view; NavMenu falls back to ph-chart-bar when unset. LiveRadar
registers its page with ph-wifi-high.

* refactor(plugins): address PR #407 review on per-plugin data directories

- Rename IPluginDataDirectory<T> to IPluginDataStore<T> (role name; the
  interface carries actions, not a DTO); rename impl PluginDataStore.
- Make PluginDatabaseRegistration pure data (a DbContext accessor); the host
  now owns migrate + WAL in ApplicationManager rather than the DI registration.
- Reuse Utilities.DirectorySeparatorChars instead of inline split literals.
- Document why the legacy-config move stays on the config handler and why
  PluginDbContextFactory is kept separate from the host DatabaseContextFactory.
- Note that PluginImporter's build-reference filter is not the routing
  host-assembly set.
- map lastmsg/qport/rate status columns to new GroupTypes and stash
  them as additional properties on parsed clients (-1 disables a
  mapping where a title lacks the column or the base index collides)
- add IRConParser.GetClientUserInfoAsync driven by per-parser
  DumpuserCommandFormat ({0} slot, {1} cleaned name); output parsed
  case-insensitively, null when unsupported/unparseable
- enable the query on CoD4x/IW4x/T7 + Plutonium T4(ZM)/T5/T6/IW5;
  Plutonium T4 verified live - resolves by name, not slot
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants