-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMSA_SpellAPI.lua
More file actions
888 lines (772 loc) · 31.6 KB
/
MSA_SpellAPI.lua
File metadata and controls
888 lines (772 loc) · 31.6 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
-- ########################################################
-- MSA_SpellAPI.lua (v4 - max performance rewrite)
--
-- Rules:
-- * pcall ONLY for Midnight secret-value APIs
-- * Font paths cached - zero pcall in hot path
-- * CD API detected once at load time
-- * All hot-path helpers accept (db, s) - no redundant lookups
-- * v4: pcall closures eliminated - named funcs only
-- * v4: MSWA_ApplyStackStyle_Fast takes db param
-- * v4: SwipeDarken dirty-flagged
-- ########################################################
local type, tostring, tonumber, select = type, tostring, tonumber, select
local pcall = pcall
local GetTime = GetTime
local wipe = wipe or table.wipe
local GetItemCooldown = GetItemCooldown
local GetItemCount = GetItemCount
-----------------------------------------------------------
-- Spell info (non-secret, no pcall needed)
-----------------------------------------------------------
function MSWA_GetSpellInfo(spellID)
if not spellID then return nil end
if C_Spell and C_Spell.GetSpellInfo then
return C_Spell.GetSpellInfo(spellID)
end
return nil
end
function MSWA_GetSpellName(spellID)
local info = MSWA_GetSpellInfo(spellID)
return info and info.name
end
function MSWA_GetSpellIcon(spellID)
local info = MSWA_GetSpellInfo(spellID)
return info and info.iconID
end
function MSWA_GetSpellCooldown(spellID)
if not spellID then return nil end
if C_Spell and C_Spell.GetSpellCooldown then
return C_Spell.GetSpellCooldown(spellID)
end
return nil
end
-----------------------------------------------------------
-- Font path cache (zero pcall in hot path)
-----------------------------------------------------------
local fontPathCache = {}
local defaultFontPath
-- Built-in font keys -> paths (matches MSWA_RebuildFontChoices in MSA_Icons)
local BUILTIN_FONT_PATHS = {
FRIZQT = "Fonts\\FRIZQT__.TTF",
ARIALN = "Fonts\\ARIALN.TTF",
MORPHEUS = "Fonts\\MORPHEUS.TTF",
SKURRI = "Fonts\\SKURRI.TTF",
}
local function GetDefaultFontPath()
if not defaultFontPath then
if GameFontNormal and GameFontNormal.GetFont then
defaultFontPath = select(1, GameFontNormal:GetFont())
end
defaultFontPath = defaultFontPath or "Fonts\\FRIZQT__.TTF"
end
return defaultFontPath
end
function MSWA_GetFontPathFromKey(fontKey)
if not fontKey or fontKey == "DEFAULT" then
return GetDefaultFontPath()
end
local cached = fontPathCache[fontKey]
if cached then return cached end
-- Built-in font keys (FRIZQT, ARIALN, MORPHEUS, SKURRI)
local builtin = BUILTIN_FONT_PATHS[fontKey]
if builtin then
fontPathCache[fontKey] = builtin
return builtin
end
-- SharedMedia lookup (pcall only here, cached forever)
local LSM = MSWA.LSM
if LSM and LSM.Fetch then
local ok, path = pcall(LSM.Fetch, LSM, "font", fontKey)
if ok and path then
fontPathCache[fontKey] = path
return path
end
end
-- Last resort: check MSWA.fontChoices (covers any future additions)
local choices = MSWA.fontChoices
if choices then
for i = 1, #choices do
local c = choices[i]
if c.key == fontKey and c.path then
fontPathCache[fontKey] = c.path
return c.path
end
end
end
local def = GetDefaultFontPath()
fontPathCache[fontKey] = def
return def
end
function MSWA_InvalidateFontCache()
for k in pairs(fontPathCache) do fontPathCache[k] = nil end
defaultFontPath = nil
end
-----------------------------------------------------------
-- Cooldown frame: detect API once, single pcall apply
-----------------------------------------------------------
local cdAPIDetected = false
local cdHasExpTime = false
local cdHasSetCD = false
local function DetectCDAPI()
if cdAPIDetected then return end
cdAPIDetected = true
local testCD = CreateFrame("Cooldown", nil, UIParent, "CooldownFrameTemplate")
cdHasExpTime = testCD.SetCooldownFromExpirationTime ~= nil
cdHasSetCD = testCD.SetCooldown ~= nil
testCD:Hide()
end
function MSWA_ClearCooldownFrame(cd)
if not cd then return end
cd.__mswaSet = false
cd.__mswaExp = nil
cd.__mswaStart = nil
cd.__mswaDur = nil
if cd.Clear then
cd:Clear()
elseif CooldownFrame_Clear then
CooldownFrame_Clear(cd)
elseif cd.SetCooldown then
cd:SetCooldown(0, 0)
end
end
function MSWA_ClearCooldown(btn)
if btn and btn.cooldown then
MSWA_ClearCooldownFrame(btn.cooldown)
end
end
-- Single pcall per icon. Secret values pass straight through to Blizzard.
-- Stores timing on cd frame for decimal timer readback.
function MSWA_ApplyCooldownFrame(cd, startTime, duration, modRate, expirationTime)
if not cd then return end
DetectCDAPI()
-- NOTE (Midnight/Beta): startTime/duration can be "secret" values.
-- NEVER do arithmetic on them here. We store raw timing and only
-- compute derived values behind an issecretvalue guard.
local _issv = _G.issecretvalue
if cdHasExpTime and expirationTime ~= nil and duration ~= nil then
local ok = pcall(cd.SetCooldownFromExpirationTime, cd, expirationTime, duration, modRate)
if ok then
cd.__mswaSet = true
cd.__mswaExp = expirationTime
cd.__mswaStart = nil
cd.__mswaDur = duration
return
end
end
if cdHasSetCD and startTime ~= nil and duration ~= nil then
local ok = pcall(cd.SetCooldown, cd, startTime, duration, modRate)
if ok then
cd.__mswaSet = true
cd.__mswaStart = startTime
cd.__mswaDur = duration
-- Best-effort: only cache expiration if we can prove values are non-secret.
if expirationTime ~= nil and (not _issv or not _issv(expirationTime)) then
cd.__mswaExp = expirationTime
elseif _issv and (not _issv(startTime) and not _issv(duration)) then
cd.__mswaExp = startTime + duration
else
cd.__mswaExp = nil
end
return
end
end
MSWA_ClearCooldownFrame(cd)
end
-----------------------------------------------------------
-- Aura / Charges - Midnight 12.0 secret-safe
--
-- Pattern from EQoL:
-- PRIMARY: C_UnitAuras.GetUnitAuras("player","HELPFUL")
-- -> returns whitelisted table with READABLE fields
-- FALLBACK: C_UnitAuras.GetPlayerAuraBySpellID(sid)
--
-- GetAuraDataBySpellID does NOT exist in Midnight.
-- GetAuraDataByAuraInstanceID returns ALL fields secret.
-- GetUnitAuras returns WHITELISTED readable fields.
-----------------------------------------------------------
local hasGetUnitAuras = C_UnitAuras and C_UnitAuras.GetUnitAuras
local hasGetPlayerAura = C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID
local hasGetCDAura = C_UnitAuras and C_UnitAuras.GetCooldownAuraBySpellID
local hasGetAuraCount = C_UnitAuras and C_UnitAuras.GetAuraApplicationDisplayCount
local hasGetCharges = C_Spell and C_Spell.GetSpellCharges
local hasTruncZero = C_StringUtil and C_StringUtil.TruncateWhenZero
local _issecretvalue = _G.issecretvalue
-- Per-frame cache: GetUnitAuras is called once, result shared
-- across all BUFF_AURA icons in the same frame
local _buffCache = {} -- [spellID] = auraData or false
local _buffCacheFrame = -1 -- GetTime() of last cache build
local function RebuildBuffCache()
local now = GetTime()
if now == _buffCacheFrame then return end
_buffCacheFrame = now
wipe(_buffCache)
if not hasGetUnitAuras then return end
-- EQoL pattern: GetUnitAuras returns all HELPFUL auras
-- with whitelisted/readable fields
local auras = C_UnitAuras.GetUnitAuras("player", "HELPFUL")
if type(auras) ~= "table" then return end
for i = 1, #auras do
local a = auras[i]
if a then
local sid = a.spellId
-- Check if spellId is readable (not secret)
if sid and not (_issecretvalue and _issecretvalue(sid)) then
-- First match wins (don't overwrite)
if not _buffCache[sid] then
_buffCache[sid] = a
end
end
end
end
end
-- Invalidate cache (called from PlayerBuffs on UNIT_AURA)
function MSWA_InvalidateBuffCache()
_buffCacheFrame = -1
end
function MSWA_GetPlayerAuraDataBySpellID(spellID)
if not spellID then return nil end
-- Primary: GetUnitAuras cache (EQoL Midnight pattern)
if hasGetUnitAuras then
RebuildBuffCache()
local cached = _buffCache[spellID]
if cached then return cached end
end
-- Fallback: GetPlayerAuraBySpellID (works for some buffs)
if hasGetPlayerAura then
local data = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
if data then return data end
end
-- Last resort: GetCooldownAuraBySpellID (CD-only auras)
if hasGetCDAura then
local ok, data = pcall(C_UnitAuras.GetCooldownAuraBySpellID, spellID)
if ok and type(data) == "table" then return data end
end
return nil
end
-- Safe field read: uses issecretvalue to check before reading
function MSWA_SafeAuraField(auraData, fieldName)
if not auraData then return nil end
local val = auraData[fieldName]
if val == nil then return nil end
if _issecretvalue and _issecretvalue(val) then return nil end
return val
end
-- Returns true if a field is secret
function MSWA_IsSecretValue(val)
if val == nil then return false end
if _issecretvalue and _issecretvalue(val) then return true end
return false
end
-- Stack text: EQoL uses (unitToken, instanceID, min, max) signature
function MSWA_GetAuraStackText(auraData, minCount)
if not auraData or not hasGetAuraCount then return nil end
minCount = minCount or 2
-- EQoL signature: GetAuraApplicationDisplayCount(unit, instanceID, min, max)
local instanceID = auraData.auraInstanceID
if instanceID then
local ok, s = pcall(C_UnitAuras.GetAuraApplicationDisplayCount,
"player", instanceID, minCount, 1000)
if ok and s then return tostring(s) end
end
-- Fallback: read applications directly if not secret
local apps = auraData.applications
if apps and not (_issecretvalue and _issecretvalue(apps)) then
if apps >= minCount then return tostring(apps) end
end
return nil
end
function MSWA_GetSpellChargesText(spellID)
if not spellID or not hasGetCharges then return nil end
local ok, info = pcall(C_Spell.GetSpellCharges, spellID)
if not ok or type(info) ~= "table" then return nil end
local cur = info.currentCharges or info.charges
if hasTruncZero then
local ok2, s = pcall(C_StringUtil.TruncateWhenZero, cur)
if ok2 and type(s) == "string" then return s end
end
if cur ~= nil then return tostring(cur) end
return nil
end
-----------------------------------------------------------
-- Glow remaining: named pcall helpers (v4: no closures)
-----------------------------------------------------------
local hasGetRemaining = C_Spell and C_Spell.GetSpellCooldownRemaining
-- v4: Named function for pcall - eliminates closure allocation
local function _spellCDRemaining(cdInfo)
local st = cdInfo.startTime
local dur = cdInfo.duration
if st <= 0 or dur <= 1.5 then return 0 end
return (st + dur) - GetTime()
end
-- Spell cooldown values are tainted in Midnight - pcall required for comparisons.
-- Returns (remaining, isOnCooldown).
function MSWA_GetSpellGlowRemaining(spellID)
if not spellID then return 0, false end
if not (C_Spell and C_Spell.GetSpellCooldown) then return 0, false end
local cdInfo = C_Spell.GetSpellCooldown(spellID)
if not cdInfo then return 0, false end
-- v4: pcall on named function, not anonymous closure
local ok, remaining = pcall(_spellCDRemaining, cdInfo)
if ok and type(remaining) == "number" and remaining > 0 then
return remaining, true
elseif ok then
return 0, false
end
-- pcall failed (tainted) - remaining unknown
return 0, false
end
-- Item cooldowns - also pcall-wrapped for safety
-- v4: Named function for pcall
local function _itemGlowRemaining(start, duration)
if start <= 0 or duration <= 1.5 then return 0 end
return (start + duration) - GetTime()
end
function MSWA_GetItemGlowRemaining(start, duration)
if not start or not duration then return 0, false end
local ok, remaining = pcall(_itemGlowRemaining, start, duration)
if ok and type(remaining) == "number" and remaining > 0 then
return remaining, true
end
return 0, false
end
-----------------------------------------------------------
-- Grayscale: __mswaSet flag, ZERO pcall
-----------------------------------------------------------
function MSWA_IsCooldownActive(btn)
if not btn or not btn.cooldown then return false end
local cd = btn.cooldown
return cd.__mswaSet and cd:IsShown()
end
-----------------------------------------------------------
-- Hot-path style helpers (accept db + s, no internal lookups)
-----------------------------------------------------------
local TEXT_POINT_OFFSETS = {
TOPLEFT = { 1, -1 },
TOPRIGHT = { -1, -1 },
BOTTOMLEFT = { 1, 1 },
BOTTOMRIGHT = { -1, 1 },
CENTER = { 0, 0 },
}
MSWA_TEXT_POINT_OFFSETS = TEXT_POINT_OFFSETS
MSWA_TEXT_POS_LABELS = {
TOPLEFT = "Top Left",
TOPRIGHT = "Top Right",
BOTTOMLEFT = "Bottom Left",
BOTTOMRIGHT = "Bottom Right",
CENTER = "Center",
}
function MSWA_GetTextPosLabel(point)
return MSWA_TEXT_POS_LABELS[point] or MSWA_TEXT_POS_LABELS.BOTTOMRIGHT
end
-- Inline text style: no MSWA_GetDB, no MSWA_GetSpellSettings
function MSWA_ApplyTextStyle(btn, db, s)
local count = btn.count
if not count then return end
local fontKey = (s and s.textFontKey) or (db and db.fontKey) or "DEFAULT"
local path = MSWA_GetFontPathFromKey(fontKey)
if not path and count.GetFont then path = select(1, count:GetFont()) end
local size = tonumber((s and s.textFontSize) or (db and db.textFontSize) or 12) or 12
if size < 6 then size = 6 elseif size > 48 then size = 48 end
local tc = (s and s.textColor) or (db and db.textColor)
local r, g, b = 1, 1, 1
if tc then r = tonumber(tc.r) or 1; g = tonumber(tc.g) or 1; b = tonumber(tc.b) or 1 end
local point = (s and s.textPoint) or (db and db.textPoint) or "BOTTOMRIGHT"
local off = TEXT_POINT_OFFSETS[point] or TEXT_POINT_OFFSETS.BOTTOMRIGHT
if path then count:SetFont(path, size, "OUTLINE") end
count:SetTextColor(r, g, b, 1)
count:ClearAllPoints()
count:SetPoint(point, btn, point, off[1], off[2])
end
-- v4: Stack style that takes db as parameter (no internal MSWA_GetDB call)
-- v6: Also styles btn.count so buff-tracking paths that write to count
-- inherit the user's stack font/color/position settings.
function MSWA_ApplyStackStyle_Fast(btn, s, db)
local st = btn.stackText
local ct = btn.count
if not st and not ct then return end
local fontKey = (s and s.stackFontKey) or (db and db.stackFontKey) or (db and db.fontKey) or "DEFAULT"
local path = MSWA_GetFontPathFromKey(fontKey)
if not path then
local ref = st or ct
if ref and ref.GetFont then path = select(1, ref:GetFont()) end
end
local size = tonumber((s and s.stackFontSize) or (db and db.stackFontSize) or 12) or 12
if size < 6 then size = 6 elseif size > 48 then size = 48 end
local tc = (s and s.stackColor) or (db and db.stackColor)
local r, g, b = 1, 1, 1
if tc then r = tonumber(tc.r) or 1; g = tonumber(tc.g) or 1; b = tonumber(tc.b) or 1 end
local point = (s and s.stackPoint) or (db and db.stackPoint) or "BOTTOMRIGHT"
local baseOff = TEXT_POINT_OFFSETS[point] or TEXT_POINT_OFFSETS.BOTTOMRIGHT
local ox = tonumber((s and s.stackOffsetX) or (db and db.stackOffsetX) or 0) or 0
local oy = tonumber((s and s.stackOffsetY) or (db and db.stackOffsetY) or 0) or 0
local px = baseOff[1] + ox
local py = baseOff[2] + oy
if st then
if path then st:SetFont(path, size, "OUTLINE") end
st:SetTextColor(r, g, b, 1)
st:ClearAllPoints()
st:SetPoint(point, btn, point, px, py)
end
if ct then
if path then ct:SetFont(path, size, "OUTLINE") end
ct:SetTextColor(r, g, b, 1)
ct:ClearAllPoints()
ct:SetPoint(point, btn, point, px, py)
end
end
-- Legacy: ApplyStackStyle (calls MSWA_GetDB internally - for Options UI)
function MSWA_ApplyStackStyle(btn, s)
MSWA_ApplyStackStyle_Fast(btn, s, MSWA_GetDB())
end
-----------------------------------------------------------
-- Buff visual (stacks/charges) - accepts db + s
-----------------------------------------------------------
function MSWA_UpdateBuffVisual_Fast(btn, s, spellID, isItem, itemID)
local target = btn.stackText or btn.count
if not target then return end
if btn.stackText and btn.count and btn.stackText ~= btn.count then
btn.count:SetText(""); btn.count:Hide()
end
local showMode = (s and s.stackShowMode) or "auto"
if showMode == "hide" then target:SetText(""); target:Hide(); return end
-- v6: hide stacks while cooldown is active if toggle is on
if s and s.hideStacksOnCooldown and MSWA_IsCooldownActive(btn) then
target:SetText(""); target:Hide(); return
end
if isItem then
if itemID and GetItemCount then
local cnt = GetItemCount(itemID, false, false)
if type(cnt) == "number" then target:SetText(tostring(cnt)); target:Show()
else target:SetText(""); target:Hide() end
else target:SetText(""); target:Hide() end
return
end
if spellID then
local auraData = MSWA_GetPlayerAuraDataBySpellID(spellID)
local stackText = MSWA_GetAuraStackText(auraData, 2)
if (not stackText) and showMode == "show" and auraData then
stackText = "1"
end
if not stackText then
stackText = MSWA_GetSpellChargesText(spellID)
end
if stackText then
target:SetText(stackText); target:Show()
else
target:SetText(""); target:Hide()
end
return
end
target:SetText(""); target:Hide()
end
-----------------------------------------------------------
-- Conditional text color - accepts s directly
-----------------------------------------------------------
local function FindCooldownText(cd)
if not cd or not cd.GetRegions then return nil end
for _, region in pairs({cd:GetRegions()}) do
if region and region.IsObjectType and region:IsObjectType("FontString") then return region end
end
if cd.GetChildren then
for _, child in pairs({cd:GetChildren()}) do
if child and child.GetRegions then
for _, region in pairs({child:GetRegions()}) do
if region and region.IsObjectType and region:IsObjectType("FontString") then return region end
end
end
end
end
return nil
end
function MSWA_ApplyConditionalTextColor_Fast(btn, s, db, remaining, isOnCooldown)
local baseTC = (s and s.textColor) or (db and db.textColor)
local fr, fg, fb = 1, 1, 1
if baseTC then fr = tonumber(baseTC.r) or 1; fg = tonumber(baseTC.g) or 1; fb = tonumber(baseTC.b) or 1 end
if s and s.textColor2Enabled and s.textColor2 then
local cond = s.textColor2Cond or "TIMER_BELOW"
local val = tonumber(s.textColor2Value) or 5
remaining = remaining or 0
local condActive = false
if cond == "TIMER_BELOW" then
condActive = isOnCooldown and remaining <= val and remaining > 0
elseif cond == "TIMER_ABOVE" then
condActive = isOnCooldown and remaining >= val
end
if condActive then
fr = tonumber(s.textColor2.r) or 1; fg = tonumber(s.textColor2.g) or 0; fb = tonumber(s.textColor2.b) or 0
end
end
if btn.cooldown then
local cdText = btn._mswaCDText
if cdText == nil then
cdText = FindCooldownText(btn.cooldown)
btn._mswaCDText = cdText or false
elseif cdText == false then
cdText = nil
end
if cdText then cdText:SetTextColor(fr, fg, fb, 1) end
end
-- Also color the decimal timer overlay (shown when showDecimal is on)
if btn._msaDecimalTimer and btn._msaDecimalTimer:IsShown() then
btn._msaDecimalTimer:SetTextColor(fr, fg, fb, 1)
end
end
-----------------------------------------------------------
-- Swipe darken - v4: dirty-flagged to skip redundant calls
-----------------------------------------------------------
function MSWA_ApplySwipeDarken_Fast(btn, s)
local cd = btn and btn.cooldown
if not cd then return end
local reverse = (s and s.swipeDarken) and true or false
local newState = reverse and 2 or 1
-- v4: skip if swipe state hasn't changed
if cd.__mswaSwipeState == newState then return end
cd.__mswaSwipeState = newState
if cd.SetDrawEdge then cd:SetDrawEdge(false) end
if cd.SetDrawSwipe then cd:SetDrawSwipe(true) end
if cd.SetSwipeColor then cd:SetSwipeColor(0, 0, 0, 0.8) end
if cd.SetReverse then
cd:SetReverse(reverse)
else
cd.reverse = reverse
end
end
-----------------------------------------------------------
-- Legacy compat shims (Options UI calls these by name)
-----------------------------------------------------------
function MSWA_GetTextStyleForKey(key)
local db = MSWA_GetDB()
local s = key and select(1, MSWA_GetSpellSettings(db, key))
local size = tonumber((s and s.textFontSize) or (db and db.textFontSize) or 12) or 12
if size < 6 then size = 6 elseif size > 48 then size = 48 end
local tc = (s and s.textColor) or (db and db.textColor) or {r=1,g=1,b=1}
local point = (s and s.textPoint) or (db and db.textPoint) or "BOTTOMRIGHT"
local off = TEXT_POINT_OFFSETS[point] or TEXT_POINT_OFFSETS.BOTTOMRIGHT
return size, tonumber(tc.r) or 1, tonumber(tc.g) or 1, tonumber(tc.b) or 1, point, off[1], off[2]
end
function MSWA_GetStackStyleForKey(key)
local db = MSWA_GetDB()
local s = key and select(1, MSWA_GetSpellSettings(db, key))
local size = tonumber((s and s.stackFontSize) or (db and db.stackFontSize) or 12) or 12
if size < 6 then size = 6 elseif size > 48 then size = 48 end
local tc = (s and s.stackColor) or (db and db.stackColor) or {r=1,g=1,b=1}
local point = (s and s.stackPoint) or (db and db.stackPoint) or "BOTTOMRIGHT"
return size, tonumber(tc.r) or 1, tonumber(tc.g) or 1, tonumber(tc.b) or 1, point, tonumber((s and s.stackOffsetX) or (db and db.stackOffsetX) or 0) or 0, tonumber((s and s.stackOffsetY) or (db and db.stackOffsetY) or 0) or 0
end
function MSWA_GetStackShowMode(key)
if not key then return "auto" end
local s = select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key))
return (s and s.stackShowMode) or "auto"
end
function MSWA_ApplyTextStyleToButton(btn, key)
local db = MSWA_GetDB()
MSWA_ApplyTextStyle(btn, db, key and select(1, MSWA_GetSpellSettings(db, key)))
end
function MSWA_ApplyStackStyleToButton(btn, key)
MSWA_ApplyStackStyle(btn, key and select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key)))
end
function MSWA_ApplyGrayscaleOnCooldownToButton(btn, key)
if not btn or not btn.icon then return end
local s = key and select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key))
btn.icon:SetDesaturated(s and s.grayOnCooldown and MSWA_IsCooldownActive(btn) or false)
end
function MSWA_UpdateBuffVisual(btn, key)
local s = key and select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key))
local isItem = MSWA_IsItemKey(key)
MSWA_UpdateBuffVisual_Fast(btn, s, not isItem and MSWA_KeyToSpellID(key), isItem, isItem and MSWA_KeyToItemID(key))
end
function MSWA_ApplyConditionalTextColor(btn, key, remaining, isOnCooldown)
local db = MSWA_GetDB()
MSWA_ApplyConditionalTextColor_Fast(btn, key and select(1, MSWA_GetSpellSettings(db, key)), db, remaining, isOnCooldown)
end
function MSWA_ApplySwipeDarken(btn, key)
MSWA_ApplySwipeDarken_Fast(btn, key and select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key)))
end
function MSWA_ShouldGrayOnCooldown(key)
if not key then return false end
local s = select(1, MSWA_GetSpellSettings(MSWA_GetDB(), key))
return (s and s.grayOnCooldown) and true or false
end
function MSWA_IsCooldownFrameActive(cd)
if not cd then return false end
return cd.__mswaSet and cd:IsShown()
end
function MSWA_UpdateItemCount(btn, itemID)
local target = btn and (btn.stackText or btn.count)
if not target or not itemID or not GetItemCount then
if target then target:SetText(""); target:Hide() end; return
end
local cnt = GetItemCount(itemID, false, false)
if type(cnt) == "number" then target:SetText(tostring(cnt)); target:Show()
else target:SetText(""); target:Hide() end
end
-----------------------------------------------------------
-- Reminder Buff: lazy-created centered label (zero cost
-- when not used - FontString only allocated on first show)
-----------------------------------------------------------
local function GetOrCreateReminderLabel(btn)
if btn._msaReminderLabel then return btn._msaReminderLabel end
local label = btn:CreateFontString(nil, "OVERLAY")
label:SetDrawLayer("OVERLAY", 7)
label:SetPoint("CENTER", btn, "CENTER", 0, 0)
label:Hide()
btn._msaReminderLabel = label
return label
end
-- _msaReminderStyleKey: dirty-flag so we skip SetFont/SetTextColor
-- when settings haven't changed (same pattern as _msaStyleKey).
function MSWA_ShowReminderLabel(btn, s, db)
local text = s and s.reminderText
if not text or text == "" then
if btn._msaReminderLabel then btn._msaReminderLabel:Hide() end
return
end
local label = GetOrCreateReminderLabel(btn)
-- Build style key for dirty-flag
local fontSize = tonumber(s.reminderFontSize) or 12
local c = s.reminderTextColor
local cr, cg, cb = 1, 0, 0
if c then cr = c.r or 1; cg = c.g or 0; cb = c.b or 0 end
local fk = db and db.fontKey or "DEFAULT"
local styleKey = text .. ":" .. fk .. ":" .. fontSize .. ":" .. cr .. ":" .. cg .. ":" .. cb
if btn._msaReminderStyleKey ~= styleKey then
btn._msaReminderStyleKey = styleKey
local fontPath = MSWA_GetFontPathFromKey(fk)
label:SetFont(fontPath, fontSize, "OUTLINE")
label:SetTextColor(cr, cg, cb, 1)
label:SetText(text)
end
label:Show()
end
function MSWA_HideReminderLabel(btn)
if btn._msaReminderLabel then
btn._msaReminderLabel:Hide()
end
btn._msaReminderStyleKey = nil
end
-----------------------------------------------------------
-- Charge Display: lazy-created label for spell charges
-- (Fire Blast 2/3) and item charges (Healthstone count).
-- Secret-safe: pcall on GetSpellCharges, GetItemCount is
-- plain Lua - no taint risk.
-----------------------------------------------------------
local function GetOrCreateChargeLabel(btn)
if btn._msaChargeLabel then return btn._msaChargeLabel end
local label = btn:CreateFontString(nil, "OVERLAY")
label:SetDrawLayer("OVERLAY", 7)
label:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
label:Hide()
btn._msaChargeLabel = label
return label
end
-----------------------------------------------------------
-- Charge Tracker: user-defined charges (100% secret-safe)
--
-- Zero API reads. Charges are tracked via cast detection
-- (UNIT_SPELLCAST_SUCCEEDED / BAG_UPDATE_COOLDOWN) and
-- client-side timers with GetTime(). All values are plain
-- Lua numbers set by the user or our code - no taint.
--
-- Runtime state: MSWA._charges[key] = {
-- remaining = N, -- current charges left
-- rechargeStart = 0, -- GetTime() when recharge began
-- }
-----------------------------------------------------------
-- Recharge tick: called from engine's main loop.
-- Restores charges when recharge duration has elapsed.
-- Returns true if still recharging (needs continued updates).
function MSWA_ChargeRechargeTick(key, s, now)
if not s or s.auraMode ~= "CHARGES" then return false end
local ch = MSWA._charges and MSWA._charges[key]
if not ch then return false end
local maxC = tonumber(s.chargeMax) or 3
local dur = tonumber(s.chargeDuration) or 0
if dur <= 0 or ch.remaining >= maxC then
ch.rechargeStart = 0
return false
end
if ch.rechargeStart > 0 then
while ch.remaining < maxC and (now - ch.rechargeStart) >= dur do
ch.remaining = ch.remaining + 1
ch.rechargeStart = ch.rechargeStart + dur
end
if ch.remaining >= maxC then
ch.rechargeStart = 0
return false
end
return true -- still recharging
end
return false
end
-- Consume one charge. Called from cast/item detection.
function MSWA_ConsumeCharge(key, s)
if not s or s.auraMode ~= "CHARGES" then return false end
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
if ch.remaining <= 0 then return false end
local wasFull = (ch.remaining >= maxC)
ch.remaining = ch.remaining - 1
-- Start recharge timer if this is the first charge spent
local dur = tonumber(s.chargeDuration) or 0
if dur > 0 and wasFull then
ch.rechargeStart = GetTime()
elseif dur > 0 and ch.rechargeStart <= 0 then
ch.rechargeStart = GetTime()
end
return true
end
-- Show charge count on a button (e.g. "2/3").
-- Dirty-flagged via _msaChargeStyleKey.
function MSWA_ShowChargeCount(btn, remaining, maxCharges, s, db)
local label = GetOrCreateChargeLabel(btn)
local text = tostring(remaining) .. "/" .. tostring(maxCharges)
local fontSize = tonumber(s and s.chargeFontSize) or (db and tonumber(db.stackFontSize)) or 12
local c = s and s.chargeColor
local cr, cg, cb = 1, 1, 1
if c then cr = c.r or 1; cg = c.g or 1; cb = c.b or 1 end
local fk = (s and s.chargeFontKey) or (db and db.fontKey) or "DEFAULT"
local pt = (s and s.chargePoint) or "BOTTOMRIGHT"
local ox = tonumber(s and s.chargeOffsetX) or 0
local oy = tonumber(s and s.chargeOffsetY) or 0
local styleKey = text .. ":" .. fk .. ":" .. fontSize .. ":" .. cr .. ":" .. cg .. ":" .. cb .. ":" .. pt .. ":" .. ox .. ":" .. oy
if btn._msaChargeStyleKey ~= styleKey then
btn._msaChargeStyleKey = styleKey
local fontPath = MSWA_GetFontPathFromKey(fk)
label:SetFont(fontPath, fontSize, "OUTLINE")
label:SetTextColor(cr, cg, cb, 1)
label:SetText(text)
label:ClearAllPoints()
local posOffsets = MSWA_TEXT_POINT_OFFSETS or {}
local off = posOffsets[pt] or { x = 1, y = 1 }
label:SetPoint(pt, btn, pt, (off.x or 0) + ox, (off.y or 0) + oy)
end
label:Show()
end
function MSWA_HideChargeLabel(btn)
if btn._msaChargeLabel then
btn._msaChargeLabel:Hide()
end
btn._msaChargeStyleKey = nil
end
-----------------------------------------------------------
-- Shared timer formatting (used by bars + icon decimal)
-----------------------------------------------------------
function MSWA_FormatTimer(seconds, showDecimal)
if not seconds or seconds <= 0 then return "" end
local floor = math.floor
if seconds >= 3600 then
return string.format("%d:%02d:%02d", floor(seconds/3600), floor(seconds%3600/60), floor(seconds%60))
elseif seconds >= 60 then
return string.format("%d:%02d", floor(seconds/60), floor(seconds%60))
elseif showDecimal and seconds < 10 then
return string.format("%.1f", seconds)
else
return string.format("%d", floor(seconds))
end
end