-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCoverageService.lua
More file actions
227 lines (199 loc) · 8.23 KB
/
CoverageService.lua
File metadata and controls
227 lines (199 loc) · 8.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
local _, ns = ...
local Constants = ns.Constants
local CoverageService = {}
-- ---------------------------------------------------------------------------
-- Lane definitions
-- ---------------------------------------------------------------------------
-- Each entry maps a logical coverage lane name to:
-- tlLane — the TIMELINE_LANE constant value to count events in
-- minEvents — minimum event count to score 1.0 (nil = any > 0 is full)
-- label — human-readable lane name for debug output
-- ---------------------------------------------------------------------------
local LANE_DEFS = {
-- T013: Recognize VISIBLE_CAST (v7+) as the primary cast lane.
-- PLAYER_CAST is the legacy alias; count both so sessions recorded before
-- the lane rename still score correctly.
visibleCasts = {
tlLane = Constants.TIMELINE_LANE.VISIBLE_CAST,
tlLaneFallback = Constants.TIMELINE_LANE.PLAYER_CAST,
label = "Visible casts",
},
-- T013: Coverage lane for actor visibility/identity transition events (v7+).
visibilityTransitions = {
tlLane = Constants.TIMELINE_LANE.VISIBILITY,
label = "Visibility transitions",
},
auras = {
tlLane = Constants.TIMELINE_LANE.VISIBLE_AURA,
label = "Aura state",
},
ccReceived = {
tlLane = Constants.TIMELINE_LANE.CC_RECEIVED,
label = "CC received",
},
drState = {
tlLane = Constants.TIMELINE_LANE.DR_UPDATE,
label = "DR state",
},
}
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
local function makeRecord(score, eventCount, droppedCount, summary)
return {
score = score,
eventCount = eventCount,
droppedCount = droppedCount,
summary = summary,
}
end
-- Count timeline events matching a specific lane.
local function countLaneEvents(timelineEvents, laneValue)
local total = 0
local dropped = 0
for _, ev in ipairs(timelineEvents) do
if ev.lane == laneValue then
total = total + 1
if ev.confidence == "dropped" then
dropped = dropped + 1
end
end
end
return total, dropped
end
-- Binary score: 1.0 if any events present, 0.0 if none.
local function binaryScore(count)
return count > 0 and 1.0 or 0.0
end
-- ---------------------------------------------------------------------------
-- Per-lane coverage computations
-- ---------------------------------------------------------------------------
-- damage: based on whether damageDone is populated and from which source.
local function computeDamageCoverage(session)
local imported = session.importedTotals and (session.importedTotals.damageDone or 0) or 0
local local_ = session.localTotals and (session.localTotals.damageDone or 0) or 0
local final = session.totals and (session.totals.damageDone or 0) or 0
local dmEvents = 0
for _, ev in ipairs(session.timelineEvents or {}) do
if ev.lane == Constants.TIMELINE_LANE.DM_CHECKPOINT
or ev.lane == Constants.TIMELINE_LANE.DM_SPELL then
dmEvents = dmEvents + 1
end
end
if final > 0 and (imported > 0 or local_ > 0) then
return makeRecord(1.0, dmEvents, 0,
string.format("%.0f dmg captured (imported=%d, local=%d)", final, imported, local_))
elseif dmEvents > 0 then
return makeRecord(0.5, dmEvents, 0,
string.format("DM events present but totals missing (%d events)", dmEvents))
else
return makeRecord(0.0, 0, 0, "No damage data captured")
end
end
-- identity: fraction of arena slots (1–5) that have a known GUID in the
-- session's identity record or arena roster.
local function computeIdentityCoverage(session)
local known = 0
local total = 5 -- assume up to 5 arena opponents
-- Check arena roster from ArenaRoundTracker state stored on session.
local roster = session.arena and session.arena.roster or {}
local seenSlots = {}
for _, entry in ipairs(roster) do
if entry.guid and entry.guid ~= "" then
local slot = entry.slot or entry.slotIndex
if slot and not seenSlots[slot] then
seenSlots[slot] = true
known = known + 1
end
end
end
-- Fallback: count unique opponent GUIDs from primaryOpponent + actors.
if known == 0 then
if session.primaryOpponent and session.primaryOpponent.guid then
known = 1
end
end
if total == 0 then
return makeRecord(0.0, 0, 0, "No arena context")
end
local score = math.min(1.0, known / total)
return makeRecord(score, known, 0,
string.format("%d/%d arena identities resolved", known, total))
end
-- postMatchMeta: whether C_PvP score data was harvested after the match.
local function computePostMatchMetaCoverage(session)
local scores = session.postMatchScores or session._postMatchScores
if scores and next(scores) then
local count = 0
for _ in pairs(scores) do count = count + 1 end
return makeRecord(1.0, count, 0,
string.format("Post-match scores: %d entries", count))
end
return makeRecord(0.0, 0, 0, "No post-match score data")
end
-- replayFidelity: ratio of confirmed events to all timeline events.
-- Partial or inferred events reduce fidelity.
local function computeReplayFidelityCoverage(session)
local total = 0
local confirmed = 0
for _, ev in ipairs(session.timelineEvents or {}) do
total = total + 1
if ev.confidence == "confirmed" then
confirmed = confirmed + 1
end
end
if total == 0 then
return makeRecord(0.0, 0, 0, "No timeline events")
end
local score = confirmed / total
return makeRecord(score, total, total - confirmed,
string.format("%d/%d events confirmed (%.0f%%)", confirmed, total, score * 100))
end
-- ---------------------------------------------------------------------------
-- Public API
-- ---------------------------------------------------------------------------
--- Compute per-lane coverage for the given session and write session.coverage.
--- Called at the end of FinalizeSession.
function CoverageService:Finalize(session)
if not session then return end
local events = session.timelineEvents or {}
local coverage = {}
-- Event-count-based lanes
for laneName, def in pairs(LANE_DEFS) do
local count, dropped = countLaneEvents(events, def.tlLane)
-- T013: For lanes with a fallback (e.g. visibleCasts uses VISIBLE_CAST
-- but falls back to legacy PLAYER_CAST for old sessions), add both counts.
if def.tlLaneFallback and count == 0 then
local fbCount, fbDropped = countLaneEvents(events, def.tlLaneFallback)
count = count + fbCount
dropped = dropped + fbDropped
end
local score = binaryScore(count)
local summary
if count == 0 then
summary = def.label .. ": no events captured"
else
summary = string.format("%s: %d events", def.label, count)
if dropped > 0 then
summary = summary .. string.format(", %d dropped", dropped)
end
end
coverage[laneName] = makeRecord(score, count, dropped, summary)
end
-- Special-cased lanes
coverage.damage = computeDamageCoverage(session)
coverage.identity = computeIdentityCoverage(session)
coverage.postMatchMeta = computePostMatchMetaCoverage(session)
coverage.replayFidelity = computeReplayFidelityCoverage(session)
session.coverage = coverage
end
--- Return the coverage score for a named lane, or nil if not computed yet.
function CoverageService:GetLaneScore(session, laneName)
if not session or not session.coverage then return nil end
local record = session.coverage[laneName]
return record and record.score or nil
end
-- ---------------------------------------------------------------------------
-- Registration
-- ---------------------------------------------------------------------------
ns.Addon:RegisterModule("CoverageService", CoverageService)