Vigil
← All news

v1.16.2 — Cross-report player_id collision bugfix

User-caught attribution bug. Their per-job breakdown showed PLD 336 / DRK 203 / WAR 13 for "Aoi Bomber", but they remembered being Paladin almost the whole prog. Empirical check a…

Why

User-caught attribution bug. Their per-job breakdown showed PLD 336 / DRK 203 / WAR 13 for "Aoi Bomber", but they remembered being Paladin almost the whole prog. Empirical check against the Combatant table showed the real distribution was PLD 359 / DRK 24 / WAR 13. The 203 DRK number was fabricated by aggregation bug.

Root cause: FFLogs player_id is report-scoped, not character-scoped. The same numeric pid maps to different characters across different reports. pid=12 specifically was:

  • "Aoi Bomber" PLD/DRK in one report (60 fights)
  • "Louis Moinet" Dancer in another (51 fights)
  • "Zun Lapix" Samurai in another (22 fights)
  • "Multiple Players" / "LimitBreak" actor elsewhere (22 fights)

The v1.16.1 fault_aggregate_for_encounter keyed everything by pid alone — so ALL 203 of pid=12's fights got attributed to whichever (name, job) was seen first in name_job_lookup. Result: Aoi Bomber's wipe count inflated by other players' fights, and the per-job breakdown placed them on jobs they never played.

Changed — fault_aggregate_for_encounter keys by (name, server) not pid

analysis/fault_attribution.py. The aggregate now:

  1. Builds char_at_fight[(fid, pid)] → (name, server, job) from Combatant rows.
  2. Converts per-fight active pids into per-character attendance: (name, server) → [(fid, pid)].
  3. Walks attendance per CHARACTER, not per pid. Per-fight FaultScore lookup still uses (pid, fid), but the per-fight pid is now correct for the character we're aggregating.
  4. Emits one row per (name, server) with jobs_breakdown: {job_name: {fights, root, cascade, mit_failure, heal_failure, heal_failure_caused, enrage, unknown, avoidable_damage, damage_downs, score}} nested inside.

Output row schema additions: name, server, primary_job (most-played), player_ids (representative list), jobs_breakdown. Legacy player_id field retained as the first pid for backward compat with the breakdown drill-down.

Changed — fault_breakdown_for_encounter keys by name not pid

analysis/fault_breakdown.py. Joint key switched from (player_id, ability_game_id) to (name, ability_game_id). Also folded the v1.16.1 inferred_ability_id into the joint key so non-attributable deaths get bucketed under the inferred mechanic (was producing a single "ability_id: null" lump).

Changed — Frontend FaultSection reads jobs_breakdown from backend

web/src/Home.jsx. Previously the frontend rebuilt the per-job table by aggregating p.job across player rows — broken when the same pid showed up under different jobs in different reports. Now reads the backend's per-character jobs_breakdown and sums across the merged-group's characters. Also: topMechanicsForPlayerSet and topOffendersForMechanicSet switched from pid-based filters to name-based filters (same root cause).

Tests

  • 2 new in tests/test_pid_attribution_v1_16_2.py:
    • test_same_pid_different_characters_does_not_conflate_attendance: seed pid=42 as Alice (PLD, report A, 5 wipes) AND Bob (Dancer, report B, 3 wipes). Assert Alice has 5 fights / PLD, Bob has 3 fights / Dancer, no cross-contamination.
    • test_same_pid_same_character_multiple_jobs_breakdown: Alice plays 4 PLD + 2 DRK wipes under one pid. Assert jobs_breakdown surfaces both with correct counts and primary_job = Paladin.
  • 1 test fixed: test_create_with_aliases_and_list no longer assumes the dev DB starts empty (asserts "Alice" in names not len == 1).
  • 511 tests passing (509 → 511, +2).

Live AC against dev DSR data

Pre-fix Aoi Bomber row: 552 fights (PLD 336, DRK 203, WAR 13). Post-fix: 396 fights (PLD 359, DRK 24, WAR 13) — matches the ground-truth count from the Combatants table exactly. Aoi Bomberman correct at 49 PLD.

Operator note

Existing FaultScore rows are unaffected. The fix is entirely at read-time in fault_aggregate_for_encounter. No recompute needed — open Home, your numbers correct on next refresh.