-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMSA_UpdateEngine.lua
More file actions
2213 lines (1979 loc) · 107 KB
/
MSA_UpdateEngine.lua
File metadata and controls
2213 lines (1979 loc) · 107 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
-- ########################################################
-- MSA_UpdateEngine.lua (v6 - zero idle CPU)
--
-- Perf fixes vs v5:
-- * Removed blanket anyCooldownActive -> dirty loop
-- (was running engine at 10 Hz for ALL CDs even without
-- time-dependent visuals like glow/textcolor conditions)
-- * New needsTimerTick flag: engine only ticks when auras
-- with active CDs also have glow or textColor2 enabled
-- * Normal CDs = zero engine cost after initial render
-- (Blizzard's CooldownFrame handles swipe natively)
-- * AutoBuff/Charges already covered by autoBuffActive
--
-- Prior v5 fixes preserved:
-- * pcall closures eliminated - use pcall(f, a, b) directly
-- * GetTime() cached once per OnUpdate frame
-- * AutoBuffTick throttled to 10 Hz (was 60 Hz)
-- * Text/Stack style dirty-flagged via _msaStyleKey
-- * Glow remaining calc shares cached now-time
-- * db fetched once, passed through everywhere
-- ########################################################
local pairs, type, pcall, tonumber, tostring = pairs, type, pcall, tonumber, tostring
local GetTime = GetTime
local GetItemCooldown = GetItemCooldown
local GetItemIcon = GetItemIcon
local wipe = wipe or table.wipe
-----------------------------------------------------------
-- Constants
-----------------------------------------------------------
local THROTTLE_INTERVAL = 0.100 -- 10 Hz
-----------------------------------------------------------
-- Haste-scaled Auto Buff duration helper
-----------------------------------------------------------
local UnitSpellHaste = UnitSpellHaste
local function GetEffectiveBuffDuration(s)
local dur = tonumber(s and s.autoBuffDuration) or 10
if dur < 0.1 then dur = 0.1 end
if s and s.hasteScaling and UnitSpellHaste then
local h = tonumber(UnitSpellHaste("player")) or 0
if h > 0 then
dur = dur / (1 + h / 100)
end
end
return dur
end
-----------------------------------------------------------
-- Reminder threshold helper for BUFF_AURA
-- Returns true if aura should be HIDDEN (above threshold).
-- Safe: secret values -> never hide, absent -> never hide.
-----------------------------------------------------------
local _issv_engine = _G.issecretvalue
local function ShouldHideByThreshold(s, auraData, now)
if not s or not s.reminderThresholdMin then return false end
local thresh = tonumber(s.reminderThresholdMin)
if not thresh or thresh <= 0 then return false end
-- No aura data = absent -> never hide (show as reminder)
if not auraData then return false end
local exp = auraData.expirationTime
local dur = auraData.duration
-- Secret values -> can't check, always show
if _issv_engine then
if (exp and _issv_engine(exp)) or (dur and _issv_engine(dur)) then
return false
end
end
-- Permanent buff (duration=0) -> always show (no timer = always remind)
if not dur or dur == 0 then return false end
if not exp or exp == 0 then return false end
local remaining = exp - now
if remaining <= 0 then return false end -- expired -> show
-- Hide if remaining time is ABOVE threshold (buff still healthy)
return remaining > (thresh * 60)
end
-----------------------------------------------------------
-- Engine frame (hidden = zero CPU)
-----------------------------------------------------------
local engineFrame = CreateFrame("Frame", "MSWA_EngineFrame", UIParent)
engineFrame:Hide()
local dirty = false
local autoBuffActive = false
local needsTimerTick = false -- true ONLY when time-dependent visuals need per-tick updates
local lastFullUpdate = 0
local forceImmediate = false
local lastActiveCount = 0
-----------------------------------------------------------
-- Forward-declared
-----------------------------------------------------------
local MSWA_UpdateEventRegistration
-----------------------------------------------------------
-- Icon state cache
-----------------------------------------------------------
local iconCache = {}
local function WipeIconCache()
for i = 1, MSWA.MAX_ICONS do
iconCache[i] = nil
end
end
-----------------------------------------------------------
-- Item key cache (avoid string.format in hot loop)
-----------------------------------------------------------
local itemKeyCache = {}
local function GetItemKey(itemID)
local k = itemKeyCache[itemID]
if not k then
k = ("item:%d"):format(itemID)
itemKeyCache[itemID] = k
end
return k
end
-----------------------------------------------------------
-- PositionButton (top-level, zero closure allocation)
-----------------------------------------------------------
local function PositionButton(btn, s, key, idx, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
local gid = MSWA_GetAuraGroup and MSWA_GetAuraGroup(key) or (_G.GetAuraGroup and _G.GetAuraGroup(key) or nil)
local group = gid and db.groups and db.groups[gid] or nil
if group then
local gf = nil
if groupCtx then
groupCtx.used[gid] = true
if not groupCtx.applied[gid] and type(MSWA_ApplyGroupAnchorFrame) == "function" then
gf = MSWA_ApplyGroupAnchorFrame(gid, group)
groupCtx.frames[gid] = gf
groupCtx.applied[gid] = true
else
gf = groupCtx.frames[gid]
if not gf and type(MSWA_GetOrCreateGroupAnchorFrame) == "function" then
gf = MSWA_GetOrCreateGroupAnchorFrame(gid)
groupCtx.frames[gid] = gf
end
end
elseif type(MSWA_ApplyGroupAnchorFrame) == "function" then
gf = MSWA_ApplyGroupAnchorFrame(gid, group)
end
if not gf then gf = frame end
btn:SetPoint("CENTER", gf, "CENTER", (s and s.x or 0), (s and s.y or 0))
local size = group.size or ICON_SIZE
local w = (s and s.width) or size
local h = (s and s.height) or size
btn:SetSize(w, h)
if groupCtx then
local b = groupCtx.bounds[gid]
if not b then
b = { init = false, minL = 0, maxR = 0, minB = 0, maxT = 0 }
groupCtx.bounds[gid] = b
end
local x = (s and s.x) or 0
local y = (s and s.y) or 0
local halfW = w * 0.5
local halfH = h * 0.5
local left = x - halfW
local right = x + halfW
local bot = y - halfH
local top = y + halfH
if not b.init then
b.init = true
b.minL = left; b.maxR = right
b.minB = bot; b.maxT = top
else
if left < b.minL then b.minL = left end
if right > b.maxR then b.maxR = right end
if bot < b.minB then b.minB = bot end
if top > b.maxT then b.maxT = top end
end
end
else
local anchorFrame = MSWA_GetAnchorFrame(s or {})
local lx = s and s.x or 0
local ly = s and s.y or 0
if s and s.anchorFrame then
btn:SetPoint("CENTER", anchorFrame, "CENTER", lx, ly)
elseif s and s.x and s.y then
btn:SetPoint("CENTER", frame, "CENTER", lx, ly)
else
btn:SetPoint("LEFT", frame, "LEFT", (idx - 1) * (ICON_SIZE + ICON_SPACE), 0)
end
if s and s.width and s.height then
btn:SetSize(s.width, s.height)
else
btn:SetSize(ICON_SIZE, ICON_SIZE)
end
end
end
-----------------------------------------------------------
-- Inline helpers
-----------------------------------------------------------
local function ClearStackAndCount(btn)
if btn.count then btn.count:SetText(""); btn.count:Hide() end
if btn.stackText then btn.stackText:SetText(""); btn.stackText:Hide() end
end
-----------------------------------------------------------
-- Zero-count: keep item visible but grayed when count == 0
-- Returns true if zero-count gray was applied (additive)
-----------------------------------------------------------
local function IsItemZeroCount(s, itemID)
if not s or not s.showOnZeroCount or not itemID then return false end
if not GetItemCount then return false end
local cnt = GetItemCount(itemID, false, false)
return type(cnt) == "number" and cnt <= 0
end
local function HideButton(btn)
btn:Hide()
btn.icon:SetTexture(nil)
btn._msaCachedKey = nil
btn._msaStyleKey = nil
MSWA_ClearCooldownFrame(btn.cooldown)
MSWA_StopGlow(btn)
MSWA_HideReminderLabel(btn)
MSWA_HideChargeLabel(btn)
if MSWA_CleanupBar then MSWA_CleanupBar(btn) end
if btn._msaDecimalTimer then btn._msaDecimalTimer:Hide() end
btn.spellID = nil
end
-----------------------------------------------------------
-- SetIconTexture with cache (skip GetSpellInfo if same key)
-----------------------------------------------------------
local function SetIconTexture(btn, key)
if btn._msaCachedKey == key then return end
btn._msaCachedKey = key
btn.icon:SetTexture(MSWA_GetIconForKey(key))
end
-----------------------------------------------------------
-- Text/Stack style with dirty-flag (skip when key matches)
-- v5: Avoids redundant SetFont/SetTextColor/ClearAllPoints
-- per icon per frame when settings haven't changed.
-----------------------------------------------------------
local function ApplyStylesIfDirty(btn, db, s, key)
if btn._msaStyleKey == key then return end
btn._msaStyleKey = key
MSWA_ApplyTextStyle(btn, db, s)
MSWA_ApplyStackStyle_Fast(btn, s, db)
end
-----------------------------------------------------------
-- Alpha computation: cdAlpha, oocAlpha, combatAlpha
-----------------------------------------------------------
local function ComputeAlpha(s, isOnCD, inCombat)
local alpha = 1.0
if inCombat then
local ca = s and tonumber(s.combatAlpha)
if ca then alpha = alpha * ca end
else
local oa = s and tonumber(s.oocAlpha)
if oa then alpha = alpha * oa end
end
if isOnCD then
local cda = s and tonumber(s.cdAlpha)
if cda then alpha = alpha * cda end
else
-- Gamz: "When Ready" alpha (0% = hidden when spell available, 100% when on CD)
local ra = s and tonumber(s.readyAlpha)
if ra then alpha = alpha * ra end
end
return alpha
end
-----------------------------------------------------------
-- pcall helpers for secret-value comparison (no closure!)
-- v5: Named functions instead of pcall(function() ... end)
-----------------------------------------------------------
local function _itemCDCheck(start, duration)
if start and start > 0 and duration and duration > 1.5 then
return true
end
return false
end
local function _itemCDRemaining(start, duration, now)
if start and start > 0 and duration and duration > 1.5 then
local r = (start + duration) - now
return r > 0 and r or 0
end
return 0
end
-----------------------------------------------------------
-- UpdateSpells (the main hot loop)
-----------------------------------------------------------
local function MSWA_UpdateSpells()
local db = MSWA_GetDB()
local tracked = db.trackedSpells
local trackedItems = db.trackedItems or {}
local settingsTable = db.spellSettings or {}
local index = 1
local frame = MSWA.frame
local ICON_SIZE = MSWA.ICON_SIZE
local ICON_SPACE = MSWA.ICON_SPACE
local MAX_ICONS = MSWA.MAX_ICONS
local previewMode = MSWA.previewMode
local autoBuff = MSWA._autoBuff
local icons = MSWA.icons
-- v5: cache GetTime once for entire update
local now = GetTime()
-- Group anchors - reuse tables to avoid churn
local groupCtx = MSWA._groupLayoutCtx
if not groupCtx then
groupCtx = { applied = {}, frames = {}, bounds = {}, used = {} }
MSWA._groupLayoutCtx = groupCtx
end
wipe(groupCtx.applied)
wipe(groupCtx.bounds)
wipe(groupCtx.used)
local optFrame = MSWA.optionsFrame
local selectedKey = (optFrame and optFrame:IsShown() and MSWA.selectedSpellID) or nil
-- Cache API availability once
local hasGetCD = C_Spell and C_Spell.GetSpellCooldown
local hasGetCDRemaining = C_Spell and C_Spell.GetSpellCooldownRemaining
-- Cache combat/encounter state ONCE
local inCombat = InCombatLockdown and InCombatLockdown() and true or false
local inEncounter = IsEncounterInProgress and IsEncounterInProgress() and true or false
-- v6: track inline (eliminates post-loop iterations)
-- foundNeedsTimerTick = true when ANY visible aura needs per-tick updates
-- (autobuff/charges ticking, glow conditions, conditional text color)
local foundCooldownActive = false
local foundAutoBuffActive = false
local foundNeedsTimerTick = false
-----------------------------------------------------------
-- 1) Spells
-----------------------------------------------------------
if hasGetCD then
for trackedKey, enabled in pairs(tracked) do
if index > MAX_ICONS then break end
if enabled then
local spellID
local itemFromSpells -- item instance keys (item:ID:N) stored in trackedSpells
if type(trackedKey) == "number" then
spellID = trackedKey
elseif MSWA_IsSpellInstanceKey(trackedKey) then
spellID = MSWA_KeyToSpellID(trackedKey)
elseif MSWA_IsItemKey(trackedKey) then
itemFromSpells = MSWA_KeyToItemID(trackedKey)
end
if spellID then
local key = trackedKey
local s = settingsTable[key] or settingsTable[tostring(key)]
local shouldLoad = MSWA_ShouldLoadAura(s, inCombat, inEncounter)
if shouldLoad or previewMode or key == selectedKey then
local btn = icons[index]
SetIconTexture(btn, key)
btn:Show()
btn.spellID = key
btn:ClearAllPoints()
ApplyStylesIfDirty(btn, db, s, key)
-- Clean stale overlays from mode switches (zero cost if nil)
if (not s or s.auraMode ~= "REMINDER_BUFF") and btn._msaReminderLabel then btn._msaReminderLabel:Hide() end
if (not s or s.auraMode ~= "CHARGES") and btn._msaChargeLabel then btn._msaChargeLabel:Hide() end
if s and (s.auraMode == "AUTOBUFF" or s.auraMode == "BUFF_THEN_CD") then
-- ========== SPELL AUTO BUFF / BUFF_THEN_CD MODE ==========
local isBuffThenCD = (s.auraMode == "BUFF_THEN_CD")
local ab = autoBuff[key]
local buffDur = GetEffectiveBuffDuration(s)
local buffDelay = tonumber(s.autoBuffDelay) or 0
local timerStart = ab and (ab.startTime + buffDelay) or 0
local inBuffPhase = false
if ab and ab.active then
local totalWindow = buffDelay + buffDur
if (now - ab.startTime) < totalWindow then
inBuffPhase = true
foundAutoBuffActive = true; foundNeedsTimerTick = true
else
ab.active = false
end
end
if inBuffPhase then
-- === BUFF PHASE (identical for AUTOBUFF & BUFF_THEN_CD) ===
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ApplyCooldownFrame(btn.cooldown, timerStart, buffDur, 1)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, true, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
local glowRem = buffDur - (now - timerStart)
if glowRem < 0 then glowRem = 0 end
local gs = s and s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, glowRem, glowRem > 0)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowRem, glowRem > 0)
MSWA_ApplySwipeDarken_Fast(btn, s)
foundCooldownActive = true
index = index + 1
elseif isBuffThenCD then
-- === BUFF_THEN_CD: buff expired -> show remaining spell CD ===
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
local cdInfo = C_Spell.GetSpellCooldown(spellID)
if cdInfo then
local exp = cdInfo.expirationTime
if hasGetCDRemaining then
local rem = C_Spell.GetSpellCooldownRemaining(spellID)
if type(rem) == "number" then
exp = now + rem
end
end
MSWA_ApplyCooldownFrame(btn.cooldown, cdInfo.startTime, cdInfo.duration, cdInfo.modRate, exp)
else
MSWA_ClearCooldownFrame(btn.cooldown)
end
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
local onCD = MSWA_IsCooldownActive(btn)
if onCD then foundCooldownActive = true end
MSWA_CheckSoundTransition(key, onCD, s)
if onCD then
if s.grayOnCooldown then
btn.icon:SetDesaturated(true)
else
btn.icon:SetDesaturated(false)
end
btn:SetAlpha(ComputeAlpha(s, true, inCombat))
local rem = 0
local gs2 = s.glow
if (gs2 and gs2.enabled) or s.textColor2Enabled then
foundNeedsTimerTick = true
local r = select(1, MSWA_GetSpellGlowRemaining(spellID))
if type(r) == "number" and r > 0 then
rem = r
end
end
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, rem, true)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, rem, true)
MSWA_ApplySwipeDarken_Fast(btn, s)
index = index + 1
elseif previewMode or key == selectedKey then
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
MSWA_StopGlow(btn)
index = index + 1
else
-- BUFF_THEN_CD: CD ready -> keep visible idle
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
MSWA_StopGlow(btn)
index = index + 1
end
elseif previewMode or key == selectedKey then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
MSWA_StopGlow(btn)
index = index + 1
else
HideButton(btn)
end
elseif s and s.auraMode == "REMINDER_BUFF" then
-- ========== REMINDER BUFF MODE ==========
-- Inverted AUTOBUFF: show alert when buff MISSING,
-- optionally show timer when buff active.
-- 100% secret-safe: reuses AUTOBUFF cast-detection
-- (UNIT_SPELLCAST_SUCCEEDED) + user-supplied duration.
-- Zero API comparisons.
local ab = autoBuff[key]
local buffDur = GetEffectiveBuffDuration(s)
local buffDelay = tonumber(s.autoBuffDelay) or 0
local inBuffPhase = false
if ab and ab.active then
local totalWindow = buffDelay + buffDur
if (now - ab.startTime) < totalWindow then
inBuffPhase = true
foundAutoBuffActive = true; foundNeedsTimerTick = true
else
ab.active = false
end
end
if not inBuffPhase then
-- BUFF MISSING -> show reminder
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
ClearStackAndCount(btn)
MSWA_ShowReminderLabel(btn, s, db)
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, 9999, true)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, 0, false)
index = index + 1
elseif s.reminderShowTimer then
-- BUFF ACTIVE + show countdown timer
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
local timerStart = ab.startTime + buffDelay
MSWA_ApplyCooldownFrame(btn.cooldown, timerStart, buffDur, 1)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, true, inCombat))
ClearStackAndCount(btn)
MSWA_HideReminderLabel(btn)
local glowRem = buffDur - (now - timerStart)
if glowRem < 0 then glowRem = 0 end
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, glowRem, glowRem > 0)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowRem, glowRem > 0)
MSWA_ApplySwipeDarken_Fast(btn, s)
foundCooldownActive = true
index = index + 1
elseif previewMode or key == selectedKey then
-- Preview: show idle
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_ShowReminderLabel(btn, s, db)
MSWA_StopGlow(btn)
index = index + 1
else
-- BUFF ACTIVE + hide reminder
HideButton(btn)
end
elseif s and s.auraMode == "CHARGES" then
-- ========== SPELL CHARGES MODE ==========
-- User-defined charges: cast consumes, timer recharges.
-- 100% secret-safe - zero API reads for charge state.
MSWA._charges = MSWA._charges or {}
local maxC = tonumber(s.chargeMax) or 3
local ch = MSWA._charges[key]
if not ch then
ch = { remaining = maxC, rechargeStart = 0 }
MSWA._charges[key] = ch
end
local forceShow = s.chargeForceShow
local recharging = MSWA_ChargeRechargeTick(key, s, now)
local rem = ch.remaining
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
-- Cooldown swipe: show recharge timer (skip in forceShow)
if not forceShow and recharging and ch.rechargeStart > 0 then
local dur = tonumber(s.chargeDuration) or 0
MSWA_ApplyCooldownFrame(btn.cooldown, ch.rechargeStart, dur, 1)
foundCooldownActive = true
else
MSWA_ClearCooldownFrame(btn.cooldown)
end
-- forceShow: always normal icon, full alpha
if forceShow then
btn.icon:SetDesaturated(false)
btn:SetAlpha(1)
else
btn.icon:SetDesaturated(rem <= 0)
btn:SetAlpha(ComputeAlpha(s, recharging, inCombat))
end
-- Charge counter label
MSWA_ShowChargeCount(btn, rem, maxC, s, db)
ClearStackAndCount(btn)
-- Glow
local glowRem = 0
if recharging and ch.rechargeStart > 0 then
local dur = tonumber(s.chargeDuration) or 0
glowRem = dur - (now - ch.rechargeStart)
if glowRem < 0 then glowRem = 0 end
end
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, glowRem, recharging)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowRem, recharging)
if not forceShow and recharging then
MSWA_ApplySwipeDarken_Fast(btn, s)
foundAutoBuffActive = true; foundNeedsTimerTick = true
end
index = index + 1
elseif s and s.auraMode == "BUFF_AURA" then
-- ========== BUFF AURA MODE (direct poll, like WeakAuras/EQoL) ==========
-- Uses GetPlayerAuraBySpellID -> nil = absent, table = active
-- issecretvalue pattern from EQoL for field access
local buffSID = s.auraSpellID or spellID
local auraData = MSWA_GetPlayerAuraDataBySpellID(buffSID)
local buffActive = (auraData ~= nil)
local showWhenAbsent = s.showWhenAbsent
local showMe = buffActive or showWhenAbsent or previewMode or key == selectedKey
-- Reminder threshold: hide if buff is healthy (remaining > threshold)
if showMe and buffActive and ShouldHideByThreshold(s, auraData, now) then
showMe = false
end
if showMe then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
if buffActive then
-- Cooldown sweep (EQoL pattern):
-- Secret fields -> SetCooldownFromExpirationTime (designed for secrets)
-- Non-secret fields -> read normally, use SetCooldown for precision
-- Duration 0 (permanent buffs like poisons) -> no sweep needed
local cd = btn.cooldown
if cd then
local dur = auraData.duration
local exp = auraData.expirationTime
local isSecret = MSWA_IsSecretValue and (MSWA_IsSecretValue(dur) or MSWA_IsSecretValue(exp))
if isSecret then
-- Secret: pass directly to Blizzard API
if cd.SetCooldownFromExpirationTime then
cd:SetCooldownFromExpirationTime(exp, dur, auraData.timeMod)
cd.__mswaSet = true
end
elseif dur and dur > 0 and exp then
-- Non-secret with duration: normal cooldown
MSWA_ApplyCooldownFrame(cd, exp - dur, dur, auraData.timeMod or 1, exp)
else
-- Permanent buff (duration=0): no sweep
MSWA_ClearCooldownFrame(cd)
end
end
-- Stacks (v6: use styled target, respect hideStacksOnCooldown)
if s.showStacks ~= false and not (s.hideStacksOnCooldown and MSWA_IsCooldownActive(btn)) then
local sText = MSWA_GetAuraStackText(auraData, 2)
local sTarget = btn.stackText or btn.count
if sText and sTarget then
sTarget:SetText(sText); sTarget:Show()
else
ClearStackAndCount(btn)
end
else
ClearStackAndCount(btn)
end
btn.icon:SetDesaturated(false)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
else
-- Absent state
MSWA_ClearCooldownFrame(btn.cooldown)
ClearStackAndCount(btn)
btn.icon:SetDesaturated(s.desaturateOnAbsent ~= false)
btn:SetAlpha(tonumber(s.alphaOnAbsent) or 0.45)
end
-- Glow: active/absent
local glowVal = buffActive and 9999 or 0
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, glowVal, buffActive)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowVal, buffActive)
if btn._msaReminderLabel then btn._msaReminderLabel:Hide() end
if btn._msaChargeLabel then btn._msaChargeLabel:Hide() end
index = index + 1
else
HideButton(btn)
end
else
-- ========== NORMAL SPELL MODE ==========
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
local cdInfo = C_Spell.GetSpellCooldown(spellID)
if cdInfo then
local exp = cdInfo.expirationTime
if hasGetCDRemaining then
local rem = C_Spell.GetSpellCooldownRemaining(spellID)
if type(rem) == "number" then
exp = now + rem
end
end
MSWA_ApplyCooldownFrame(btn.cooldown, cdInfo.startTime, cdInfo.duration, cdInfo.modRate, exp)
else
MSWA_ClearCooldownFrame(btn.cooldown)
end
MSWA_UpdateBuffVisual_Fast(btn, s, spellID, false, nil)
local onCD = MSWA_IsCooldownActive(btn)
if onCD then foundCooldownActive = true end
MSWA_CheckSoundTransition(key, onCD, s)
if s and s.grayOnCooldown then
btn.icon:SetDesaturated(onCD)
else
btn.icon:SetDesaturated(false)
end
local rem = 0
if onCD and s then
local gs2 = s.glow
if (gs2 and gs2.enabled) or s.textColor2Enabled then
foundNeedsTimerTick = true
local r = select(1, MSWA_GetSpellGlowRemaining(spellID))
if type(r) == "number" and r > 0 then
rem = r
end
end
end
btn:SetAlpha(ComputeAlpha(s, onCD, inCombat))
local gs = s and s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, rem, onCD)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, rem, onCD)
MSWA_ApplySwipeDarken_Fast(btn, s)
index = index + 1
end
end
elseif itemFromSpells then
-- ========== ITEM INSTANCE (item:ID:N in trackedSpells) ==========
local itemID = itemFromSpells
local key = trackedKey
local s = settingsTable[key] or settingsTable[tostring(key)]
local shouldLoad = MSWA_ShouldLoadAura(s, inCombat, inEncounter)
if shouldLoad or previewMode or key == selectedKey then
local btn = icons[index]
SetIconTexture(btn, key)
btn:Show()
btn.spellID = key
btn:ClearAllPoints()
ApplyStylesIfDirty(btn, db, s, key)
-- Clean stale overlays from mode switches (zero cost if nil)
if (not s or s.auraMode ~= "REMINDER_BUFF") and btn._msaReminderLabel then btn._msaReminderLabel:Hide() end
if (not s or s.auraMode ~= "CHARGES") and btn._msaChargeLabel then btn._msaChargeLabel:Hide() end
if s and s.auraMode == "BUFF_AURA" then
-- ========== ITEM INSTANCE: BUFF AURA (direct poll) ==========
local buffSID = s.auraSpellID or itemID
local auraData = MSWA_GetPlayerAuraDataBySpellID(buffSID)
local buffActive = (auraData ~= nil)
local showMe = buffActive or s.showWhenAbsent or previewMode or key == selectedKey
-- Reminder threshold: hide if buff is healthy (remaining > threshold)
if showMe and buffActive and ShouldHideByThreshold(s, auraData, now) then
showMe = false
end
if showMe then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
if buffActive then
local cd = btn.cooldown
if cd then
local dur = auraData.duration
local exp = auraData.expirationTime
local isSecret = MSWA_IsSecretValue and (MSWA_IsSecretValue(dur) or MSWA_IsSecretValue(exp))
if isSecret and cd.SetCooldownFromExpirationTime then
cd:SetCooldownFromExpirationTime(exp, dur, auraData.timeMod); cd.__mswaSet = true
elseif dur and dur > 0 and exp then
MSWA_ApplyCooldownFrame(cd, exp - dur, dur, auraData.timeMod or 1, exp)
else MSWA_ClearCooldownFrame(cd) end
end
if s.showStacks ~= false and not (s.hideStacksOnCooldown and MSWA_IsCooldownActive(btn)) then
local sText = MSWA_GetAuraStackText(auraData, 2)
local sTarget = btn.stackText or btn.count
if sText and sTarget then sTarget:SetText(sText); sTarget:Show() else ClearStackAndCount(btn) end
else ClearStackAndCount(btn) end
btn.icon:SetDesaturated(false); btn:SetAlpha(ComputeAlpha(s, false, inCombat))
else
MSWA_ClearCooldownFrame(btn.cooldown); ClearStackAndCount(btn)
btn.icon:SetDesaturated(s.desaturateOnAbsent ~= false); btn:SetAlpha(tonumber(s.alphaOnAbsent) or 0.45)
end
local gs = s.glow; local glowVal = buffActive and 9999 or 0
if gs and gs.enabled then MSWA_UpdateGlow_Fast(btn, gs, glowVal, buffActive)
elseif btn._msaGlowActive then MSWA_StopGlow(btn) end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowVal, buffActive)
if btn._msaReminderLabel then btn._msaReminderLabel:Hide() end
if btn._msaChargeLabel then btn._msaChargeLabel:Hide() end
index = index + 1
else HideButton(btn) end
elseif s and (s.auraMode == "AUTOBUFF" or s.auraMode == "BUFF_THEN_CD") then
-- ========== ITEM INSTANCE: AUTO BUFF / BUFF_THEN_CD ==========
local isBuffThenCD = (s.auraMode == "BUFF_THEN_CD")
local ab = autoBuff[key]
local buffDur = GetEffectiveBuffDuration(s)
local buffDelay = tonumber(s.autoBuffDelay) or 0
local timerStart = ab and (ab.startTime + buffDelay) or 0
local inBuffPhase = false
if ab and ab.active then
local totalWindow = buffDelay + buffDur
if (now - ab.startTime) < totalWindow then
inBuffPhase = true
foundAutoBuffActive = true; foundNeedsTimerTick = true
else
ab.active = false
end
end
if inBuffPhase then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ApplyCooldownFrame(btn.cooldown, timerStart, buffDur, 1)
btn.icon:SetDesaturated(false)
if IsItemZeroCount(s, itemID) then btn.icon:SetDesaturated(true) end
btn:SetAlpha(ComputeAlpha(s, true, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
local glowRem = buffDur - (now - timerStart)
if glowRem < 0 then glowRem = 0 end
local gs = s and s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, glowRem, glowRem > 0)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, glowRem, glowRem > 0)
MSWA_ApplySwipeDarken_Fast(btn, s)
foundCooldownActive = true
index = index + 1
elseif isBuffThenCD then
-- === BUFF_THEN_CD: buff expired -> show remaining item CD ===
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
if GetItemCooldown then
local iStart, iDuration = GetItemCooldown(itemID)
local ok, onCD = pcall(_itemCDCheck, iStart, iDuration)
if ok and onCD then
MSWA_ApplyCooldownFrame(btn.cooldown, iStart, iDuration, 1)
else
MSWA_ClearCooldownFrame(btn.cooldown)
end
else
MSWA_ClearCooldownFrame(btn.cooldown)
end
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
local onCD = MSWA_IsCooldownActive(btn)
if onCD then foundCooldownActive = true end
MSWA_CheckSoundTransition(key, onCD, s)
if onCD then
if s.grayOnCooldown then
btn.icon:SetDesaturated(true)
else
btn.icon:SetDesaturated(false)
end
if IsItemZeroCount(s, itemID) then btn.icon:SetDesaturated(true) end
btn:SetAlpha(ComputeAlpha(s, true, inCombat))
local rem = 0
local need = (s.glow and s.glow.enabled) or s.textColor2Enabled
if need then foundNeedsTimerTick = true end
if need and GetItemCooldown then
local st, dur = GetItemCooldown(itemID)
local ok2, r = pcall(_itemCDRemaining, st, dur, now)
if ok2 and type(r) == "number" then
rem = r
end
end
local gs = s.glow
if gs and gs.enabled then
MSWA_UpdateGlow_Fast(btn, gs, rem, true)
elseif btn._msaGlowActive then
MSWA_StopGlow(btn)
end
MSWA_ApplyConditionalTextColor_Fast(btn, s, db, rem, true)
MSWA_ApplySwipeDarken_Fast(btn, s)
index = index + 1
elseif previewMode or key == selectedKey then
btn.icon:SetDesaturated(false)
if IsItemZeroCount(s, itemID) then btn.icon:SetDesaturated(true) end
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
MSWA_StopGlow(btn)
index = index + 1
else
-- BUFF_THEN_CD: CD ready -> keep visible idle
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
if IsItemZeroCount(s, itemID) then btn.icon:SetDesaturated(true) end
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
MSWA_StopGlow(btn)
index = index + 1
end
elseif previewMode or key == selectedKey then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(false)
if IsItemZeroCount(s, itemID) then btn.icon:SetDesaturated(true) end
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
MSWA_StopGlow(btn)
index = index + 1
else
if IsItemZeroCount(s, itemID) then
PositionButton(btn, s, key, index, frame, ICON_SIZE, ICON_SPACE, db, groupCtx)
MSWA_ClearCooldownFrame(btn.cooldown)
btn.icon:SetDesaturated(true)
btn:SetAlpha(ComputeAlpha(s, false, inCombat))
MSWA_UpdateBuffVisual_Fast(btn, s, nil, true, itemID)
MSWA_StopGlow(btn)
index = index + 1
else
HideButton(btn)
end
end
elseif s and s.auraMode == "REMINDER_BUFF" then
-- ========== ITEM INSTANCE: REMINDER BUFF ==========
local ab = autoBuff[key]
local buffDur = GetEffectiveBuffDuration(s)
local buffDelay = tonumber(s.autoBuffDelay) or 0
local inBuffPhase = false
if ab and ab.active then