-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain.lua
More file actions
565 lines (447 loc) · 21.2 KB
/
Copy pathmain.lua
File metadata and controls
565 lines (447 loc) · 21.2 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
local folderName, Addon = ...
-- Locals for frequently used global frames and functions.
local C_Map_GetPlayerMapPosition = _G.C_Map.GetPlayerMapPosition
local Clamp = _G.Clamp
local GetTime = _G.GetTime
local WorldMapFrame = _G.WorldMapFrame
local IsShiftKeyDown = _G.IsShiftKeyDown
local GetScaledCursorPosition = _G.GetScaledCursorPosition
-- To prevent player pin pings when we don't need them.
local playerPin = nil
Addon.PlayerPingAnimation = function(start)
-- If we do not have the player pin yet, search it.
if not playerPin or not playerPin.ShouldShowUnit or not playerPin:ShouldShowUnit("player") then
playerPin = nil
for k, _ in pairs(WorldMapFrame.dataProviders) do
if type(k) == "table" and k.ShouldShowUnit then
-- print("Found GroupMembersDataProvider.")
if k:ShouldShowUnit("player") then
playerPin = k
break
end
end
end
end
if playerPin then
-- TOOD: Make PIN size an option.
-- TODO: Also continuous ping animation.
-- Default is 27.
-- playerPin:SetUnitPinSize("player", 100)
if start then
-- Arguments are duration and fade-out duration.
playerPin.pin:StartPlayerPing(2, .25)
else
playerPin.pin:StopPlayerPing(2, .25)
end
-- Got to call this to prevent pin size flicker.
playerPin.pin:SynchronizePinSizes()
end
end
local function EaseOutQuart(t)
local invT = 1 - t
local invT2 = invT * invT -- (1 - t)^2
local invT4 = invT2 * invT2 -- ((1 - t)^2)^2 = (1 - t)^4
return 1 - invT4
end
-- Current target zoom.
local zoomStartTime
local currentStartScale = nil
local currentTargetScale = nil
local currentStartX = nil
local currentTargetX = nil
local currentStartY = nil
local currentTargetY = nil
-- Using an OnUpdate frame did not work without flickering of quest blobs.
-- local smoothZoomFrame = CreateFrame("Frame")
-- local function SmoothZoomOnUpdateFunction(self, elapsed)
-- local zoomElapsed = Clamp(GetTime() - zoomStartTime, 0, PWM_config.zoomTimeSeconds)
-- if zoomElapsed >= PWM_config.zoomTimeSeconds then
-- WorldMapFrame.ScrollContainer:InstantPanAndZoom(currentTargetScale, currentTargetX, currentTargetY, true)
-- smoothZoomFrame:SetScript("OnUpdate", nil)
-- return
-- end
-- local zoomElapsedNormalized = zoomElapsed / PWM_config.zoomTimeSeconds
-- WorldMapFrame.ScrollContainer:InstantPanAndZoom(currentStartScale + zoomElapsedNormalized * (currentTargetScale - currentStartScale), currentTargetX, currentTargetY, true)
-- end
-- So instead I use a super-fast ticker.
local zoomTicker
local function ZoomAndPanStop()
if zoomTicker then
zoomTicker:Cancel()
zoomTicker = nil
zoomStartTime = nil
currentStartScale = nil
currentTargetScale = nil
currentStartX = nil
currentTargetX = nil
currentStartY = nil
currentTargetY = nil
end
end
local function ZoomTickerFunction(self)
local zoomElapsed = Clamp(GetTime() - zoomStartTime, 0, PWM_config.zoomTimeSeconds)
-- When we are done, make sure we have the final position.
if zoomElapsed >= PWM_config.zoomTimeSeconds or (
WorldMapFrame.ScrollContainer:GetCanvasScale() == currentTargetScale and
WorldMapFrame.ScrollContainer:GetCurrentScrollX() == currentTargetX and
WorldMapFrame.ScrollContainer:GetCurrentScrollY() == currentTargetY ) then
local xMin, xMax, yMin, yMax = WorldMapFrame.ScrollContainer:CalculateScrollExtentsAtScale(currentTargetScale)
local nextX = Clamp(currentTargetX, xMin, xMax)
local nextY = Clamp(currentTargetY, yMin, yMax)
WorldMapFrame.ScrollContainer:InstantPanAndZoom(currentTargetScale, currentTargetX, currentTargetY, true)
ZoomAndPanStop()
-- Otherwise, set the intermediate zoom and pan.
else
local zoomElapsedNormalized = EaseOutQuart(zoomElapsed / PWM_config.zoomTimeSeconds)
local nextScale = currentStartScale + zoomElapsedNormalized * (currentTargetScale - currentStartScale)
local xMin, xMax, yMin, yMax = WorldMapFrame.ScrollContainer:CalculateScrollExtentsAtScale(nextScale)
local nextX = Clamp(currentStartX + zoomElapsedNormalized * (currentTargetX - currentStartX), xMin, xMax)
local nextY = Clamp(currentStartY + zoomElapsedNormalized * (currentTargetY - currentStartY), yMin, yMax)
WorldMapFrame.ScrollContainer:InstantPanAndZoom(nextScale, nextX, nextY, true)
end
end
local function ZoomAndPan(currentScale, targetScale, currentX, targetX, currentY, targetY)
if PWM_config.zoomTimeSeconds == 0 then
WorldMapFrame.ScrollContainer:InstantPanAndZoom(targetScale, targetX, targetY, true)
else
-- Setup variables for the zoom/pan process.
zoomStartTime = GetTime()
currentStartScale = currentScale
currentTargetScale = targetScale
currentStartX = currentX
currentTargetX = targetX
currentStartY = currentY
currentTargetY = targetY
-- Using an OnUpdate frame did not work without flickering of quest blobs.
-- smoothZoomFrame:SetScript("OnUpdate", SmoothZoomOnUpdateFunction)
-- So instead we use a super-fast ticker.
if not zoomTicker then
zoomTicker = C_Timer.NewTicker(0.01, ZoomTickerFunction)
end
end
end
-- Disable default zoom.
WorldMapFrame.ScrollContainer:SetMouseWheelZoomMode(MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_NONE)
-- Override mouse wheel for custom zoom behavior.
WorldMapFrame.ScrollContainer:HookScript("OnMouseWheel", function(self, delta)
-- If we are currently zooming, we take currentTargetScale as the current scale.
local currentScale = currentTargetScale or self:GetCanvasScale()
-- Check if zooming is still possible.
if (delta < 0 and currentScale == self:GetScaleForMinZoom()) or (delta > 0 and currentScale == self:GetScaleForMaxZoom()) then
return
end
-- We could do it like MapCanvasScrollControllerMixin:OnMouseWheel()
-- but self.zoomAmountPerMouseWheelDelta behaves differently on different maps.
-- local targetScale = currentScale + self.zoomAmountPerMouseWheelDelta * delta
-- Using the predefined zoom levels of each map is better.
local currentZoomLevelIndex = self:GetZoomLevelIndexForScale(currentScale)
local targetScale = (self.zoomLevels[currentZoomLevelIndex + delta] or self.zoomLevels[currentZoomLevelIndex]).scale
local currentX = self:GetCurrentScrollX()
local currentY = self:GetCurrentScrollY()
local targetX = currentX
local targetY = currentY
-- TODO: Make zoom-panning towards cursor position optional.
-- The map boundaries are taken care of by the zoom function.
ZoomAndPan(currentScale, targetScale, currentX, targetX, currentY, targetY)
end)
-- Do a smooth pan before activating auto-centering.
local autoCenterStartTimer
local function AutoCenterStartTimerFunction()
autoCenterStartTimer = nil
end
Addon.EnableCenterOnPlayer = function()
if PWM_config.autoCentering then return end
PWM_config.autoCentering = true
Addon.UpdateAutoCenterLockButton()
-- Do a smooth pan before activating auto-centering.
-- But not when zoomed fully out, because that causes a slight map jerk.
local currentScale = WorldMapFrame.ScrollContainer:GetCanvasScale()
if currentScale > WorldMapFrame.ScrollContainer:GetScaleForMinZoom() then
local currentX = WorldMapFrame.ScrollContainer:GetCurrentScrollX()
local currentY = WorldMapFrame.ScrollContainer:GetCurrentScrollY()
local playerPos = C_Map_GetPlayerMapPosition(WorldMapFrame:GetMapID(), "player")
if playerPos then
local targetX, targetY = playerPos:GetXY()
targetX = Clamp(targetX, WorldMapFrame.ScrollContainer.scrollXExtentsMin, WorldMapFrame.ScrollContainer.scrollXExtentsMax)
targetY = Clamp(targetY, WorldMapFrame.ScrollContainer.scrollYExtentsMin, WorldMapFrame.ScrollContainer.scrollYExtentsMax)
Addon.PlayerPingAnimation(true)
ZoomAndPan(currentScale, currentScale, currentX, targetX, currentY, targetY)
end
autoCenterStartTimer = C_Timer.NewTimer(PWM_config.zoomTimeSeconds, AutoCenterStartTimerFunction)
end
end
Addon.DisableCenterOnPlayer = function()
if autoCenterStartTimer then
autoCenterStartTimer:Cancel()
autoCenterStartTimer = nil
end
ZoomAndPanStop()
PWM_config.autoCentering = false
Addon.UpdateAutoCenterLockButton()
end
-- Disable center on player when trying to drag the map.
-- Enable when double-clicking the map.
local isMouseDown = false
local lastCursorX = nil
local lastCursorY = nil
-- For double click detection
local lastClickTime = 0
-- Shift click the map to reset it!
local resetMap = false
WorldMapFrame.ScrollContainer:HookScript("OnMouseDown", function(self, button)
if button == "LeftButton" then
isMouseDown = true
lastCursorX, lastCursorY = GetScaledCursorPosition()
if self:CanPan() then
-- Stop any zoom animation.
ZoomAndPanStop()
-- If currently moving towards auto-centering, interrupt.
if autoCenterStartTimer then
Addon.DisableCenterOnPlayer()
end
end
if IsShiftKeyDown() then
resetMap = true
else
resetMap = false
end
end
end)
WorldMapFrame.ScrollContainer:HookScript("OnMouseUp", function(self, button)
if button == "LeftButton" then
isMouseDown = false
-- Check for double click
if PWM_config.autoCenterEnabled then
local currentTime = GetTime()
if currentTime - lastClickTime < PWM_config.doubleClickTime then
Addon.EnableCenterOnPlayer()
end
lastClickTime = currentTime
end
if resetMap then
if IsShiftKeyDown() then
Addon.ResetMap()
end
resetMap = false
end
end
end)
WorldMapFrame.ScrollContainer:HookScript("OnUpdate", function(self)
-- Disable auto-centering and stop zooming when player starts to drag.
if isMouseDown then
ZoomAndPanStop()
local cursorX, cursorY = GetScaledCursorPosition()
if cursorX ~= lastCursorX or cursorY ~= lastCursorY then
Addon.DisableCenterOnPlayer()
end
end
-- Auto-centering.
if PWM_config.autoCentering and not autoCenterStartTimer then
-- Ensure that targetScale exists. Not checking this sometimes caused an error
-- when changing into a zone with loading screen (e.g. Ringing Deeps to Dornogal).
if not self.targetScale then return end
-- Ensure scroll extents are initialized. Has not caused issues so far, but you never know.
if not self.scrollXExtentsMin or not self.scrollXExtentsMax or
not self.scrollYExtentsMin or not self.scrollYExtentsMax then
return
end
local playerPos = C_Map_GetPlayerMapPosition(self:GetParent():GetMapID(), "player")
if not playerPos then return end
local targetX, targetY = playerPos:GetXY()
targetX = Clamp(targetX, self.scrollXExtentsMin, self.scrollXExtentsMax)
targetY = Clamp(targetY, self.scrollYExtentsMin, self.scrollYExtentsMax)
-- Use instant pan to correct the position.
if self.currentScrollX ~= targetX then
self.currentScrollX = targetX
self.targetScrollX = targetX
self:SetNormalizedHorizontalScroll(targetX)
end
if self.currentScrollY ~= targetY then
self.currentScrollY = targetY
self.targetScrollY = targetY
self:SetNormalizedVerticalScroll(targetY)
end
end
end)
local updateMapFrame = CreateFrame("Frame")
-- Needed to remove tomb stone pin.
updateMapFrame:RegisterEvent("PLAYER_UNGHOST")
-- Sometimes accepting (world) quests does not remove the exlamation mark.
updateMapFrame:RegisterEvent("QUEST_ACCEPTED")
-- Just to be on the safe side.
updateMapFrame:RegisterEvent("QUEST_REMOVED")
-- Needed to change flightpoint icon colour after learning a new flightpoint.
updateMapFrame:RegisterEvent("TAXI_NODE_STATUS_CHANGED")
-- Update map after killing dungeon/raid boss.
updateMapFrame:RegisterEvent("TREASURE_PICKER_CACHE_FLUSH")
-- To refresh map after trader's tenders chest reward collected.
updateMapFrame:RegisterEvent("CHEST_REWARDS_UPDATED_FROM_SERVER")
-- To refresh map when neighborhood house ownerships change.
updateMapFrame:RegisterEvent("NEIGHBORHOOD_INFO_UPDATED")
updateMapFrame:SetScript("OnEvent", function()
-- Sometimes does not work right away.
C_Timer.NewTimer(0.5, function()
if not WorldMapFrame:IsShown() then return end
WorldMapFrame:OnMapChanged()
Addon.PlayerPingAnimation(false)
end)
end)
-- RareScanner integration: refresh RareScanner's map pins after events that
-- change pin state without triggering a world-map refresh.
--
-- Three such events exist:
--
-- (a) FILTER CHANGE via the scanner popup's two buttons. The popup (the
-- global RARESCANNER_BUTTON frame) has a FilterEntityButton (adds to
-- filter) and UnFilterEntityButton (removes from filter). Despite the
-- visual impression of a single Stop/Go toggle, these are two distinct
-- Button frames overlapping pixel-perfect at the same anchor; one is
-- always hidden and the other shown. They share the same WoW global
-- name "FilterEntityButton" (RareScanner passes the same string as the
-- second arg to both CreateFrame calls -- /framestack shows that name
-- regardless of which variant is currently visible), and are only
-- distinguished by their field name on scanner_button. Clicking either
-- calls RSConfigDB.SetNpcFiltered / SetContainerFiltered /
-- SetEventFiltered, but RareScanner does NOT refresh the world map
-- afterwards.
--
-- (b) ENTITY DETECTION via the scanner. When a vignette is detected,
-- RSButtonHandler eventually calls RSRecentlySeenTracker.AddRecentlySeen,
-- which marks the entity recently-seen (this is what flips the map pin
-- to the PINK_*_TEXTURE "just detected" variant and updates the "last
-- seen" timestamp in the pin's tooltip). AddRecentlySeen then forwards
-- to RSRecentlySeenTracker.AddPendingAnimation WITHOUT the
-- refreshWorldMap flag (see Core/Service/RSRecentlySeenTracker.lua line
-- 129), so the world map is not refreshed. After detection,
-- scanner_button:ShowButton() surfaces the popup; that method is the
-- closest reliably-hookable point to the detection.
--
-- (c) DRAGON GLYPH COLLECTION. RareScanner shows pins for uncollected
-- Skyriding/Dragonriding glyphs (see Core/Service/POI/RSDragonGlyphPOI.lua
-- :44 -- GetDragonGlyphPOIs filters out collected glyphs via
-- RSDragonGlyphDB.isDragonGlyphCollected). It marks a glyph collected
-- from two event paths in Core/Service/RSEventHandler.lua:
-- * ACHIEVEMENT_EARNED -> OnAchievementEarned (lines 510-516) when
-- the achievement itself is the glyph (synchronous).
-- * CRITERIA_EARNED -> OnCriteriaEarned -> OnAchievementCriteriaEarned
-- (lines 518-566) when the glyph is a CRITERIA of a parent zone
-- achievement. This path uses C_Timer.After(0.2, ...) to wait for
-- the achievement system to update.
-- Both call RSDragonGlyphDB.SetDragonGlyphCollected + RSMinimap.HideIcon
-- -- updating the minimap and database but NOT the world map. We listen
-- for the same events and refresh ~0.3s later (longer than RareScanner's
-- 0.2s internal wait, so the DB is already updated by the time we
-- rebuild). Per-event filtering would require access to
-- RSDragonGlyphDB.IsDragonGlyph which is private, but NotifyUpdate is
-- cheap and the events are infrequent (achievements during play), so we
-- just refresh on every CRITERIA_EARNED / ACHIEVEMENT_EARNED when the
-- map is shown.
--
-- (Other filter-change entry points already refresh on their own:
-- RareScanner's world-map dropdown button does it via
-- RSWorldMapButtonMixin:NotifyUpdate -> RSProvider.RefreshAllDataProviders.
-- The Shift+Alt+click filter on a map pin -- RSEntityPinMixin:OnMouseDown
-- in RareScanner -- calls RSProvider.RefreshAllDataProviders itself.)
--
-- We CAN'T just call WorldMapFrame:OnMapChanged() here (the way the
-- updateMapFrame handler above does for game events): that only iterates
-- WorldMapFrame.dataProviders, and RareScanner registers its pins on a
-- separate RSWorldMap.dataProviders table that Blizzard's OnMapChanged
-- doesn't touch (see RareScanner/Core/Libs/RSProvider.lua and
-- /Core/Plugins/MapPlugin/MapCanvas/RSWorldMap.lua).
--
-- We also can't hooksecurefunc RSConfigDB.SetXxxFiltered,
-- RSRecentlySeenTracker.AddRecentlySeen, or RSDragonGlyphDB.SetDragonGlyph-
-- Collected directly -- they all live in RareScanner's private addon
-- namespace (private.libs in Core/RSAddon.lua, no LibStub registration).
-- But two things ARE global and reachable:
-- * RARESCANNER_BUTTON (the scanner popup frame) and its child buttons --
-- hookable via HookScript on OnClick (filter buttons) and
-- hooksecurefunc on its :ShowButton method (detection path).
-- * RSWorldMapButtonMixin (the world-map button mixin) -- its NotifyUpdate
-- method body only references file-local RSMinimap / RSProvider upvalues,
-- not `self`, so we can invoke it from outside the addon: it triggers
-- the same RSMinimap.RefreshAllData + RSProvider.RefreshAllDataProviders
-- pair RareScanner uses for its dropdown.
do
local function DoRefresh()
if not WorldMapFrame:IsShown() then return end
if RSWorldMapButtonMixin and RSWorldMapButtonMixin.NotifyUpdate then
RSWorldMapButtonMixin.NotifyUpdate(nil)
end
end
-- Deferred variant for the detection path (case (b)): ShowAlert in
-- RSButtonHandler.lua calls button:ShowButton() at line 494 but only calls
-- RSRecentlySeenTracker.AddRecentlySeen at line 524 -- so a synchronous
-- post-hook on ShowButton would rebuild POIs BEFORE the entity is marked
-- recently-seen, and the pin texture wouldn't pick up the PINK_*_TEXTURE
-- "recently seen" variant. C_Timer.After(0, ...) defers to next frame, by
-- which time AddRecentlySeen has populated recently_seen_entities.
local function DoRefreshDeferred()
C_Timer.After(0, DoRefresh)
end
-- Deferred variant for the dragon-glyph path (case (c)): RareScanner's
-- OnAchievementCriteriaEarned wraps its DB write in C_Timer.After(0.2, ...)
-- (RSEventHandler.lua:520). We must defer past that or NotifyUpdate would
-- rebuild POIs from a DB that hasn't yet marked the glyph collected, and
-- the pin would persist until the next refresh. 0.3s gives a 100ms cushion.
local function DoRefreshAfterRSGlyphHandler()
C_Timer.After(0.3, DoRefresh)
end
local rsHookFrame = CreateFrame("Frame")
rsHookFrame:RegisterEvent("PLAYER_LOGIN")
rsHookFrame:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_LOGIN" then
self:UnregisterEvent("PLAYER_LOGIN")
local scanner = _G.RARESCANNER_BUTTON
if not scanner then return end -- RareScanner not installed; nothing to hook.
-- (a) Filter buttons. Refresh synchronously: post-hooks fire after the
-- original OnClick body returns, by which time RSConfigDB.SetXxxFiltered
-- has committed (same pattern RareScanner's pin Shift+Alt+click uses --
-- see RSEntityPinMixin.lua:81).
local function HookFilterButton(button)
if button and not button.pwm_filter_hooked then
button.pwm_filter_hooked = true
button:HookScript("OnClick", DoRefresh)
end
end
HookFilterButton(scanner.FilterEntityButton)
HookFilterButton(scanner.UnFilterEntityButton)
-- (b) Detection. hooksecurefunc (not HookScript("OnShow")) because the
-- popup may already be visible from a previous detection -- the new
-- ShowButton call updates its content without firing OnShow. We want
-- to catch every detection.
if scanner.ShowButton then
hooksecurefunc(scanner, "ShowButton", DoRefreshDeferred)
end
-- (c) Dragon glyph collection. Both events can mark a glyph collected
-- in RareScanner. Register only after we've confirmed RareScanner is
-- installed; otherwise no point waking the frame.
self:RegisterEvent("CRITERIA_EARNED")
self:RegisterEvent("ACHIEVEMENT_EARNED")
elseif event == "CRITERIA_EARNED" or event == "ACHIEVEMENT_EARNED" then
-- (c) Refresh after RareScanner's 0.2s timer has updated the glyph DB.
DoRefreshAfterRSGlyphHandler()
end
end)
end
-- -- In case you ever need this again.
-- function RedrawBlobs()
-- for provider, _ in pairs(WorldMapFrame.dataProviders) do
-- if type(provider) == "table" then
-- -- if provider.AddQuest then
-- -- print("Found QuestDataProvider")
-- -- provider:RefreshAllData() -- Working!
-- -- end
-- -- Minimal alternative to redraw blob. Unfortunately does not fix flicker.
-- if provider.IsShowingWorldQuests then
-- -- print("Found QuestBlobDataProvider")
-- -- provider:OnMapChanged()
-- -- provider.pin:OnMapChanged()
-- -- provider.pin:Refresh()
-- provider.pin:DrawNone()
-- provider.pin:DrawBlob(provider.pin.questID, true)
-- end
-- end
-- end
-- end