CombatAnalytics

CombatAnalytics is a combat session tracking addon for World of Warcraft: Midnight

File Details

1.5-release

  • R
  • Mar 22, 2026
  • 393.76 KB
  • 32
  • 12.0.1
  • Retail

File Name

CombatAnalytics-1.5-release.zip

Supported Versions

  • 12.0.1

tag acb536d0a645e12e37c315d04d12a6e6228bc4a2 1.5-release
Author:    Lazar Dilov <ldilov@yahoo.com>
Date:    Sun Mar 22 03:07:48 2026 +0200

Major fixes + build comparator

commit 35c4ce6f5dd95e6806c124fb8234d020224ae324
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 03:05:17 2026 +0200

    fix: guard secret numeric comparisons in UNIT_AURA handler

    Midnight arena forbids comparison operators (>, <=, ~=) on secret number
    values just as it forbids using them as table keys.  Three sites affected:

    - isProcCandidate: wrap auraData.duration > 0 / <= 20 in pcall; secret
      duration fields now silently skip the short-duration proc heuristic.
    - OpenAuraWindow: pcall-guard the stacksObserved arithmetic and
      math.max(maxStacksObserved, stackCount) — stackCount is auraData.applications
      which may be secret; math.max uses comparison internally.
    - applyAuraData: simplify the maxStacksObserved update to a direct pcall
      around math.max(aggregate.maxStacksObserved, auraData.applications or 0);
      the previous pcall only guarded the read, not the comparison.

commit b67a51a81a140b11e3c3abcc1bc3ad7f5134c742
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:57:34 2026 +0200

    feat: show spec name + PvP talents in Build Comparator label

    CombatStore.lua:
    - When a build bucket is first created, stamp it with specName, specId,
      classFile, pvpTalents (spell ID array), and heroTalentSpecId from the
      session's playerSnapshot.  Written once; the hash guarantees these never
      change for a given bucket key.

    UI/BuildComparatorView.lua:
    - Replace opaque "Build 4bc8f064 (2F)" label with readable metadata.
    - resolvePvpTalentNames(): looks up spell names synchronously via
      C_Spell.GetSpellName() (PvP talents are always cached by the client).
    - snapshotFromLastSession(): self-heal for existing buckets that predate
      the metadata stamp — looks up the stored lastSessionId session.
    - buildLabel() priority: bucket.specName → last-session snapshot → hash.
    - Format: "Havoc  —  Mortal Coil, Precognition, Oppressor  (2F)"
    - Widen name FontStrings from 240 → 340px to accommodate full label.

commit 629b6e88bc51e309188ec30f71fe6f524f979fe7
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:49:32 2026 +0200

    fix: revert CLEU frame registration + guard secret aura keys in Midnight arena

    Constants.lua / Events.lua:
    - Remove COMBAT_LOG_EVENT_UNFILTERED from ROUTER_EVENTS and TRACKER_EVENT_MAP.
      Frame:RegisterEvent() on CLEU is forbidden in Midnight arena (raises lua error).
      Update comments to reflect that damage comes from C_DamageMeter instead.

    CombatTracker.lua:
    - Add safeMakeKey() helper: probes a value as a table key inside pcall; returns
      the original value if safe, nil if secret ("table index is secret" in Midnight).
    - isProcCandidate: pcall-guard Constants.SPELL_CATEGORIES[auraData.spellId].
    - OpenAuraWindow / CloseAuraWindow: run auraId through safeMakeKey before any
      table-index operation; bail early if secret.
    - removeAuraState: run auraInstanceId through safeMakeKey; bail if secret.
    - applyAuraData: extract safeInstanceID and safeSpellId at entry; skip tracking
      for auras whose IDs are secret. Guard applications comparison with pcall.
    - isFullUpdate block: use safeMakeKey before writing to seenAuraInstanceIds.

    These fixes eliminate "table index is secret" errors from UNIT_AURA handlers
    for arena opponent units in Midnight arena.

commit ad35e35b6c01d40d2dd80143e2f30a0aaa1303c1
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:38:44 2026 +0200

    fix: wire COMBAT_LOG_EVENT_UNFILTERED and harden bucket label sanitization

    CLEU was never dispatched — Frame:RegisterEvent() is not a protected
    function and cannot raise ADDON_ACTION_BLOCKED; the original comment was
    wrong.  Add the event to ROUTER_EVENTS and map it to HandleCombatLogEvent
    in TRACKER_EVENT_MAP.  NormalizeCombatLogEvent already sanitizes every
    field via SanitizeString/SanitizeNumber so restricted arena events
    (secret src/dst values) are handled safely with zeroed-out amounts.

    Also extend getOrCreateBucket to sanitize the label parameter with the
    same double-pcall pattern used for key.  opponent.name is a secret string
    in arena; storing it in bucket.label would crash any UI code that calls
    FontString:SetText() on it.  Falls back to the already-sanitized key.

    Root causes of "arena matches not recorded / win rate broken":
    - Zero damage: CLEU never fired → UpdateSpellStats never called
    - Win rate 0%: UpdateAggregatesForSession crashed in the C_Timer callback
      because getOrCreateBucket compared a secret opponent.guid (already fixed)
      and secret opponent.name in bucket.label (fixed here)

commit 3e187f8804772f8901542bebb6629e6be4179461
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:12:44 2026 +0200

    fix(ui): split tab strip into two rows to prevent overflow

    11 visible tabs at ~73-80px each exceed the 828px available in a single row.
    Split into row 1 (first 6: Summary-Dummy) and row 2 (last 5: Rating-Cleanup).
    tabStrip height: 28 → 62 (two 28px rows + 6px gap); contentShell shifts down
    automatically since it anchors to tabStrip BOTTOMLEFT.

commit ac952629b3801228aff05bef8ff7b2d9bb472877
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:08:00 2026 +0200

    fix: strengthen isSecretValue to catch concat-survives-but-still-tainted case

    On WoW Midnight, tostring() and .. propagate the secret flag rather than
    stripping it.  The previous single-pcall test (tostring(v) .. "") succeeded
    for many tainted strings, making IsSecretValue return false and letting
    tainted values reach table-key assignments (SnapshotService.lua:385:
    "table index is secret").

    Add a second pcall that tests asStr == "" — comparison to a plain string
    reliably throws for any tainted value, even ones that survived concatenation.
    This matches the double-pcall pattern already applied in getOrCreateBucket.

    All four IsSecretValue call sites (SnapshotService, CombatTracker,
    ArenaRoundTracker, ApiCompat.GetCreatureIdFromGUID) benefit automatically.

commit d4250c4938f0adc235f663c03a90788fe7510a67
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 02:05:52 2026 +0200

    fix: correct two bugs found in post-sprint review

    ReplayView._label: SetTextColor(cr or unpack(...)) only passed the R channel
    when cr was non-nil.  Fix with proper if/else so all four channels are
    forwarded; drop redundant color args from the empty-state caller.

    CombatStore: remove GetSessionById — an exact duplicate of the existing
    GetCombatById (line 534).  Update CombatHistoryView Replay button to use
    the canonical GetCombatById.

commit 04e7730592f31ba4ad0924f88dc6cf732510f830
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:58:32 2026 +0200

    feat(ui): register Builds tab in MainFrame; add BuildComparatorView to TOC

    Inserts the Builds tab between Counters and Cleanup.  BuildComparatorView.lua
    added to load order so it is available when MainFrame registers tabs.

commit 9a87eb0cb038abf08f7af36ee17f7f9aed88f9ef
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:55:39 2026 +0200

    feat(ui): add BuildComparatorView — side-by-side build performance comparison

    Tab panel with < / > selectors for Build A and Build B.  Comparison table
    shows Record, Win Rate, Avg Pressure, Avg Damage, Avg Deaths, Avg Dmg Taken.
    Winning side highlighted green.  Values prefixed ~ when < 5 fights.  Verdict
    line summarises which build wins more metrics.  Empty state for no history.

commit 12d25fddb768a8ff0c60b96563d3fff05d8ac46d
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:53:44 2026 +0200

    fix: pcall-guard GetCreatureIdFromGUID against secret arena GUIDs

    On WoW Midnight, C_CreatureInfo.GetCreatureID and strsplit are C functions
    that explicitly reject secret string arguments with a hard error.  Wrap both
    paths in pcall so UNIT_AURA handlers don't crash when the unit's GUID is
    tainted.

commit 012bde18a0de3488e8e6a5dd8556b0347e35935d
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:52:03 2026 +0200

    feat(ui): wire Replay button in history view; add ReplayView to TOC

    Add one small Replay button per history row.  On click it calls
    CombatStore:GetSessionById (new helper) then ns.ReplayView:Show(session).
    Buttons show/hide alongside their row in Refresh.  ReplayView.lua added
    to load order in CombatAnalytics.toc.

commit a201e8afa7b9c6f0b7ea19b81e443d7875cc934b
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:50:16 2026 +0200

    feat(ui): add ReplayView — session timeline canvas with 4 lanes and coaching cards

    Stateless floating frame. 4 lanes: offensive casts, defensive casts (cooldowns),
    CC received bars, kill window bars. Death marker vertical line. Time axis with
    tick labels. 3 coaching cards: Opener, CC Pressure, Death Context.
    All data sourced from session.rawEvents / ccReceived / killWindows — no CLEU.

commit dea0b17cd30ee6c00017e68f34b034239fcf3cd7
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:47:59 2026 +0200

    fix(P0): replace broken playerSnapshot field reads with canonical accessor

    Three consumers read runtime.playerSnapshot (never written); buildHash was
    always nil, disabling build-personalised guide decisions and pre-match advisory.
    Replace all three with ns.Addon:GetLatestPlayerSnapshot() which reads the
    correct runtime.latestPlayerSnapshot field.

    Also fix CounterGuideView getSpecWinLoss to use GetAggregateBucketByKey so
    W/L badges populate from real history instead of silently returning zeros.

commit c2c1e69facf1661e4c763f55dfc159d491e42415
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:47:28 2026 +0200

    fix(P0): replace map-index aggregate lookup in SuggestionEngine

    SPEC_WINRATE_DEFICIT rule was indexing GetAggregateBuckets result as a map,
    always getting nil and never firing.  Switch to GetAggregateBucketByKey.

commit 7597c21068cc9f499e31c467a810dbb0e999b840
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:47:12 2026 +0200

    fix(P0): replace map-index aggregate lookups in StrategyEngine

    GetAggregateBuckets returns a list, not a map.  Indexing by specKey always
    returned nil, zeroing out historicalWinRate and breaking HasSufficientData.
    Swap both call sites to GetAggregateBucketByKey which does a key scan.

commit 2aa70d836811ac835ed4570751b9c495eaa1d032
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:46:36 2026 +0200

    fix: guard secret-string comparison in getOrCreateBucket

    On WoW Midnight, secret arena strings survive "" concatenation but still
    throw on comparison with plain strings.  Add a second pcall around the
    safeKey ~= "" check so the crash at line 173 never reaches the Lua VM.

commit c306c9706f30008c272f75cc6b999d951d0a03df
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:40:11 2026 +0200

    feat(store): add GetAggregateBucketByKey and GetSpecBucket helpers

commit 0280c49c23749b0c8c7014354d626ee904701c2e
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:33:55 2026 +0200

    docs: add Sprint B implementation plan (9 tasks, P0 fixes + timeline + build comparator)

commit fd5619f696d86e3298b4d05926f52b84ecf0e845
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 01:17:47 2026 +0200

    docs: add Sprint B design doc (P0 fixes + timeline replay + build comparator)

    Covers two P0 correctness fixes (aggregate lookup helpers, snapshot accessor
    inconsistency) and two new features (Visual Timeline Replay, Build Comparator).

commit f3462953829ad10307669b2a77aef3ea3bc8274a
Author: Lazar Dilov <ldilov@yahoo.com>
Date:   Sun Mar 22 00:37:24 2026 +0200

    fix(arena): prevent player's own name appearing as arena opponent

    Three defensive guards against the "played against myself" bug:

    1. GetPrimaryEnemy: skip slots whose GUID matches the local player.
       After a match ends, arena unit tokens can briefly resolve to the
       player's own GUID during unit-frame transition states.

    2. HandleArenaOpponentUpdate: reject buildUnitSnapshot results whose
       GUID is the local player before storing into the slot or guidToSlot
       map.  Prevents the contaminated slot from reaching GetPrimaryEnemy.

    3. ResolveOpponentName: when entry.guid is nil (harvested while still
       secret, or not available), fall back to name comparison to skip the
       player's own score entry.  nil ~= anyGUID is always true, so
       without this guard the player's own entry passes the old check and
       their name is returned as the opponent name.