-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMinimapStack.lua
More file actions
744 lines (690 loc) · 30.5 KB
/
Copy pathMinimapStack.lua
File metadata and controls
744 lines (690 loc) · 30.5 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
local ADDON, ns = ...
-- Some Minimap descendants deliberately ignore parent alpha (notably pooled
-- pins and addon chrome), while EUI can keep its grouped addon-button panel as
-- a UIParent sibling. The normal Minimap target remains the real Minimap frame
-- for Cinematic ownership and hover behavior. This helper only composes its PF
-- opacity onto visuals that can escape that parent fade.
local owned = setmetatable({}, { __mode = "k" })
local hostAlpha = setmetatable({}, { __mode = "k" })
local appliedAlpha = setmetatable({}, { __mode = "k" })
local guarded = setmetatable({}, { __mode = "k" })
local hooked = setmetatable({}, { __mode = "k" })
local pending = setmetatable({}, { __mode = "k" })
local hoverExcluded = setmetatable({}, { __mode = "k" })
local multiplier = 1
local scan
local nativeMarkerScale
-- The native marker compositor is separate from the Minimap alpha tree. Keep
-- its baseline unknown by default: some layouts use a non-default icon scale
-- and SetIconScale has no portable getter. Only an explicitly selected
-- experimental mode may opt into the documented 100% fallback.
local nativeMarkerHostScale
local nativeMarkerBaselineAssumed = false
local nativeMarkerMode
local nativeMarkerAlpha = 1
local nativeMarkerGuard = false
local nativeMarkerHooked = false
local nativeMarkerAuditAt = 0
local nativeMarkerRestorePending = false
-- Quest areas are rendered by a separate native blob compositor. They do not
-- obey Minimap alpha or SetIconScale and they are not frames/regions that can
-- join the semantic scan. Lease the fill, outer edge, ring alpha, and ring
-- scalar exposed for each Blizzard blob type.
local nativeBlobHostAlpha = {}
local nativeBlobAppliedAlpha = {}
local nativeBlobHooked = {}
local nativeBlobGuard = false
local nativeBlobAuditAt = {}
local nativeBlobRestorePending = false
local NATIVE_BLOB_ALPHA_SETTERS = {
"SetArchBlobInsideAlpha",
"SetArchBlobOutsideAlpha",
"SetArchBlobRingAlpha",
"SetArchBlobRingScalar",
"SetQuestBlobInsideAlpha",
"SetQuestBlobOutsideAlpha",
"SetQuestBlobRingAlpha",
"SetQuestBlobRingScalar",
"SetTaskBlobInsideAlpha",
"SetTaskBlobOutsideAlpha",
"SetTaskBlobRingAlpha",
"SetTaskBlobRingScalar",
}
ns.MINIMAP_NATIVE_MARKER_MODES = {
keep = "Leave unchanged",
hide_zero = "Hide at 0%",
scale = "Scale with map",
}
ns.MINIMAP_NATIVE_MARKER_ORDER = { "keep", "hide_zero", "scale" }
local function IsSecret(value)
return issecretvalue and issecretvalue(value) or false
end
local function ValidAlpha(value)
return type(value) == "number" and not IsSecret(value) and value == value
and value >= 0 and value <= 1
end
-- Unlike alpha, an icon scale is not bounded to 1. Keep the validation
-- intentionally permissive so a host's legitimate large scale survives our
-- temporary compositor override and its later restoration.
local function ValidScale(value)
return type(value) == "number" and not IsSecret(value) and value == value
and value >= 0 and value < math.huge
end
local function Method(object, name, ...)
if not object then return nil end
local okMethod, callback = pcall(function() return object[name] end)
if not okMethod or type(callback) ~= "function" then return nil end
local ok, value = pcall(callback, object, ...)
if not ok or IsSecret(value) then return nil end
return value
end
local function Alpha(frame)
local value = Method(frame, "GetAlpha")
return ValidAlpha(value) and value or nil
end
local function SetAlpha(frame, value)
if not ValidAlpha(value) then return false end
guarded[frame] = true
local ok = pcall(function() frame:SetAlpha(value) end)
guarded[frame] = nil
if ok then appliedAlpha[frame] = value end
return ok
end
local function Apply(frame)
local base = hostAlpha[frame]
return owned[frame] and ValidAlpha(base) and SetAlpha(frame, base * multiplier) or false
end
local function HostAlphaChanged(frame, value)
if guarded[frame] or not owned[frame] then return end
if not ValidAlpha(value) then
Apply(frame)
return
end
hostAlpha[frame] = value
Apply(frame)
end
local function Hook(frame)
if hooked[frame] or type(hooksecurefunc) ~= "function" then return end
if pcall(hooksecurefunc, frame, "SetAlpha", function(_, value) HostAlphaChanged(frame, value) end) then
hooked[frame] = true
end
end
local function Claim(frame)
if not frame or owned[frame] then return end
local base = Alpha(frame)
if base == nil then return end
local restore = pending[frame]
if restore then
if math.abs(base - restore.lastApplied) <= 0.001 then base = restore.base end
pending[frame] = nil
end
owned[frame] = true
hostAlpha[frame] = base
Hook(frame)
Apply(frame)
end
local function Release(frame)
if not owned[frame] then return end
local base, last = hostAlpha[frame], appliedAlpha[frame]
local live = Alpha(frame)
if base ~= nil and last ~= nil and live ~= nil and math.abs(live - last) <= 0.001 then
if not SetAlpha(frame, base) then pending[frame] = { base = base, lastApplied = last } end
elseif live == nil and base ~= nil and last ~= nil then
pending[frame] = { base = base, lastApplied = last }
end
owned[frame] = nil
hostAlpha[frame] = nil
appliedAlpha[frame] = nil
end
local function IsDescendant(frame, ancestor)
local current, seen, depth = frame, {}, 0
while current and depth < 20 and not seen[current] do
if current == ancestor then return true end
seen[current] = true
current = Method(current, "GetParent")
depth = depth + 1
end
return false
end
local function Children(frame)
if not frame then return {} end
local okMethod, callback = pcall(function() return frame.GetChildren end)
if not okMethod or type(callback) ~= "function" then return {} end
local values = { pcall(callback, frame) }
if not table.remove(values, 1) then return {} end
return values
end
local function Regions(frame)
if not frame then return {} end
local okMethod, callback = pcall(function() return frame.GetRegions end)
if not okMethod or type(callback) ~= "function" then return {} end
local values = { pcall(callback, frame) }
if not table.remove(values, 1) then return {} end
return values
end
local function VisualChildren(frame)
local values = Children(frame)
for _, region in ipairs(Regions(frame)) do values[#values + 1] = region end
return values
end
local function AddEUIFlyoutPanels(result, minimap)
-- EUI's grouped addon buttons can live in one anonymous UIParent flyout
-- rather than beneath Minimap. Claim that small shared panel, not each
-- button and not an arbitrary addon's original UI root.
local parents = {}
for _, button in pairs(type(_G._EBS_CachedAddonButtons) == "table" and _G._EBS_CachedAddonButtons or {}) do
local parent = Method(button, "GetParent")
if parent and parent ~= UIParent and not IsDescendant(parent, minimap)
and Method(parent, "GetParent") == UIParent and Method(parent, "GetName") == nil
and Method(parent, "GetFrameStrata") == "DIALOG" then
parents[parent] = (parents[parent] or 0) + 1
end
end
for parent, count in pairs(parents) do
-- Requiring a shared anonymous direct UIParent parent prevents a stale
-- cached button from making PF claim an unrelated addon container.
if count >= 1 then
result[parent] = true
-- The panel itself inherits PF's multiplier, but some third-party
-- buttons deliberately ignore parent alpha. Lease only those
-- escapees (and nested escapees), avoiding a double multiplier on
-- ordinary children that already inherit the panel correctly.
local queue, seen, index = VisualChildren(parent), {}, 1
while index <= #queue and index <= 256 do
local child = queue[index]
index = index + 1
if child and not seen[child] then
seen[child] = true
if Method(child, "IsIgnoringParentAlpha") == true then result[child] = true end
for _, nested in ipairs(VisualChildren(child)) do
if not seen[nested] then queue[#queue + 1] = nested end
end
end
end
end
end
end
local function HasClaimedAncestor(result, frame)
local current, seen, depth = Method(frame, "GetParent"), {}, 0
while current and depth < 20 and not seen[current] do
if result[current] then return true end
seen[current] = true
current = Method(current, "GetParent")
depth = depth + 1
end
return false
end
local function AddSemanticFrame(result, frame, minimap)
if not frame or frame == minimap then return end
local ignoresParent = Method(frame, "IsIgnoringParentAlpha") == true
-- Ordinary Minimap children already inherit its opacity. Only claim the
-- unusual children that explicitly opt out of parent alpha.
if IsDescendant(frame, minimap) then
if ignoresParent then result[frame] = true end
return
end
-- If a semantic container is already leased, its normal descendants must
-- not receive the multiplier twice. Descendants that ignore parent alpha
-- remain separate leases by design.
if HasClaimedAncestor(result, frame) then
if ignoresParent then result[frame] = true end
return
end
result[frame] = true
end
local function AddAlphaEscapingDescendants(result, root, minimap)
-- Super Tracking owns a small, UIParent-level directional presentation.
-- Its individual native regions can opt out of their root's alpha, so a
-- root-only lease leaves the clamped quest-direction arc visible when the
-- Minimap rests at zero. Walk only this known, narrow tree and claim those
-- explicit alpha escapees; normal children still inherit the root once.
local queue, seen, index = VisualChildren(root), {}, 1
while index <= #queue and index <= 96 do
local child = queue[index]
index = index + 1
if child and not seen[child] then
seen[child] = true
if Method(child, "IsIgnoringParentAlpha") == true then
AddSemanticFrame(result, child, minimap)
end
for _, nested in ipairs(VisualChildren(child)) do
if not seen[nested] then queue[#queue + 1] = nested end
end
end
end
end
local function AddSemanticMinimapFrames(result, minimap)
AddEUIFlyoutPanels(result, minimap)
-- Retail's visible minimap is not one alpha tree. Mail, tracking and other
-- indicators are commonly siblings of Minimap under MinimapCluster.
local cluster = _G.MinimapCluster
local indicator = cluster and cluster.IndicatorFrame
AddSemanticFrame(result, indicator, minimap)
AddSemanticFrame(result, cluster and cluster.Tracking, minimap)
AddSemanticFrame(result, cluster and cluster.InstanceDifficulty, minimap)
-- Stable Blizzard minimap surfaces which may move between Minimap,
-- MinimapCluster and UIParent across layouts/skins.
local known = {
_G.AddonCompartmentFrame,
_G.ExpansionLandingPageMinimapButton,
_G.QueueStatusButton,
_G.GameTimeFrame,
_G.MiniMapTracking,
_G.MiniMapMailFrame,
}
for _, frame in ipairs(known) do AddSemanticFrame(result, frame, minimap) end
-- The navigation/super-tracking presentation is a UIParent root rather
-- than a Minimap child. It belongs to the same visual stack but must not
-- enlarge the minimap's mouseover hit area if Blizzard gives it a broad
-- layout rectangle.
local superTracked = _G.SuperTrackedFrame
AddSemanticFrame(result, superTracked, minimap)
if superTracked then
hoverExcluded[superTracked] = true
AddAlphaEscapingDescendants(result, superTracked, minimap)
end
-- LibDBIcon is the canonical registry for third-party minimap buttons.
-- Querying its object table is both cheaper and more accurate than guessing
-- ownership from names or screen position after another addon reparents it.
local lib
if type(_G.LibStub) == "table" or type(_G.LibStub) == "function" then
local ok, value = pcall(_G.LibStub, "LibDBIcon-1.0", true)
if ok and type(value) == "table" then lib = value end
end
for _, button in pairs(lib and type(lib.objects) == "table" and lib.objects or {}) do
AddSemanticFrame(result, button, minimap)
end
-- EUI publishes the buttons it has collected even while their current
-- parent changes between Minimap and its anonymous flyout.
for _, button in pairs(type(_G._EBS_CachedAddonButtons) == "table" and _G._EBS_CachedAddonButtons or {}) do
AddSemanticFrame(result, button, minimap)
end
end
local function BeginScan(minimap)
-- Minimap POIs are frequently Texture/Region objects rather than child
-- Frames. Some deliberately ignore their pin or Minimap parent's alpha.
-- Regions still expose GetAlpha/SetAlpha and can safely participate in the
-- same composed visibility lease.
scan = { minimap = minimap, queue = VisualChildren(minimap), index = 1, seen = {}, found = {} }
AddSemanticMinimapFrames(scan.found, minimap)
end
local function ContinueScan(minimap)
if not scan or scan.minimap ~= minimap then BeginScan(minimap) end
local processed = 0
local started = type(debugprofilestop) == "function" and debugprofilestop() or nil
while scan.index <= #scan.queue and processed < 48
and (processed == 0 or not started or debugprofilestop() - started < 0.60) do
local child = scan.queue[scan.index]
scan.index = scan.index + 1
processed = processed + 1
if child and not scan.seen[child] then
scan.seen[child] = true
if Method(child, "IsIgnoringParentAlpha") == true then scan.found[child] = true end
for _, nested in ipairs(VisualChildren(child)) do
if not scan.seen[nested] then scan.queue[#scan.queue + 1] = nested end
end
end
end
if scan.index <= #scan.queue then return nil end
AddSemanticMinimapFrames(scan.found, minimap)
local result = scan.found
scan = nil
return result
end
local function ProcessPending()
if InCombatLockdown() then return end
for frame, record in pairs(pending) do
local live = Alpha(frame)
if live ~= nil and math.abs(live - record.lastApplied) > 0.001 then
pending[frame] = nil
elseif live ~= nil and SetAlpha(frame, record.base) then
pending[frame] = nil
appliedAlpha[frame] = nil
end
end
end
local function ReadNativeMarkerScale(minimap)
-- GetIconScale is available on current Retail builds, but it is deliberately
-- optional here: UI skins and older client variants do not all publish it.
-- A future host SetIconScale call is the other authoritative source.
local value = Method(minimap, "GetIconScale")
return ValidScale(value) and value or nil
end
local function CaptureNativeMarkerHostScale(minimap, allowAssumedDefault)
if nativeMarkerHostScale ~= nil then return nativeMarkerHostScale end
local value = ReadNativeMarkerScale(minimap)
if value ~= nil then
nativeMarkerHostScale = value
nativeMarkerBaselineAssumed = false
elseif allowAssumedDefault then
-- Some Retail builds expose only SetIconScale. Selecting an
-- experimental marker mode is the user's explicit opt-in to use the
-- native 100% scale as the temporary restoration baseline. The
-- default "Leave unchanged" mode never reaches this path.
nativeMarkerHostScale = 1
nativeMarkerBaselineAssumed = true
end
return nativeMarkerHostScale
end
local function HasNativeMarkerSetter(minimap)
if not minimap then return false end
local ok, setter = pcall(function() return minimap.SetIconScale end)
return ok and type(setter) == "function"
end
local function SetNativeMarkerScale(minimap, value)
if not minimap or not ValidScale(value) then return false end
local okMethod, setter = pcall(function() return minimap.SetIconScale end)
if not okMethod or type(setter) ~= "function" then return false end
nativeMarkerGuard = true
local ok = pcall(setter, minimap, value)
nativeMarkerGuard = false
return ok
end
local function NativeBlobGetterName(setterName)
return "Get" .. setterName:sub(4)
end
local function CaptureNativeBlobHostAlpha(minimap, setterName, allowAssumedDefault)
local known = nativeBlobHostAlpha[setterName]
if known ~= nil then return known end
local value = Method(minimap, NativeBlobGetterName(setterName))
if ValidAlpha(value) then
nativeBlobHostAlpha[setterName] = value
elseif allowAssumedDefault then
-- These APIs do not consistently provide getters. Experimental marker
-- modes use the same explicit 100% fallback as SetIconScale until the
-- host announces a real blob alpha through its setter.
nativeBlobHostAlpha[setterName] = 1
end
return nativeBlobHostAlpha[setterName]
end
local function SetNativeBlobAlpha(minimap, setterName, value)
if not minimap or not ValidAlpha(value) then return false end
local okMethod, setter = pcall(function() return minimap[setterName] end)
if not okMethod or type(setter) ~= "function" then return false end
nativeBlobGuard = true
local ok = pcall(setter, minimap, value)
nativeBlobGuard = false
return ok
end
local function NativeBlobWanted(baseAlpha, alpha, mode)
if mode == "scale" then return baseAlpha * alpha end
if mode == "hide_zero" then return alpha <= 0.001 and 0 or baseAlpha end
end
local function HookNativeMarkerBlobs(minimap)
if not minimap or type(hooksecurefunc) ~= "function" then return end
for _, setterName in ipairs(NATIVE_BLOB_ALPHA_SETTERS) do
if not nativeBlobHooked[setterName] then
local ok = pcall(hooksecurefunc, minimap, setterName, function(_, value)
if nativeBlobGuard or not ValidAlpha(value) then return end
local applied = nativeBlobAppliedAlpha[setterName]
-- While fully faded, a compositor rebuild can announce the
-- same zero Frame Gambit already applied. That is not a new
-- visible host baseline; remembering it would keep the blob
-- missing after the Minimap returns.
if nativeMarkerAlpha <= 0.001 and applied ~= nil
and math.abs(value - applied) <= 0.001 then
nativeBlobAuditAt[setterName] = GetTime()
return
end
nativeBlobHostAlpha[setterName] = value
nativeBlobRestorePending = false
local wanted = NativeBlobWanted(value, nativeMarkerAlpha, nativeMarkerMode)
if wanted == nil then
nativeBlobAppliedAlpha[setterName] = nil
nativeBlobAuditAt[setterName] = nil
elseif math.abs(value - wanted) <= 0.001 then
nativeBlobAppliedAlpha[setterName] = value
nativeBlobAuditAt[setterName] = GetTime()
elseif SetNativeBlobAlpha(minimap, setterName, wanted) then
nativeBlobAppliedAlpha[setterName] = wanted
nativeBlobAuditAt[setterName] = GetTime()
end
end)
if ok then nativeBlobHooked[setterName] = true end
end
end
end
local function RestoreNativeMarkerBlobs(minimap)
if next(nativeBlobAppliedAlpha) == nil then
nativeBlobRestorePending = false
nativeBlobAuditAt = {}
return true
end
local complete = true
for setterName, applied in pairs(nativeBlobAppliedAlpha) do
local restore = CaptureNativeBlobHostAlpha(minimap, setterName)
if restore == nil then
complete = false
elseif math.abs(applied - restore) > 0.001 and not SetNativeBlobAlpha(minimap, setterName, restore) then
complete = false
else
nativeBlobAppliedAlpha[setterName] = nil
nativeBlobAuditAt[setterName] = nil
end
end
nativeBlobRestorePending = not complete
if complete then nativeBlobAuditAt = {} end
return complete
end
local function UpdateNativeMarkerBlobs(minimap, alpha, mode)
if mode ~= "hide_zero" and mode ~= "scale" then
RestoreNativeMarkerBlobs(minimap)
return
end
HookNativeMarkerBlobs(minimap)
local now = GetTime()
for _, setterName in ipairs(NATIVE_BLOB_ALPHA_SETTERS) do
local blobHostAlpha = CaptureNativeBlobHostAlpha(minimap, setterName, true)
local wanted = blobHostAlpha and NativeBlobWanted(blobHostAlpha, alpha, mode)
local applied = nativeBlobAppliedAlpha[setterName]
-- Native map rebuilds can restore any blob channel without calling its
-- Lua setter. Audit each channel independently: one shared timestamp
-- lets the first setter suppress every later setter in this loop.
local auditedAt = nativeBlobAuditAt[setterName] or 0
local auditDue = wanted and wanted <= 0.001 and now - auditedAt >= 0.25
if wanted ~= nil and (auditDue or applied == nil or math.abs(applied - wanted) > 0.001)
and SetNativeBlobAlpha(minimap, setterName, wanted) then
nativeBlobAppliedAlpha[setterName] = wanted
nativeBlobAuditAt[setterName] = now
end
end
nativeBlobRestorePending = false
end
local function HookNativeMarkers(minimap)
if nativeMarkerHooked or not minimap or type(hooksecurefunc) ~= "function" then return end
local ok = pcall(hooksecurefunc, minimap, "SetIconScale", function(_, value)
if nativeMarkerGuard or not ValidScale(value) then return end
nativeMarkerHostScale = value
nativeMarkerBaselineAssumed = false
nativeMarkerScale = value
nativeMarkerRestorePending = false
local wanted
if nativeMarkerMode == "scale" then
wanted = value * nativeMarkerAlpha
elseif nativeMarkerMode == "hide_zero" then
wanted = nativeMarkerAlpha <= 0.001 and 0 or value
end
if wanted ~= nil and math.abs(value - wanted) > 0.001 then
if SetNativeMarkerScale(minimap, wanted) then
nativeMarkerScale = wanted
nativeMarkerAuditAt = GetTime()
end
end
end)
if ok then nativeMarkerHooked = true end
end
local function RestoreNativeMarkers(minimap)
-- If an override was never applied there is nothing to put back. Clear
-- immediately rather than attempting to "restore" an assumed scale.
if nativeMarkerScale == nil then
nativeMarkerMode = nil
nativeMarkerAlpha = 1
nativeMarkerRestorePending = false
nativeMarkerAuditAt = 0
return true
end
-- Stop composing immediately, even if the host setter is temporarily
-- unavailable. A later host SetIconScale call must become the new host
-- baseline instead of being transformed by the old fade mode.
nativeMarkerMode = nil
nativeMarkerAlpha = 1
local restore = CaptureNativeMarkerHostScale(minimap)
if restore == nil then
-- We never overwrite an unknown host scale. Keep retrying in case the
-- client later exposes a getter or the host calls SetIconScale.
nativeMarkerRestorePending = true
return false
end
if math.abs(nativeMarkerScale - restore) > 0.001 and not SetNativeMarkerScale(minimap, restore) then
-- Failed setters are transient during client rebuilds/lockdown. Do
-- not lose the original host scale; the next update will retry.
nativeMarkerRestorePending = true
return false
end
nativeMarkerScale = nil
-- Keep the last authoritative host baseline. Some Retail builds expose
-- SetIconScale without a matching getter, so forgetting a value announced
-- by the host would make the safe experimental modes unavailable again on
-- the very next keep-mode update. A later host setter refreshes this value
-- through HookNativeMarkers.
nativeMarkerRestorePending = false
nativeMarkerAuditAt = 0
return true
end
local function UpdateNativeMarkers(minimap, alpha, mode)
if mode ~= "hide_zero" and mode ~= "scale" then
RestoreNativeMarkers(minimap)
return
end
HookNativeMarkers(minimap)
local hostScale = CaptureNativeMarkerHostScale(minimap, true)
if hostScale == nil then
-- The setter itself is unavailable or rejected. Keep the requested
-- mode recorded, but do not claim that a visual change was applied.
nativeMarkerMode = mode
nativeMarkerAlpha = alpha
return
end
nativeMarkerMode = mode
nativeMarkerAlpha = alpha
nativeMarkerRestorePending = false
local wanted = mode == "scale" and (hostScale * alpha)
or (alpha <= 0.001 and 0 or hostScale)
local now = GetTime()
-- Blizzard's native blip compositor can rebuild without making a Lua
-- SetIconScale call. Reassert at a low rate so a rebuilt quest/service-pin
-- layer cannot remain visible while the Minimap is fully faded.
local auditDue = wanted <= 0.001 and now - nativeMarkerAuditAt >= 0.25
if auditDue or nativeMarkerScale == nil or math.abs(nativeMarkerScale - wanted) > 0.001 then
if SetNativeMarkerScale(minimap, wanted) then
nativeMarkerScale = wanted
nativeMarkerAuditAt = now
end
end
end
function ns:GetMinimapNativeMarkerAvailability(mode)
local minimap = _G.Minimap
if not minimap then return false, "The native Minimap is not available." end
if not HasNativeMarkerSetter(minimap) then
return false, "This WoW build does not expose native marker scaling."
end
-- Install the read-only post-hook even while leaving markers unchanged. If
-- Blizzard or the owning UI later announces a scale, experimental modes
-- can become available without guessing or requiring a reload.
HookNativeMarkers(minimap)
if CaptureNativeMarkerHostScale(minimap) ~= nil then
if nativeMarkerBaselineAssumed then
return true, "This WoW build cannot read the current native marker scale. Because an experimental marker mode is selected, Frame Gambit is using 100% as its restoration baseline until Blizzard or the owning UI announces another value."
end
return true
end
local note = "This WoW build cannot read the current native marker scale. Choosing an experimental marker mode will explicitly use 100% as its restoration baseline; Leave unchanged never writes it."
if mode == "hide_zero" or mode == "scale" then
CaptureNativeMarkerHostScale(minimap, true)
end
return true, note
end
function ns:HasMinimapStackPendingWork()
return nativeMarkerRestorePending == true or nativeBlobRestorePending == true
or scan ~= nil or next(pending) ~= nil
end
function ns:UpdateMinimapStack(rescan)
ProcessPending()
local minimap = _G.Minimap
local active = self.runtime and self.runtime.active and self.runtime.active.minimap
local controlled = self.runtime and self.runtime.frameByID and self.runtime.frameByID.minimap == minimap
local nextMultiplier = active and active.alpha
if not minimap or not controlled or not ValidAlpha(nextMultiplier) then
if next(owned) then
local frames = {}
for frame in pairs(owned) do frames[#frames + 1] = frame end
for _, frame in ipairs(frames) do Release(frame) end
end
multiplier = 1
scan = nil
RestoreNativeMarkers(minimap or _G.Minimap)
RestoreNativeMarkerBlobs(minimap or _G.Minimap)
return
end
local settings = self.GetTargetSettings and self:GetTargetSettings("minimap")
local markerMode = settings and settings.nativeMarkerMode or "keep"
UpdateNativeMarkers(minimap, nextMultiplier, markerMode)
UpdateNativeMarkerBlobs(minimap, nextMultiplier, markerMode)
local changed = math.abs(multiplier - nextMultiplier) > 0.001
multiplier = nextMultiplier
if rescan and not scan then BeginScan(minimap) end
if rescan then
-- The EUI flyout is a tiny, exact adapter and should become a
-- Cinematic exemption immediately; descendant pin discovery can keep
-- progressing incrementally without delaying the visible button stack.
local semantic = {}
AddSemanticMinimapFrames(semantic, minimap)
for frame in pairs(semantic) do
local otherID = self.runtime.managedIDByFrame and self.runtime.managedIDByFrame[frame]
if not otherID or otherID == "minimap" then Claim(frame) end
end
end
if scan then
local discovered = ContinueScan(minimap)
if discovered then
local current = {}
for frame in pairs(discovered) do
local otherID = self.runtime.managedIDByFrame and self.runtime.managedIDByFrame[frame]
if not otherID or otherID == "minimap" then
current[frame] = true
Claim(frame)
end
end
local retired = {}
for frame in pairs(owned) do if not current[frame] then retired[#retired + 1] = frame end end
for _, frame in ipairs(retired) do Release(frame) end
end
end
-- Target ownership can change without Minimap topology changing. Never
-- compose the stack multiplier onto a frame now controlled explicitly by
-- another Priority Fader target.
local conflicted = {}
for frame in pairs(owned) do
local otherID = self.runtime.managedIDByFrame and self.runtime.managedIDByFrame[frame]
if otherID and otherID ~= "minimap" then conflicted[#conflicted + 1] = frame end
end
for _, frame in ipairs(conflicted) do Release(frame) end
for frame in pairs(owned) do
local last = appliedAlpha[frame]
if not hooked[frame] then
local live = Alpha(frame)
if live ~= nil and last ~= nil and math.abs(live - last) > 0.001 then hostAlpha[frame] = live end
end
local wanted = hostAlpha[frame] and hostAlpha[frame] * multiplier
if wanted and (changed or last == nil or math.abs(wanted - last) > 0.001) then Apply(frame) end
end
end
function ns:IsMinimapStackFrame(frame)
return frame and owned[frame] == true or false
end
function ns:MinimapStackContainsCursor(x, y)
for frame in pairs(owned) do
if not hoverExcluded[frame] and self:FrameContainsCursor(frame, x, y) then return true end
end
return false
end