-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDebug.lua
More file actions
2883 lines (2615 loc) · 117 KB
/
Copy pathDebug.lua
File metadata and controls
2883 lines (2615 loc) · 117 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- WeakestAuras -- in-game probes and verification commands for the runtime
-- engine. Registers /wa probe, soundprobe, states, libs, addons, gen, load,
-- conditions, codeprobe, textprobe, texprobe, wa2probe, wa2, and cdtest.
if WeakestAuras.disabled then return end
local WA = WeakestAuras
WA.Debug = {}
local D = WA.Debug
local MAX_LOG_LINES = 500
local MAX_SLOT = 40 -- safe superset of any plausible buff/debuff cap on this client
-- ---------------------------------------------------------------------------
-- Output window: a scrollable EditBox so dumps can be Ctrl+C'd out instead of
-- scrolling off in chat. UIPanelScrollFrameTemplate is a stock Blizzard
-- template referenced by name, same "no XML of our own" approach the aura
-- list already uses with FauxScrollFrameTemplate (see OptionsFrame.lua).
-- ---------------------------------------------------------------------------
local buffer = {}
local frame, editBox
local function refresh()
editBox:SetText(table.concat(buffer, "\n"))
end
local function ensureFrame()
if frame then return end
frame = CreateFrame("Frame", "WA_DebugFrame", UIParent)
frame:SetWidth(560); frame:SetHeight(400)
frame:SetPoint("CENTER", UIParent, "CENTER", 250, 0)
frame:SetBackdrop(WA.Widgets.PANEL_BACKDROP)
frame:SetBackdropColor(0, 0, 0, 1)
frame:SetFrameStrata("DIALOG")
frame:SetToplevel(true)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function() frame:StartMoving() end)
frame:SetScript("OnDragStop", function() frame:StopMovingOrSizing() end)
frame:Hide()
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", 0, -14)
title:SetText("WeakestAuras Debug")
title:SetTextColor(1, 0.82, 0)
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", -4, -4)
close:SetScript("OnClick", function() frame:Hide() end)
local clearBtn = WA.Widgets.button(frame, "Clear", function() D.Clear() end)
clearBtn:SetWidth(70)
clearBtn:SetPoint("TOPLEFT", 12, -14)
local selectBtn = WA.Widgets.button(frame, "Select All", function()
editBox:SetFocus()
editBox:HighlightText()
end)
selectBtn:SetWidth(90)
selectBtn:SetPoint("LEFT", clearBtn, "RIGHT", 6, 0)
local scroll = CreateFrame("ScrollFrame", "WA_DebugFrameScroll", frame, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", 12, -44)
scroll:SetPoint("BOTTOMRIGHT", -30, 12)
editBox = CreateFrame("EditBox", "WA_DebugFrameEditBox", scroll)
editBox:SetMultiLine(true)
editBox:SetAutoFocus(false)
editBox:SetFontObject(ChatFontNormal)
editBox:SetWidth(500)
editBox:SetHeight(2000) -- generously tall; the scroll frame clips/scrolls it
editBox:SetTextInsets(4, 4, 4, 4)
editBox:SetScript("OnEscapePressed", function() editBox:ClearFocus() end)
scroll:SetScrollChild(editBox)
end
function D.Log(line)
ensureFrame()
table.insert(buffer, line)
if table.getn(buffer) > MAX_LOG_LINES then
table.remove(buffer, 1)
end
refresh()
frame:Show()
end
function D.Clear()
buffer = {}
if editBox then refresh() end
end
function D.Show()
ensureFrame()
frame:Show()
end
-- ---------------------------------------------------------------------------
-- /wa dump [unit] [filter] -- settles: exact AuraData field shapes, whether
-- `name` ever carries a "(Rank N)" suffix, and where the list actually
-- terminates (the buff-cap blind-spot question).
-- ---------------------------------------------------------------------------
local function dumpOne(unit, filter)
D.Log(string.format("--- dump unit=%s filter=%s ---", unit, filter))
local found, highest = 0, 0
for i = 1, MAX_SLOT do
local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, filter)
if aura then
found = found + 1
highest = i
local remain = -1
if aura.expirationTime and aura.expirationTime > 0 then
remain = aura.expirationTime - GetTime()
end
D.Log(string.format(
" i=%d name=%s apps=%s spellId=%s dispel=%s helpful=%s harmful=%s dur=%s exp=%s remain=%.1f src=%s srcGUID=%s",
i, tostring(aura.name), tostring(aura.applications), tostring(aura.spellId), tostring(aura.dispelName),
tostring(aura.isHelpful), tostring(aura.isHarmful), tostring(aura.duration), tostring(aura.expirationTime),
remain, tostring(aura.sourceUnit), tostring(aura.sourceGUID)))
end
end
D.Log(string.format("--- end dump: %d aura(s), highest occupied slot=%d (scanned 1..%d) ---", found, highest, MAX_SLOT))
end
function D.Dump(unit, filter)
if not unit or unit == "" then unit = "player" end
if filter and filter ~= "" then
dumpOne(unit, string.upper(filter))
else
dumpOne(unit, "HELPFUL")
dumpOne(unit, "HARMFUL")
end
end
-- ---------------------------------------------------------------------------
-- /wa watch [unit] -- settles: does UNIT_AURA actually fire on every aura
-- change (including a same-buff refresh), or does it miss some the way
-- pfUI's own comment claims on their server? Diffs a snapshot on a timer and
-- flags any change that happened without a UNIT_AURA fire in that window.
-- ---------------------------------------------------------------------------
local WATCH_FILTERS = { "HELPFUL", "HARMFUL" }
local watch = {
active = {}, -- [unit] = true
lastSnapshot = {}, -- [unit] = { [filter..index] = packed string }
eventSeen = {}, -- [unit] = true/false since last tick
ticker = nil,
eventFrame = nil,
}
local function snapshotUnit(unit)
local snap = {}
for f = 1, table.getn(WATCH_FILTERS) do
local filter = WATCH_FILTERS[f]
for i = 1, MAX_SLOT do
local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, filter)
if aura then
snap[filter .. i] = tostring(aura.name) .. "|" .. tostring(aura.applications) .. "|" ..
tostring(aura.duration) .. "|" .. tostring(aura.expirationTime)
end
end
end
return snap
end
local function diffAndLog(unit)
local old = watch.lastSnapshot[unit] or {}
local new = snapshotUnit(unit)
for f = 1, table.getn(WATCH_FILTERS) do
local filter = WATCH_FILTERS[f]
for i = 1, MAX_SLOT do
local key = filter .. i
if old[key] ~= new[key] then
D.Log(string.format("[watch:%s] %s slot %d changed: %s -> %s (UNIT_AURA seen: %s)",
unit, filter, i, tostring(old[key]), tostring(new[key]), tostring(watch.eventSeen[unit] and true or false)))
end
end
end
watch.lastSnapshot[unit] = new
watch.eventSeen[unit] = false
end
local function tickWatch()
for unit in pairs(watch.active) do
diffAndLog(unit)
end
end
local function ensureWatchRunning()
if not watch.eventFrame then
watch.eventFrame = CreateFrame("Frame")
watch.eventFrame:RegisterEvent("UNIT_AURA")
watch.eventFrame:SetScript("OnEvent", function()
if watch.active[arg1] then
watch.eventSeen[arg1] = true
end
end)
end
if not watch.ticker then
watch.ticker = C_Timer.NewTicker(0.5, tickWatch)
end
end
function D.ToggleWatch(unit)
if not unit or unit == "" then unit = "player" end
if watch.active[unit] then
watch.active[unit] = nil
D.Log("[watch] stopped watching " .. unit)
else
watch.active[unit] = true
watch.lastSnapshot[unit] = snapshotUnit(unit)
watch.eventSeen[unit] = false
D.Log("[watch] started watching " .. unit .. " (diffing every 0.5s against UNIT_AURA)")
ensureWatchRunning()
end
end
-- ---------------------------------------------------------------------------
-- /wa events [EVENT ...] -- raw event firehose, unfiltered and independent of
-- /wa watch's per-unit bookkeeping: logs every fire with its whole payload, so
-- the actual stream can be read instead of inferred from diffs (does it ever
-- double-fire for one change? fire for a unit nobody's watching?). Defaults to
-- UNIT_AURA. Naming events is how an unfamiliar one's argument order gets
-- settled -- a doc listing an order is not evidence of it on this client.
-- ---------------------------------------------------------------------------
local eventLogFrame
local eventLogActive = false
local eventLogCount = 0
local eventLogNames = {}
-- The payload as "arg1=x arg3=y", skipping the trailing nils a shorter event
-- leaves behind.
local function eventLogPayload()
local vals = { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 }
local parts = {}
for i = 1, 9 do
if vals[i] ~= nil then
table.insert(parts, "arg" .. i .. "=" .. tostring(vals[i]))
end
end
if table.getn(parts) == 0 then return "(no args)" end
return table.concat(parts, " ")
end
function D.ToggleEventLog(rest)
if eventLogActive then
eventLogActive = false
for i = 1, table.getn(eventLogNames) do
eventLogFrame:UnregisterEvent(eventLogNames[i])
end
D.Log(string.format("[events] stopped (%d fire(s) logged this session)", eventLogCount))
return
end
local wanted = {}
for name in string.gfind(rest or "", "%S+") do
table.insert(wanted, string.upper(name))
end
if table.getn(wanted) == 0 then wanted = { "UNIT_AURA" } end
if not eventLogFrame then
eventLogFrame = CreateFrame("Frame")
eventLogFrame:SetScript("OnEvent", function()
eventLogCount = eventLogCount + 1
D.Log(string.format("[events] #%d t=%.2f %s %s",
eventLogCount, GetTime(), event, eventLogPayload()))
end)
end
eventLogNames = {}
for i = 1, table.getn(wanted) do
local name = wanted[i]
-- RegisterEvent throws on a name this client doesn't know, which is
-- itself the answer when the question is whether the event exists here.
if pcall(function() eventLogFrame:RegisterEvent(name) end) then
table.insert(eventLogNames, name)
else
D.Log("[events] " .. name .. " -- refused by the client, no such event")
end
end
eventLogActive = true
if table.getn(eventLogNames) == 0 then
D.Log("[events] started, but nothing registered")
else
D.Log("[events] started on: " .. table.concat(eventLogNames, ", "))
end
end
-- ---------------------------------------------------------------------------
-- /wa auraprobe [unit|all] -- Nampower's aura-cast events, decoded against the
-- watched unit's harmful descriptor. The question is whether a debuff the
-- descriptor never transmits (only 16 harmful slots exist, and a raid boss
-- carries more) is still knowable: whether AURA_CAST_ON_OTHER carries *other*
-- players' casts and not only ours, whether arg9's debuff-bar-full bit sets,
-- whether arg8 is a real talented duration, and whether an aura it reports is
-- one /wa dump cannot see.
--
-- Arg order is taken from two working consumers on this client,
-- ../SuperCleveRoidMacros/NampowerAPI.lua and ../pfUI/libs/libdebuff.lua:
-- arg1 spellId arg2 casterGuid arg3 targetGuid arg4 effect
-- arg5 effectAuraName (a numeric aura type, whatever the name suggests)
-- arg6 effectAmplitude (a periodic effect's tick period, ms)
-- arg7 effectMiscValue arg8 durationMs
-- arg9 auraCapStatus (bit 1 buff bar full, bit 2 debuff bar full)
-- The SELF/OTHER split is by *target*, not by caster.
-- ---------------------------------------------------------------------------
local AURA_CAST_EVENTS = { "AURA_CAST_ON_SELF", "AURA_CAST_ON_OTHER" }
local AURA_CAST_CVAR = "NP_EnableAuraCastEvents"
local AURA_CAST_MIN = "2.20.0"
local AURA_CAST_DEDUPE = 0.1 -- pfUI's window: one event fires per spell *effect*
local AURA_CAST_SETTLE = 2 -- the cast event precedes the descriptor update
local HARMFUL_SLOTS = 16 -- descriptor slots 32..47; see design/client/gotchas.md
local auraProbe = { registered = {}, session = 0, stats = {} }
local function auraProbeReset()
auraProbe.n = 0
auraProbe.last = {}
auraProbe.pending = 0
auraProbe.stats = {
seen = 0, elsewhere = 0, onSelf = 0, onOther = 0,
byPlayer = 0, byOther = 0, dupes = 0,
capDebuff = 0, capBuff = 0, capNil = 0, zeroDur = 0,
hit = 0, miss = 0, overflow = 0,
}
end
-- GUIDs arrive as strings from two different sources here (Nampower's event and
-- the client's UnitGUID), so they are compared case-insensitively rather than
-- assumed to agree on hex case.
local function sameGuid(a, b)
if not a or not b then return false end
return string.lower(tostring(a)) == string.lower(tostring(b))
end
local function shortGuid(guid)
local s = tostring(guid)
if string.len(s) > 6 then return "~" .. string.sub(s, -6) end
return s
end
-- nil when the token is not addressable at all, which a raw GUID used as a unit
-- token may well not be -- that is one of the things being probed.
local function harmfulScan(unit, wantSpellId)
local count, highest, slot = 0, 0, nil
local ok = pcall(function()
for i = 1, MAX_SLOT do
local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, "HARMFUL")
if aura then
count = count + 1
highest = i
if wantSpellId and not slot and aura.spellId == wantSpellId then slot = i end
end
end
end)
if not ok then return nil end
return count, highest, slot
end
local function capBits(status)
local n = tonumber(status)
if not n then return nil end
return math.mod(n, 2) == 1, math.mod(math.floor(n / 2), 2) == 1
end
-- The descriptor lags the cast event, so the read that settles "did this one get
-- a slot?" is the one taken a couple of seconds later.
local function auraProbeCheckLater(unit, guid, spellId, name)
local session = auraProbe.session
auraProbe.pending = auraProbe.pending + 1
C_Timer.After(AURA_CAST_SETTLE, function()
if auraProbe.session ~= session then return end
auraProbe.pending = auraProbe.pending - 1
local label = string.format("[auraprobe] +%ds %s(%s):", AURA_CAST_SETTLE, tostring(name), tostring(spellId))
if not sameGuid(UnitGUID and UnitGUID(unit), guid) then
D.Log(label .. " " .. unit .. " is a different unit now, cross-check skipped")
return
end
local count, _, slot = harmfulScan(unit, spellId)
if not count then
D.Log(label .. " " .. unit .. " is no longer readable, cross-check skipped")
elseif slot then
auraProbe.stats.hit = auraProbe.stats.hit + 1
D.Log(string.format("%s descriptor HIT slot %d (harm=%d)", label, slot, count))
else
auraProbe.stats.miss = auraProbe.stats.miss + 1
if count >= HARMFUL_SLOTS then
auraProbe.stats.overflow = auraProbe.stats.overflow + 1
D.Log(string.format("%s descriptor MISS at harm=%d -- OVERFLOW, /wa dump cannot see this one", label, count))
else
D.Log(string.format("%s descriptor MISS at harm=%d -- not full, so it expired, was resisted, or never applied", label, count))
end
end
end)
end
local function auraProbeEvent()
local spellId, casterGuid, targetGuid = arg1, arg2, arg3
local effect, auraName, amplitude, misc = arg4, arg5, arg6, arg7
local durationMs, capStatus = arg8, arg9
local st = auraProbe.stats
st.seen = st.seen + 1
if auraProbe.unit and not sameGuid(targetGuid, UnitGUID and UnitGUID(auraProbe.unit)) then
st.elsewhere = st.elsewhere + 1
return
end
if event == "AURA_CAST_ON_SELF" then st.onSelf = st.onSelf + 1 else st.onOther = st.onOther + 1 end
local now = GetTime()
local key = tostring(targetGuid) .. "|" .. tostring(spellId) .. "|" .. tostring(casterGuid)
local prev = auraProbe.last[key]
local dup = prev and (now - prev) < AURA_CAST_DEDUPE
auraProbe.last[key] = now
if dup then st.dupes = st.dupes + 1 end
local mine = sameGuid(casterGuid, auraProbe.playerGuid)
if mine then st.byPlayer = st.byPlayer + 1 else st.byOther = st.byOther + 1 end
local casterLabel = "you"
if not mine then
casterLabel = "OTHER " .. shortGuid(casterGuid)
-- SuperWoW makes a GUID a unit token, but whether an arbitrary caster
-- resolves through one is part of what this probe answers.
local ok, nm = pcall(UnitName, casterGuid)
if ok and nm then casterLabel = casterLabel .. " " .. nm end
end
local buffFull, debuffFull = capBits(capStatus)
if capStatus == nil then st.capNil = st.capNil + 1 end
if debuffFull then st.capDebuff = st.capDebuff + 1 end
if buffFull then st.capBuff = st.capBuff + 1 end
local capText = tostring(capStatus)
if debuffFull then capText = capText .. "(debuff-full)" end
if buffFull then capText = capText .. "(buff-full)" end
local ms = tonumber(durationMs)
if not ms or ms == 0 then st.zeroDur = st.zeroDur + 1 end
local count, highest = harmfulScan(auraProbe.unit or targetGuid)
local name = C_Spell and C_Spell.GetSpellName and C_Spell.GetSpellName(spellId)
auraProbe.n = auraProbe.n + 1
D.Log(string.format(
"[auraprobe] #%d %s %s(%s) by %s -> %s harm=%s%s dur=%s cap=%s eff=%s aura=%s amp=%s misc=%s%s",
auraProbe.n,
event == "AURA_CAST_ON_SELF" and "SELF " or "OTHER",
tostring(name), tostring(spellId), casterLabel, shortGuid(targetGuid),
tostring(count), (count and count >= HARMFUL_SLOTS) and " FULL" or "",
ms and string.format("%.1fs", ms / 1000) or tostring(durationMs),
capText, tostring(effect), tostring(auraName), tostring(amplitude), tostring(misc),
dup and " DUP" or ""))
if highest and count and highest > count then
D.Log(string.format("[auraprobe] ^ descriptor has a gap: %d aura(s) but highest slot %d", count, highest))
end
if auraProbe.unit and not dup then
auraProbeCheckLater(auraProbe.unit, targetGuid, spellId, name)
end
end
local function auraProbeStop()
auraProbe.active = false
for i = 1, table.getn(auraProbe.registered) do
auraProbe.frame:UnregisterEvent(auraProbe.registered[i])
end
auraProbe.registered = {}
local st = auraProbe.stats
local where = auraProbe.unit or "any unit"
D.Log("--- auraprobe summary ---")
D.Log(string.format(" %d event(s) fired, %d on another unit, %d logged on %s",
st.seen, st.elsewhere, auraProbe.n, where))
D.Log(string.format(" event split on %s: ON_SELF %d, ON_OTHER %d", where, st.onSelf, st.onOther))
D.Log(string.format(" caster split on %s: you %d, someone else %d <- Q2, the load-bearing one",
where, st.byPlayer, st.byOther))
D.Log(string.format(" arg9: debuff-bar-full %d, buff-bar-full %d, nil %d <- Q3",
st.capDebuff, st.capBuff, st.capNil))
D.Log(string.format(" arg8: %d carried a duration, %d were zero <- Q4",
auraProbe.n - st.zeroDur, st.zeroDur))
D.Log(string.format(" descriptor at +%ds: %d hit, %d miss, %d of those missed while full <- Q5",
AURA_CAST_SETTLE, st.hit, st.miss, st.overflow))
D.Log(string.format(" %d event(s) landed inside the %.1fs dedupe window (multi-effect spells)",
st.dupes, AURA_CAST_DEDUPE))
if auraProbe.pending > 0 then
D.Log(string.format(" %d cross-check(s) still pending; their lines follow this summary",
auraProbe.pending))
end
D.Log("--- end auraprobe ---")
end
function D.AuraProbe(rest)
if auraProbe.active then
auraProbeStop()
return
end
local _, _, word = string.find(string.lower(rest or ""), "^%s*(%S*)")
local unit = "target"
if word == "all" then unit = nil
elseif word ~= "" then unit = word end
auraProbe.session = auraProbe.session + 1
auraProbeReset()
auraProbe.unit = unit
auraProbe.playerGuid = (UnitGUID and UnitGUID("player")) or (GetPlayerGuid and GetPlayerGuid())
D.Log("--- auraprobe ---")
if not WA.hasNampower then
D.Log(" GetNampowerVersion absent -- no aura-cast events on this client")
else
local ok, major, minor, patch = pcall(GetNampowerVersion)
if not ok or not major then
D.Log(" GetNampowerVersion did not answer")
else
minor, patch = minor or 0, patch or 0
local have = WA.ParseVersion(major .. "." .. minor .. "." .. patch)
local need = WA.ParseVersion(AURA_CAST_MIN)
D.Log(string.format(" Nampower %s.%s.%s, aura-cast events need %s: %s <- Q1",
tostring(major), tostring(minor), tostring(patch), AURA_CAST_MIN,
(have and have >= need) and "OK" or "TOO OLD"))
end
end
-- Nampower sends nothing at all with this CVar off, so the probe turns it on
-- rather than reporting an empty stream as a negative result.
if type(GetCVar) ~= "function" then
D.Log(" GetCVar absent -- cannot read " .. AURA_CAST_CVAR .. " <- Q1")
else
local ok, value = pcall(GetCVar, AURA_CAST_CVAR)
D.Log(string.format(" %s = %s <- Q1", AURA_CAST_CVAR, ok and tostring(value) or "unreadable"))
if ok and value ~= "1" and type(SetCVar) == "function" then
pcall(SetCVar, AURA_CAST_CVAR, "1")
local reread, after = pcall(GetCVar, AURA_CAST_CVAR)
D.Log(" set it to 1 -> reads back " .. (reread and tostring(after) or "unreadable"))
end
end
-- The overflow cache drops helpful auras on this classifier's word, so a
-- client whose ClassicAPI does not carry it caches more than it needs to.
if not (C_Spell and C_Spell.IsSpellHarmful) then
D.Log(" C_Spell.IsSpellHarmful absent -- helpful auras cannot be filtered out")
else
local okHarm, harmful = pcall(C_Spell.IsSpellHarmful, 9835)
local okHelp, helpful = pcall(C_Spell.IsSpellHarmful, 1126)
D.Log(string.format(" C_Spell.IsSpellHarmful: Moonfire(9835)=%s, Mark of the Wild(1126)=%s (want true, false)",
okHarm and tostring(harmful) or "errored", okHelp and tostring(helpful) or "errored"))
end
if not auraProbe.frame then
auraProbe.frame = CreateFrame("Frame")
auraProbe.frame:SetScript("OnEvent", function() WA.safecall("auraprobe", auraProbeEvent) end)
end
for i = 1, table.getn(AURA_CAST_EVENTS) do
local name = AURA_CAST_EVENTS[i]
-- RegisterEvent throws on an event this client does not know, which is
-- itself the answer when the question is whether it exists here.
if pcall(function() auraProbe.frame:RegisterEvent(name) end) then
table.insert(auraProbe.registered, name)
else
D.Log(" " .. name .. " -- refused by the client, no such event")
end
end
D.Log(" registered: " .. (table.getn(auraProbe.registered) > 0
and table.concat(auraProbe.registered, ", ") or "nothing"))
D.Log(" player GUID " .. tostring(auraProbe.playerGuid))
if unit then
local count, highest = harmfulScan(unit)
D.Log(string.format(" watching %s (guid %s): %s harmful aura(s) now, highest slot %s",
unit, tostring(UnitGUID and UnitGUID(unit)), tostring(count), tostring(highest)))
else
D.Log(" watching every target -- no descriptor cross-check without a unit token, and a raid is a firehose")
end
auraProbe.active = true
D.Log(" running -- /wa auraprobe again to stop and print the summary")
end
-- ---------------------------------------------------------------------------
-- /wa overflow [unit] -- what the overflow cache holds for a unit, how long each
-- entry has left, and whether the trust gate currently lets any of it be used.
-- Runs the same reconcile a trigger would, so it evicts as it reports: the dump
-- is the state a scan would see, not the state before one.
-- ---------------------------------------------------------------------------
function D.Overflow(rest)
local _, _, word = string.find(string.lower(rest or ""), "^%s*(%S*)")
local unit = (word ~= "" and word) or "target"
local AO = WA.AuraOverflow
D.Log("--- overflow " .. unit .. " ---")
if not (AO and AO.Enabled()) then
D.Log(" the cache is not running -- Nampower absent or below 2.20")
D.Log("--- end overflow ---")
return
end
D.Log(" global toggle: " .. (WA.Options().auraOverflow == false and "OFF" or "on"))
local guid = UnitGUID and UnitGUID(unit)
if not guid then
D.Log(" " .. unit .. " has no GUID")
D.Log("--- end overflow ---")
return
end
local gate, count = AO.Reconcile(unit, guid)
D.Log(string.format(" %s guid %s, harmful descriptor %s/%d -- trust gate %s",
unit, tostring(guid), tostring(count), HARMFUL_SLOTS, gate and "PASSES" or "fails"))
local now = GetTime()
local entries = AO.EntriesFor(guid) or {}
local n = table.getn(entries)
for i = 1, n do
local e = entries[i]
local remain = "unknown"
if e.duration > 0 then remain = string.format("%.1fs", (e.start + e.duration) - now) end
D.Log(string.format(" %s(%s) caster=%s remain=%s age=%.1fs capped=%s",
tostring(e.name), tostring(e.spellId), tostring(e.caster), remain,
now - e.start, tostring(AO.WasCapped(e))))
end
D.Log(string.format(" %d entry(ies)%s", n,
gate and "" or " -- none of them can be surfaced while the gate fails"))
D.Log("--- end overflow ---")
end
-- ---------------------------------------------------------------------------
-- /wa linkprobe -- which entry point, if any, a shift-clicked item actually
-- reaches on this client. Vanilla FrameXML routes a bag shift-click straight
-- into ChatEdit_InsertLink (and only when the chat box is open);
-- HandleModifiedItemClick is a later-expansion function that may not exist
-- here at all, and a replacement bag UI bypasses both. Wraps every candidate
-- and logs which one fires, so the answer comes from a click rather than a doc.
-- ---------------------------------------------------------------------------
local linkProbeOn = false
function D.LinkProbe()
local names = {
"HandleModifiedItemClick", "ChatEdit_InsertLink", "SetItemRef",
"ContainerFrameItemButton_OnClick", "PickupContainerItem",
"IsModifiedClick", "GetContainerItemLink",
}
for i = 1, table.getn(names) do
D.Log("[link] " .. names[i] .. " = " .. type(getglobal(names[i])))
end
D.Log("[link] chat edit box visible = "
.. tostring(ChatFrameEditBox and ChatFrameEditBox:IsVisible() and true or false))
if linkProbeOn then
D.Log("[link] already logging -- shift-click an item in your bags now")
return
end
linkProbeOn = true
-- Each wrapper logs and calls through, so nothing it touches changes
-- behaviour. Left installed for the session: these are cold paths.
for i = 1, table.getn(names) do
local name = names[i]
local orig = getglobal(name)
if type(orig) == "function" then
setglobal(name, function(a1, a2, a3, a4)
D.Log("[link] " .. name .. "(" .. tostring(a1) .. ", " .. tostring(a2) .. ")")
return orig(a1, a2, a3, a4)
end)
end
end
D.Log("[link] logging installed -- now shift-click an item in your bags")
end
-- ---------------------------------------------------------------------------
-- /wa timers -- what C_Timer offers a scheduler that has to retract a pending
-- callback. Two questions a type check cannot answer on its own: whether After
-- hands back any handle at all, and whether a Cancel that returns cleanly
-- actually suppresses the callback rather than just not erroring. Both are
-- settled behaviourally here, by scheduling one of each and reporting which
-- ones fired after the deadline has passed.
-- ---------------------------------------------------------------------------
function D.Timers()
if type(C_Timer) ~= "table" then
D.Log("[timers] C_Timer is " .. type(C_Timer) .. " -- nothing to probe")
return
end
D.Log("[timers] After=" .. type(C_Timer.After)
.. " NewTimer=" .. type(C_Timer.NewTimer)
.. " NewTicker=" .. type(C_Timer.NewTicker))
local fired = {}
local function shape(label, h)
local s = "[timers] " .. label .. " returned " .. type(h)
if type(h) == "table" then s = s .. " (Cancel=" .. type(h.Cancel) .. ")" end
D.Log(s)
end
if type(C_Timer.After) ~= "function" then
D.Log("[timers] no After -- the rest of this probe cannot run")
return
end
shape("After", C_Timer.After(1, function() fired.after = true end))
local cancelled
if type(C_Timer.NewTimer) == "function" then
shape("NewTimer", C_Timer.NewTimer(1, function() fired.newtimer = true end))
cancelled = C_Timer.NewTimer(1, function() fired.cancelled = true end)
end
if type(cancelled) == "table" and type(cancelled.Cancel) == "function" then
local ok, err = pcall(function() cancelled:Cancel() end)
D.Log("[timers] Cancel() " .. (ok and "returned cleanly" or ("errored: " .. tostring(err))))
else
D.Log("[timers] no cancellable handle -- a generation counter on the owner is the fallback")
end
local t0 = GetTime()
C_Timer.After(2, function()
D.Log(string.format("[timers] +%.2fs elapsed -- After fired=%s NewTimer fired=%s cancelled fired=%s",
GetTime() - t0,
tostring(fired.after and true or false),
tostring(fired.newtimer and true or false),
tostring(fired.cancelled and true or false)))
D.Log("[timers] a cancelled timer reading true means Cancel does not suppress the callback")
end)
D.Log("[timers] scheduled -- results in ~2s. No follow-up line at all means After never fired.")
end
-- ---------------------------------------------------------------------------
-- /wa cdtest -- settles: can we render a native cooldown swipe on this client?
-- CreateFrame("Cooldown", ...) throws "Unknown frame type" here, but in vanilla
-- the swipe is really a 3D Model, so the working constructor is a "Model" frame
-- inheriting CooldownFrameTemplate -- the technique CooldownTracker uses on this
-- exact client (../reference/CooldownTracker/CooldownTracker.lua:502). This
-- probes that path; if the spiral shows in-world, the Icon region can adopt it.
-- ---------------------------------------------------------------------------
local cdTestFrame
local cdTestFailed = false
-- Guarded with pcall so a client that lacks CooldownFrameTemplate/the Model type
-- re-logs the finding instead of erroring on every re-run.
function D.CooldownTest()
if cdTestFailed then
D.Log("[cdtest] native cooldown swipe is not available on this client -- already confirmed, see previous log line.")
return
end
if not cdTestFrame then
cdTestFrame = CreateFrame("Frame", nil, UIParent)
cdTestFrame:SetWidth(48); cdTestFrame:SetHeight(48)
cdTestFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 150)
local icon = cdTestFrame:CreateTexture(nil, "ARTWORK")
icon:SetAllPoints(cdTestFrame)
icon:SetTexture("Interface\\Icons\\Spell_Nature_LightningShield")
local ok, cooldown = pcall(CreateFrame, "Model", nil, cdTestFrame, "CooldownFrameTemplate")
if not ok or not cooldown then
cdTestFailed = true
D.Log("[cdtest] CreateFrame(\"Model\", ..., \"CooldownFrameTemplate\") failed: " .. tostring(cooldown) ..
" -- no native cooldown swipe on this client; Icon region stays text-only.")
return
end
cdTestFrame.cooldown = cooldown
-- The Model swipe underfills and sits bottom-left at scale 1; CooldownTracker
-- fixes it with SetScale((1/32)*iconSize) + a two-corner anchor (its
-- CalculateCooldownScale, line 505-512). iconSize here is 48.
cooldown:SetAllPoints(cdTestFrame)
cooldown:SetScale(48 / 32)
cooldown:ClearAllPoints()
cooldown:SetPoint("TOPLEFT", cdTestFrame, "TOPLEFT")
cooldown:SetPoint("BOTTOMRIGHT", cdTestFrame, "BOTTOMRIGHT")
D.Log("[cdtest] CreateFrame(\"Model\", ..., \"CooldownFrameTemplate\") succeeded, scaled 48/32 -- watch for the swipe.")
end
cdTestFrame:Show()
cdTestFrame.cooldown:Show()
CooldownFrame_SetTimer(cdTestFrame.cooldown, GetTime(), 10, 1)
D.Log("[cdtest] armed a 10s swipe on a test icon at CENTER,0,150 -- watch it in-world (not in this window). If it sweeps, the Model swipe works here.")
end
-- ---------------------------------------------------------------------------
-- /wa swipetest [sizes...] + /wa swipenudge <k> [yflat] -- fast, no-/reload
-- loop for tuning RegionPrototype.lua's SizeSwipe alignment constants.
-- swipetest spawns one real icon+swipe rig per size (default 16/32/64/128)
-- side by side so drift across sizes is visible in one screenshot; swipenudge
-- writes WA.regionPrototype.swipeNudgeK/swipeYFlat live and re-sizes every
-- active rig immediately, since SizeSwipe re-reads those fields on every call.
-- ---------------------------------------------------------------------------
local swipeTestRigs = {}
local swipeTestTicker
local SWIPE_TEST_DURATION = 6
-- Re-arms every active rig on a fresh 6s cycle -- CooldownFrame_SetTimer
-- doesn't loop on its own, so this ticker (period == duration) restarts each
-- swipe right as the previous one finishes.
local function swipeTestLoop()
for i = 1, table.getn(swipeTestRigs) do
local rig = swipeTestRigs[i]
WA.regionPrototype.ArmSwipe(rig.swipe, GetTime() + SWIPE_TEST_DURATION, SWIPE_TEST_DURATION)
end
end
function D.SwipeTest(sizesStr)
for i = table.getn(swipeTestRigs), 1, -1 do
swipeTestRigs[i]:Hide()
swipeTestRigs[i] = nil
end
if swipeTestTicker then swipeTestTicker:Cancel(); swipeTestTicker = nil end
if sizesStr == "0" then
D.Log("[swipetest] cleared")
return
end
-- Each entry is {w=, h=} -- a bare "64" means square (w=h=64), "64x32"
-- means non-square, for testing swipeStretchMode against a real W ~= H.
local sizes = {}
if sizesStr and sizesStr ~= "" then
local rest = sizesStr
while true do
local _, e, wStr, hStr = string.find(rest, "^%s*(%d+)x(%d+)")
if wStr then
table.insert(sizes, { w = tonumber(wStr), h = tonumber(hStr) })
rest = string.sub(rest, e + 1)
else
local _, e2, numStr = string.find(rest, "^%s*(%d+)")
if not numStr then break end
table.insert(sizes, { w = tonumber(numStr), h = tonumber(numStr) })
rest = string.sub(rest, e2 + 1)
end
end
end
if table.getn(sizes) == 0 then
sizes = { { w = 16, h = 16 }, { w = 32, h = 32 }, { w = 64, h = 64 }, { w = 128, h = 128 } }
end
-- Enrage (5229) instead of a hardcoded texture -- brighter icon, easier to
-- eyeball the swipe edge against than the dark LightningShield art.
local _, _, testIcon = GetSpellInfo(5229)
testIcon = testIcon or "Interface\\Icons\\Spell_Nature_LightningShield"
-- Edge-tracked, not "prev size + gap": a flat per-icon increment ignores
-- the NEXT icon's own half-width, so consecutive very-different sizes
-- (64 -> 128) would actually overlap instead of just looking cramped.
local gap = 30
local rightEdge = -300
for i = 1, table.getn(sizes) do
local w, h = sizes[i].w, sizes[i].h
local x = rightEdge + gap + w / 2
rightEdge = x + w / 2
local rig = CreateFrame("Frame", nil, UIParent)
rig:SetWidth(w); rig:SetHeight(h)
rig:SetPoint("CENTER", UIParent, "CENTER", x, 150)
local tex = rig:CreateTexture(nil, "ARTWORK")
tex:SetAllPoints(rig)
tex:SetTexture(testIcon)
tex:SetTexCoord(0.07, 0.93, 0.07, 0.93)
rig.swipe = WA.regionPrototype.CreateSwipe(rig)
WA.regionPrototype.SizeSwipe(rig.swipe, w, h)
WA.regionPrototype.ArmSwipe(rig.swipe, GetTime() + SWIPE_TEST_DURATION, SWIPE_TEST_DURATION)
if rig.swipe then rig.swipe:Show() end
local label = rig:CreateFontString(nil, "OVERLAY", "GameFontNormal")
label:SetPoint("TOP", rig, "BOTTOM", 0, -4)
label:SetText(w == h and (tostring(w) .. "px") or (tostring(w) .. "x" .. tostring(h)))
rig:Show()
table.insert(swipeTestRigs, rig)
end
swipeTestTicker = C_Timer.NewTicker(SWIPE_TEST_DURATION, swipeTestLoop)
D.Log("[swipetest] spawned " .. table.getn(swipeTestRigs) .. " rig(s) at CENTER,*,150, looping every " ..
SWIPE_TEST_DURATION .. "s -- /wa swipenudge <k> [yflat] to tune live, /wa swipetest (no args) to respawn defaults, /wa swipetest 0 to clear")
end
function D.SwipeNudge(rest)
local _, _, kStr, yStr = string.find(rest or "", "^(%S*)%s*(%S*)$")
local k = tonumber(kStr)
if not k then
D.Log(string.format("[swipenudge] current: swipeNudgeK=%s swipeYFlat=%s -- usage: /wa swipenudge <k> [yflat]",
tostring(WA.regionPrototype.swipeNudgeK), tostring(WA.regionPrototype.swipeYFlat)))
return
end
local y = tonumber(yStr)
WA.regionPrototype.swipeNudgeK = k
if y then WA.regionPrototype.swipeYFlat = y end
for i = 1, table.getn(swipeTestRigs) do
local rig = swipeTestRigs[i]
WA.regionPrototype.SizeSwipe(rig.swipe, rig:GetWidth(), rig:GetHeight())
end
D.Log(string.format("[swipenudge] swipeNudgeK=%s swipeYFlat=%s -- re-sized %d active rig(s)",
tostring(WA.regionPrototype.swipeNudgeK), tostring(WA.regionPrototype.swipeYFlat), table.getn(swipeTestRigs)))
end
-- ---------------------------------------------------------------------------
-- /wa track <spellName> -- settles: is the player's own duration/
-- expirationTime actually monotonic and does it reset cleanly on recast, or
-- does it exhibit the same flakiness DoiteAuras had to work around?
-- ---------------------------------------------------------------------------
local trackSpell
local trackTicker
local function trackTick()
if not trackSpell then return end
local found
for i = 1, MAX_SLOT do
local aura = C_UnitAuras.GetAuraDataByIndex("player", i, "HELPFUL")
if not aura then break end
if aura.name == trackSpell then
found = aura
break
end
end
if found then
local remain = -1
if found.expirationTime and found.expirationTime > 0 then
remain = found.expirationTime - GetTime()
end
D.Log(string.format("[track] t=%.1f %s stacks=%s dur=%s exp=%s remain=%.1f",
GetTime(), trackSpell, tostring(found.applications), tostring(found.duration),
tostring(found.expirationTime), remain))
else
D.Log(string.format("[track] t=%.1f %s NOT FOUND", GetTime(), trackSpell))
end
end
function D.Track(spellName)
if not spellName or spellName == "" then
trackSpell = nil
if trackTicker then trackTicker:Cancel(); trackTicker = nil end
D.Log("[track] stopped")
return
end
trackSpell = spellName
D.Log("[track] now tracking \"" .. spellName .. "\" (logging every 1s)")
if not trackTicker then
trackTicker = C_Timer.NewTicker(1, trackTick)
end
end
-- ---------------------------------------------------------------------------
-- /wa states <id> -- dumps triggerState[id]: the combination flags and every
-- trigger's per-clone states. The single most useful command for debugging the
-- state machine -- shows what the producers wrote and what the glue resolved.
-- ---------------------------------------------------------------------------
local STATE_FIELDS = {
"show", "changed", "active", "progressType", "name", "stacks", "duration",
"expirationTime", "value", "total", "spellId", "unit", "unitCaster",
"initialTime", "refreshTime", "stackGainTime", "stackLostTime",
}
local function dumpState(triggernum, cloneId, state)
local parts = {}
for i = 1, table.getn(STATE_FIELDS) do
local f = STATE_FIELDS[i]
if state[f] ~= nil then
local v = state[f]
if f == "expirationTime" and type(v) == "number" and v > 0 then
v = string.format("%s (rem %.1f)", tostring(v), v - GetTime())
end
table.insert(parts, f .. "=" .. tostring(v))
end
end
D.Log(string.format(" [%d][%q] %s", triggernum, cloneId, table.concat(parts, " ")))
end
function D.States(id)
if not id or id == "" then
D.Log("[states] usage: /wa states <aura id>")
return
end
local ts = WA.GetDisplayTriggerState and WA.GetDisplayTriggerState(id)
if not ts then
D.Log(string.format("[states] no triggerState for %q (unknown id, or a group)", id))
return
end
local ftCount = WA.regionPrototype.CountFrameTick and WA.regionPrototype.CountFrameTick() or 0
D.Log(string.format("--- states %q: show=%s disjunctive=%s numTriggers=%d triggerCount=%d activeMode=%s (global FrameTick subscribers: %d) ---",
id, tostring(ts.show), tostring(ts.disjunctive), ts.numTriggers, ts.triggerCount, tostring(ts.activeTriggerMode), ftCount))
for triggernum = 1, ts.numTriggers do
D.Log(string.format(" trigger %d: active=%s", triggernum, tostring(ts.triggers[triggernum])))
local allstates = ts[triggernum]
if allstates then
for cloneId, state in pairs(allstates) do
dumpState(triggernum, cloneId, state)
end
end
end
D.Log("--- end states ---")