Skip to content

versus: accrue per-player stats per chapter, not per whole match (0.7.0) - #8

Open
draxios wants to merge 1 commit into
mainfrom
feat/versus-per-chapter-stats
Open

draxios wants to merge 1 commit into
mainfrom
feat/versus-per-chapter-stats

Conversation

@draxios

@draxios draxios commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Versus stats now accrue per chapter, not per whole match

In versus a match is a whole campaign and almost never finishes — players
leave near the end. But player_versus_stats (the per-player versus record a
profile/leaderboard reads) only rolled up at match close, so in practice it
stayed empty, match_maps mostly sat incomplete, and every match_rounds
row was round 1 (round 2 never recorded). Diagnosed live: 85 match_rounds, 412
player_round_stats rows, 0 player_versus_stats rows.

What changed

  • Per-chapter rollup. player_versus_stats now rolls up at the close of each
    chapter (a map's two halves — "play Survivor and Infected once"), which
    reliably completes. It runs inside the round-2 DB transaction so the rollup sees
    both halves' player_round_stats atomically. A completed chapter credits every
    player immediately, even if the match is later abandoned. maps_won/maps_lost
    are the win/loss record (one per chapter); streaks count consecutive chapters.
    Match close only touches matches_* (no double count).
  • Round 2 is recorded. Half detection moved from the unreliable
    versus_round_start/is_secondary_round (stays false here → Duplicate entry 'N-1') to counting the engine round_start with idempotency guards.
  • First-chapter drop fixed. The first chapter of every match was lost to an
    async race (OpenMatchMap() ran while g_MatchId was still 0); it's now opened
    from the OnMatchInserted callback.
  • Chapter winner from plugin Survivor points (the engine distance netprop
    reads 0 on this build — an engine-accurate winner is a documented follow-up).
  • Robustness: HookEvent → HookEventEx (a build missing an optional event
    like entered_checkpoint no longer aborts OnPluginStart — this was blocking
    the plugin from loading on the prod build), and GetAdminTopMenu() guarded with
    LibraryExists("adminmenu").

Verification

  • Compiles clean on SM 1.12 (no errors); loads Status: running on prod.
  • Reviewed by a 4-lens adversarial pass (compile / SQL / state-machine /
    semantics) — its findings (first-chapter race, engine-score index, round W/L
    over-count, event-ordering) are addressed here.
  • Deployed to Bizzy's Dugout (prod, that server only) to bake. Per-chapter
    recording needs a live versus match to fully validate — an empty server can't
    reproduce team flips. Once a real match confirms player_versus_stats /
    match_maps populate correctly, the compiled plugin will be vendored into the
    bizzymod-campaign monorepo PR and both merged.

In versus a "match" is a whole campaign and almost never finishes (players leave
near the end), so player_versus_stats — which only rolled up at match close —
stayed empty in practice, and match_maps mostly sat 'incomplete'. This reworks
versus.sp's round/map lifecycle so completed chapters credit players immediately.

- Detect the two halves from the engine `round_start` event with a per-chapter
  counter + idempotency guards, instead of the `versus_round_start`
  is_secondary_round flag (which stays false on this build, so every half tried
  to insert as round 1 and hit the match_rounds unique key). Round 2 is now
  recorded and chapters close.
- Roll player_versus_stats up PER CHAPTER at chapter close, inside the round-2
  transaction so the rollup SELECT sees both halves' player_round_stats
  atomically. maps_won/maps_lost are the win/loss record; streaks count
  consecutive chapters. Match close only touches matches_* (no double count).
- Fix the async race that silently dropped the FIRST chapter of every match:
  open the first chapter from OnMatchInserted after g_MatchId is set, not inline
  after the async OpenMatch.
- Chapter winner from plugin Survivor points (engine distance netprop reads 0 on
  this build; engine-accurate winner is a documented future refinement). Flush
  the open chapter + a dangling half on abandon/mode-change/finale so nothing is
  lost; add a same-map re-entry guard.
- Make event hooks robust: HookEvent -> HookEventEx so a build missing an
  optional event (e.g. entered_checkpoint) doesn't abort OnPluginStart.
- Guard GetAdminTopMenu() with LibraryExists("adminmenu") so startup doesn't
  crash when the plugin loads before adminmenu (matchmode reload chain).

Verified: compiles clean on SM 1.12; loads Status=running on prod. Per-chapter
recording needs a live versus match to fully validate (empty-server can't
reproduce team flips) — deployed to the Dugout to bake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6a1P8bjmnJ45TanqwQekA
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.

1 participant