-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathQuestLogCollapse.lua
More file actions
1262 lines (1118 loc) · 57.7 KB
/
Copy pathQuestLogCollapse.lua
File metadata and controls
1262 lines (1118 loc) · 57.7 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
-- QuestLogCollapse: Automatically collapses quest log when entering dungeons
-- Author: Gaspode
-- Contributors: Artherion77
-- Version: 1.4.0
-- Updated: 2024-06-14
-- TAINT PROTECTION STRATEGY:
-- Implemented namespace to avoid global variable pollution
-- Added extensive error handling and logging to detect and isolate taint issues
-- If this resolves taint issues, will remove previous mitigation strategies (disabling collapsing of certain trackers) in future updates
-- Use addon namespace to prevent global variable pollution and taint
local addonName, ns = ...
-- Create addon frame (local to prevent global pollution)
local QuestLogCollapse = CreateFrame("Frame")
QuestLogCollapse:RegisterEvent("ADDON_LOADED")
QuestLogCollapse:RegisterEvent("ZONE_CHANGED_NEW_AREA")
QuestLogCollapse:RegisterEvent("PLAYER_REGEN_DISABLED")
QuestLogCollapse:RegisterEvent("PLAYER_REGEN_ENABLED")
QuestLogCollapse:RegisterEvent("PLAYER_ENTERING_WORLD")
QuestLogCollapse:RegisterEvent("PLAYER_STARTED_MOVING")
QuestLogCollapse:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
QuestLogCollapse:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED")
-- Trackers that must never be collapsed from addon Lua code.
-- SetCollapsed() on any tracker that contains UIWidget-based content (status bars,
-- capsules) causes UIWidgetManager:ProcessWidget → InitPartitions to run in a tainted
-- execution context, which permanently taints the pooled StatusBar frame widths.
-- Those poisoned widths then surface as "secret number" errors in LayoutFrame.lua
-- and Blizzard_UIWidgetTemplateBase.lua whenever the GameTooltip or WorldMap later
-- tries to lay out widget sets. The pool frames are never size-reset on release,
-- so the taint persists until /reload.
--
-- Keys here are the 'name' strings passed to SafeCollapseTracker/SafeExpandTracker.
-- Dynamic per-session additions are also written here when a pcall catches a taint error.
local TAINT_BLACKLIST = {
["UI widgets"] = true, -- UIWidgetObjectiveTracker: directly manages widget pool frames
["Monthly activities"] = false, -- MonthlyActivitiesObjectiveTracker: UIWidget status bars for seasonal/event progress
["Adventure map"] = false, -- AdventureMapQuestObjectiveTracker: causes world map taint (collapseAdventureMaps defaults false; safety net for users who enable it)
["World quest"] = false, -- WorldQuestObjectiveTracker: causes map system taint (collapseWorldQuests defaults false; safety net for users who enable it)
["Bonus objectives"] = false, -- BonusObjectiveTracker: SetCollapsed taints UIWidget pool frame widths → LayoutFrame:491 "secret number" on Area POI tooltips
["Quest"] = false, -- QuestObjectiveTracker: SetCollapsed taints widget pool frame dimensions when the tracker holds quests with embedded reward / UIWidget content (delve coffer-key timers, world quest reward icons). Confirmed reproducer: tracked World Quest in the log → hover any World Quest pin on the world map → 38× "attempt to perform arithmetic on a secret number value" at Blizzard_GameTooltip/Mainline/GameTooltip.lua:754 (EmbeddedItemTooltip_UpdateSize). Also surfaces at LayoutFrame:491 / UIWidgetTemplateTextWithState:35 in Area POI tooltips.
}
-- Helper function to check if a value is tainted
local function IsTainted(value)
if type(value) == "number" then
-- Try to use the number in a protected operation
local success = securecall(function()
local _ = value + 0
return true
end)
return not success
end
return false
end
-- Taint-safe deferral logic
local mapSystemBusy = false
local mapSystemBusyUntil = 0
local pendingOperations = {}
local function SetMapSystemBusy(seconds)
mapSystemBusy = true
mapSystemBusyUntil = GetTime() + seconds
end
local function CheckMapSystemBusy()
if mapSystemBusy and GetTime() > mapSystemBusyUntil then
mapSystemBusy = false
mapSystemBusyUntil = 0
-- Process pending operations
for _, op in ipairs(pendingOperations) do
if op.action == "collapse" then
op.func()
elseif op.action == "expand" then
op.func()
end
end
pendingOperations = {}
end
end
local busyFrame = CreateFrame("Frame")
busyFrame:SetScript("OnUpdate", function()
CheckMapSystemBusy()
end)
-- Track loading state to prevent operations during initialization
local isFullyLoaded = false
-- Track combat state for delayed operations
local combatStateQueue = {
shouldCollapseOnCombatEnd = false,
shouldExpandOnCombatEnd = false,
enteredCombatOutsideInstance = false,
trackersWereCollapsedInCombat = false
}
-- Track nameplate state to restore properly
local namePlateState = {
originalShowAll = nil, -- Original value before addon touched it
addonControlled = false -- Whether the addon is currently controlling nameplates
}
-- Track quest tracking state to restore properly
local questTrackingState = {
originalTrackedQuests = {}, -- Store original tracked quest IDs
addonModifiedTracking = false -- Whether the addon has modified quest tracking
}
-- Track if zone filtering is needed (set on zone change, cleared when filtering runs)
-- This flag approach allows the filter to run from hardware-initiated events without taint
local needsZoneFilter = false
-- "Have we installed the HookScript already?" guards. Kept as file-locals rather
-- than as fields on the Blizzard frames themselves: writing a custom key onto a
-- Blizzard frame from addon-tainted code marks the frame's table as tainted,
-- which subsequently blocks every protected operation dispatched against it
-- (e.g. ObjectiveTrackerFrame:Show / mixin'd methods on world-map open). See
-- the commit that introduced these for the ADDON_ACTION_BLOCKED Frame:Show
-- repro.
local mapFrameHooked = false
local minimizeButtonHooked = false
-- Default settings
local defaults = {
enabled = true,
debug = false,
filterQuestsByZone = false,
filterQuestsByZoneMode = "openworld", -- "openworld" skips filtering inside instances
collapseQuests = false, -- Disabled by default - causes taint
collapseAchievements = true,
collapseBonusObjectives = false, -- Disabled by default - causes area POI taint
collapseScenarios = true,
collapseCampaigns = true,
collapseProfessions = true,
collapseMonthlyActivities = false, -- Disabled by default - UIWidget status bars cause taint
collapseUIWidgets = false, -- Disabled by default - causes widget taint
collapseAdventureMaps = false, -- Disabled by default - causes map taint
collapseWorldQuests = false, -- Disabled by default - causes map taint
namePlates = { enabled = false }
}
-- Initialize saved variables
QuestLogCollapseDB = QuestLogCollapseDB or {}
-- Debug print function (stored in namespace)
local function DebugPrint(message)
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if profile and profile.debug then
print("|cff00ff00[QuestLogCollapse]|r " .. message)
end
end
-- Make DebugPrint available to other addon files via namespace
ns.DebugPrint = DebugPrint
-- Expose taint blacklist so the config UI can gate toggles for trackers we refuse to collapse
ns.TAINT_BLACKLIST = TAINT_BLACKLIST
local function IsInDungeon()
local instanceType = select(2, IsInInstance())
return instanceType == "party" or instanceType == "raid" or instanceType == "scenario" or instanceType == "pvp" or
instanceType == "arena" or instanceType == "neighborhood" or instanceType == "interior"
end
-- Safe function to collapse a tracker using secure methods
local function SafeCollapseTracker(tracker, name, shouldCollapse)
if not isFullyLoaded or not tracker or not shouldCollapse then
return false
end
-- NEVER manipulate trackers during combat to avoid taint
if InCombatLockdown() then
DebugPrint("Skipping " .. name .. " collapse - in combat")
return false
end
-- Skip blacklisted trackers that cause taint
if TAINT_BLACKLIST[name] then
DebugPrint("Skipping " .. name .. " collapse - blacklisted (causes UI taint)")
return false
end
-- Also check by tracker object reference
local trackerName = tracker and tracker:GetName()
if trackerName and TAINT_BLACKLIST[trackerName] then
DebugPrint("Skipping " .. name .. " collapse - blacklisted by object name (" .. trackerName .. ")")
return false
end
-- Avoid operations when map system might be busy
if mapSystemBusy then
DebugPrint("Deferring " .. name .. " collapse - map system busy")
-- Store for later execution
pendingOperations[name] = {action = "collapse", tracker = tracker}
return true
end
-- Check if this operation is already pending
if pendingOperations[name] then
DebugPrint("Operation already pending for " .. name)
return true
end
-- Use secure execution with frame script
local success = false
-- Wrap in error handler to catch and suppress taint errors
if tracker.SetCollapsed and type(tracker.SetCollapsed) == "function" then
local executeFrame = CreateFrame("Frame")
executeFrame:SetScript("OnUpdate", function(self)
self:SetScript("OnUpdate", nil)
if InCombatLockdown() then
DebugPrint("Combat started, aborting " .. name .. " collapse")
return
end
local ok, err = pcall(function()
if tracker and tracker.SetCollapsed then
tracker:SetCollapsed(true)
end
end)
if ok then
DebugPrint(name .. " section collapsed successfully")
success = true
else
-- Taint error - log but don't propagate
if string.find(err or "", "taint") then
DebugPrint("Warning: Taint detected when collapsing " .. name .. ": " .. tostring(err))
-- Add to taint blacklist for this session
TAINT_BLACKLIST[name] = true
else
-- Don't try a `tracker.collapsed = true` direct property write as a
-- fallback — that taints the Blizzard tracker frame just as badly as
-- the SetCollapsed API call (writing arbitrary keys onto a Blizzard
-- frame from addon-tainted code marks the table as tainted, blocking
-- every later protected operation on it). Leave the tracker in
-- whatever state SetCollapsed left it.
DebugPrint("SetCollapsed failed for " .. name .. ": " .. tostring(err) .. " (no fallback — direct property write would taint)")
end
end
end)
end
return true
end
local function CollapseQuestLog()
-- NEVER do anything during combat to avoid taint
if InCombatLockdown() then
DebugPrint("CollapseQuestLog() skipped - in combat")
return
end
-- Get instance-specific settings from config system
local settings = ns.GetCurrentInstanceSettings and ns.GetCurrentInstanceSettings()
DebugPrint("CollapseQuestLog() called")
if not settings then
DebugPrint("No instance settings found")
return
end
if not settings.enabled then
DebugPrint("Instance type not enabled for collapsing")
return
end
DebugPrint("Instance settings found and enabled, proceeding with collapse")
local collapsed = 0
-- Use safe collapse function for all trackers
if SafeCollapseTracker(QuestObjectiveTracker, "Quest", settings.collapseQuests) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(AchievementObjectiveTracker, "Achievement", settings.collapseAchievements) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(BonusObjectiveTracker, "Bonus objectives", settings.collapseBonusObjectives) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(ScenarioObjectiveTracker, "Scenario", settings.collapseScenarios) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(CampaignQuestObjectiveTracker, "Campaign", settings.collapseCampaigns) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(ProfessionsRecipeTracker, "Professions", settings.collapseProfessions) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(MonthlyActivitiesObjectiveTracker, "Monthly activities", settings.collapseMonthlyActivities) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(UIWidgetObjectiveTracker, "UI widgets", settings.collapseUIWidgets) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(_G["AdventureMapQuestObjectiveTracker"], "Adventure map", settings.collapseAdventureMaps) then
collapsed = collapsed + 1
end
if SafeCollapseTracker(WorldQuestObjectiveTracker, "World quest", settings.collapseWorldQuests) then
collapsed = collapsed + 1
end
DebugPrint("Collapsed " .. collapsed .. " sections")
-- Handle nameplate settings (only if not in combat)
if settings.namePlates and settings.namePlates.enabled and not InCombatLockdown() then
DebugPrint("Enabling ENEMY nameplates for instance")
-- Store original state before changing it (only if we haven't already)
if not namePlateState.addonControlled then
namePlateState.originalShowAll = GetCVar("nameplateShowEnemies")
DebugPrint("Stored original ENEMY nameplate state: " .. tostring(namePlateState.originalShowAll))
-- Debug: Show current state of all nameplate CVars
DebugPrint("Before change - nameplateShowAll: " .. tostring(GetCVar("nameplateShowAll")))
DebugPrint("Before change - nameplateShowEnemies: " .. tostring(GetCVar("nameplateShowEnemies")))
DebugPrint("Before change - nameplateShowFriends: " .. tostring(GetCVar("nameplateShowFriends")))
end
namePlateState.addonControlled = true
SetCVar("nameplateShowEnemies", "1")
-- Debug: Show state after change
DebugPrint("After change - nameplateShowAll: " .. tostring(GetCVar("nameplateShowAll")))
DebugPrint("After change - nameplateShowEnemies: " .. tostring(GetCVar("nameplateShowEnemies")))
DebugPrint("After change - nameplateShowFriends: " .. tostring(GetCVar("nameplateShowFriends")))
end
end
-- Safe function to expand a tracker using secure methods
local function SafeExpandTracker(tracker, name)
if not isFullyLoaded or not tracker then
DebugPrint(name .. " not found or not fully loaded")
return false
end
-- NEVER manipulate trackers during combat to avoid taint
if InCombatLockdown() then
DebugPrint("Skipping " .. name .. " expand - in combat")
return false
end
-- Skip blacklisted trackers that cause taint
if TAINT_BLACKLIST[name] then
DebugPrint("Skipping " .. name .. " expand - blacklisted (causes UI taint)")
return false
end
-- Also check by tracker object reference
local trackerName = tracker and tracker:GetName()
if trackerName and TAINT_BLACKLIST[trackerName] then
DebugPrint("Skipping " .. name .. " expand - blacklisted by object name (" .. trackerName .. ")")
return false
end
-- Avoid operations when map system might be busy
if mapSystemBusy then
DebugPrint("Deferring " .. name .. " expand - map system busy")
-- Store for later execution
pendingOperations[name] = {action = "expand", tracker = tracker}
return true
end
-- Check if this operation is already pending
if pendingOperations[name] then
DebugPrint("Operation already pending for " .. name)
return true
end
-- Use secure execution with frame script
local success = false
-- Wrap in error handler to catch and suppress taint errors
if tracker.SetCollapsed and type(tracker.SetCollapsed) == "function" then
local executeFrame = CreateFrame("Frame")
executeFrame:SetScript("OnUpdate", function(self)
self:SetScript("OnUpdate", nil)
if InCombatLockdown() then
DebugPrint("Combat started, aborting " .. name .. " expand")
return
end
local ok, err = pcall(function()
if tracker and tracker.SetCollapsed then
tracker:SetCollapsed(false)
end
end)
if ok then
DebugPrint(name .. " section expanded successfully")
success = true
else
-- Taint error - log but don't propagate
if string.find(err or "", "taint") then
DebugPrint("Warning: Taint detected when expanding " .. name .. ": " .. tostring(err))
-- Add to taint blacklist for this session
TAINT_BLACKLIST[name] = true
else
-- Don't try a `tracker.collapsed = false` direct property write as
-- a fallback — same anti-pattern as SafeCollapseTracker's removed
-- Method 2.
DebugPrint("SetCollapsed(false) failed for " .. name .. ": " .. tostring(err) .. " (no fallback — direct property write would taint)")
end
end
end)
end
return true
end
local function ExpandQuestLog()
-- NEVER do anything during combat to avoid taint
if InCombatLockdown() then
DebugPrint("ExpandQuestLog() skipped - in combat")
return
end
-- When leaving an instance, expand all sections regardless of settings
-- This ensures we restore the original state
DebugPrint("ExpandQuestLog() called")
local expanded = 0
-- Use safe expand function for all trackers
if SafeExpandTracker(QuestObjectiveTracker, "Quest") then
expanded = expanded + 1
end
if SafeExpandTracker(AchievementObjectiveTracker, "Achievement") then
expanded = expanded + 1
end
if SafeExpandTracker(BonusObjectiveTracker, "Bonus objectives") then
expanded = expanded + 1
end
if SafeExpandTracker(ScenarioObjectiveTracker, "Scenario") then
expanded = expanded + 1
end
if SafeExpandTracker(CampaignQuestObjectiveTracker, "Campaign") then
expanded = expanded + 1
end
if SafeExpandTracker(ProfessionsRecipeTracker, "Professions") then
expanded = expanded + 1
end
if SafeExpandTracker(MonthlyActivitiesObjectiveTracker, "Monthly activities") then
expanded = expanded + 1
end
if SafeExpandTracker(UIWidgetObjectiveTracker, "UI widgets") then
expanded = expanded + 1
end
if SafeExpandTracker(_G["AdventureMapQuestObjectiveTracker"], "Adventure map") then
expanded = expanded + 1
end
if SafeExpandTracker(WorldQuestObjectiveTracker, "World quest") then
expanded = expanded + 1
end
DebugPrint("Expanded " .. expanded .. " sections/modules")
-- Restore nameplate settings (only if the addon was controlling them)
if namePlateState.addonControlled and not InCombatLockdown() then
DebugPrint("Restoring original ENEMY nameplate state: " .. tostring(namePlateState.originalShowAll))
-- Debug: Show current state before restoration
DebugPrint("Before restore - nameplateShowAll: " .. tostring(GetCVar("nameplateShowAll")))
DebugPrint("Before restore - nameplateShowEnemies: " .. tostring(GetCVar("nameplateShowEnemies")))
DebugPrint("Before restore - nameplateShowFriends: " .. tostring(GetCVar("nameplateShowFriends")))
SetCVar("nameplateShowEnemies", namePlateState.originalShowAll or "0")
-- Debug: Show state after restoration
DebugPrint("After restore - nameplateShowAll: " .. tostring(GetCVar("nameplateShowAll")))
DebugPrint("After restore - nameplateShowEnemies: " .. tostring(GetCVar("nameplateShowEnemies")))
DebugPrint("After restore - nameplateShowFriends: " .. tostring(GetCVar("nameplateShowFriends")))
namePlateState.addonControlled = false
namePlateState.originalShowAll = nil
end
end
-- Filter quests by current zone
-- This function is only safe to call from:
-- 1. Slash commands (user-initiated)
-- 2. Hardware-event hooks (map open, quest log open, player movement)
-- 3. Frame OnShow events triggered by user action
-- NEVER call from ZONE_CHANGED or other game events - it will cause taint!
local function FilterQuestsByZone()
-- NEVER do anything during combat to avoid taint
if InCombatLockdown() then
DebugPrint("FilterQuestsByZone() skipped - in combat")
return
end
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if not profile or not profile.filterQuestsByZone then
needsZoneFilter = false -- Clear the flag
return
end
-- Respect the zone filter mode: skip when inside any instance if set to open-world only
local filterMode = profile.filterQuestsByZoneMode or "openworld"
if filterMode == "openworld" and IsInInstance() then
DebugPrint("FilterQuestsByZone() skipped - in instance (mode: Open World Only)")
needsZoneFilter = false
return
end
-- Clear the flag since we're running now
needsZoneFilter = false
DebugPrint("========================================")
DebugPrint("=== FILTERING QUESTS BY CURRENT ZONE ===")
DebugPrint("========================================")
C_Timer.After(0.5, function()
if InCombatLockdown() then
DebugPrint("Combat started, skipping quest filtering")
return
end
-- Get current zone
local currentMapID = C_Map.GetBestMapForUnit("player")
local currentMapInfo = currentMapID and C_Map.GetMapInfo(currentMapID)
DebugPrint("Current map ID: " .. tostring(currentMapID))
if currentMapInfo then
DebugPrint("Current map name: '" .. (currentMapInfo.name or "unknown") .. "'")
end
if not currentMapID then
DebugPrint("Unable to determine current zone, skipping quest filtering")
return
end
-- Helper function to check if a quest is in the current zone
local function IsQuestInCurrentZone(questID, questInfo)
-- Check if the quest has markers or objectives in the current zone
-- isOnMap = quest objectives/markers are on the current zone's map
-- hasLocalPOI = quest has a Point of Interest in the current zone
local isOnCurrentMap = questInfo and questInfo.isOnMap
local hasLocalMarker = questInfo and questInfo.hasLocalPOI
DebugPrint(" Quest " .. questID .. ": isOnMap=" .. tostring(isOnCurrentMap) .. ", hasLocalPOI=" .. tostring(hasLocalMarker))
if isOnCurrentMap or hasLocalMarker then
return true, "has objectives/markers in current zone"
else
return false, "no objectives/markers in current zone"
end
end
-- Step 1: Untrack quests not in current zone
local untracked = 0
local kept = 0
-- Get the number of tracked quests
local numTracked = C_QuestLog.GetNumQuestWatches()
DebugPrint("=== STEP 1: Checking " .. numTracked .. " currently tracked quests ===")
-- Snapshot the watch list before modifying it so we can restore it later
if not questTrackingState.addonModifiedTracking then
questTrackingState.originalTrackedQuests = {}
for i = 1, numTracked do
local qid = C_QuestLog.GetQuestIDForQuestWatchIndex(i)
if qid then questTrackingState.originalTrackedQuests[qid] = true end
end
questTrackingState.addonModifiedTracking = true
DebugPrint("Saved original quest tracking state (" .. numTracked .. " quests)")
end
-- Iterate through tracked quests (iterate backwards to avoid index issues when removing)
for i = numTracked, 1, -1 do
local questID = C_QuestLog.GetQuestIDForQuestWatchIndex(i)
if questID then
-- Find the quest info for this tracked quest
local trackedQuestInfo = nil
for j = 1, C_QuestLog.GetNumQuestLogEntries() do
local info = C_QuestLog.GetInfo(j)
if info and info.questID == questID then
trackedQuestInfo = info
break
end
end
DebugPrint("Examining tracked quest " .. questID .. " (index " .. i .. ")")
local isInCurrentZone, reason = IsQuestInCurrentZone(questID, trackedQuestInfo)
if not isInCurrentZone then
C_QuestLog.RemoveQuestWatch(questID)
DebugPrint(">>> UNTRACKED quest " .. questID .. " - " .. reason)
untracked = untracked + 1
else
DebugPrint(">>> KEPT quest " .. questID .. " - " .. reason)
kept = kept + 1
end
else
DebugPrint("Warning: No questID at watch index " .. i)
end
end
DebugPrint("=== Step 1 complete: kept " .. kept .. ", untracked " .. untracked .. " ===")
-- Step 2: Track quests that ARE in current zone
local tracked = 0
local skipped = 0
local numQuestLogEntries = C_QuestLog.GetNumQuestLogEntries()
DebugPrint("=== STEP 2: Scanning " .. numQuestLogEntries .. " quest log entries for current zone quests ===")
for i = 1, numQuestLogEntries do
local info = C_QuestLog.GetInfo(i)
if info and not info.isHeader and not info.isHidden then
local questID = info.questID
if questID then
DebugPrint("Checking quest log entry " .. i .. ": questID=" .. questID .. ", title='" .. (info.title or "unknown") .. "'")
-- Check if quest is already tracked
local alreadyTracked = false
for j = 1, C_QuestLog.GetNumQuestWatches() do
if C_QuestLog.GetQuestIDForQuestWatchIndex(j) == questID then
alreadyTracked = true
break
end
end
if alreadyTracked then
DebugPrint(" Quest " .. questID .. " already tracked, skipping")
skipped = skipped + 1
else
local isInCurrentZone, reason = IsQuestInCurrentZone(questID, info)
if isInCurrentZone then
local success = C_QuestLog.AddQuestWatch(questID)
if success then
DebugPrint(">>> TRACKED quest " .. questID .. " - " .. reason)
tracked = tracked + 1
else
DebugPrint(">>> FAILED to track quest " .. questID .. " (AddQuestWatch returned false)")
end
else
DebugPrint(" Quest " .. questID .. " not in current zone - " .. reason)
end
end
end
end
end
DebugPrint("=== Step 2 complete: newly tracked " .. tracked .. ", skipped (already tracked) " .. skipped .. " ===")
DebugPrint("=== FINAL: kept " .. kept .. ", untracked " .. untracked .. ", newly tracked " .. tracked .. " quests ===")
end)
end
local function OnZoneChanged()
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
-- Set flag for zone filtering - will be triggered by user action (map open, movement, etc.)
-- We can't call FilterQuestsByZone() directly here because it would cause taint
if profile and profile.filterQuestsByZone then
needsZoneFilter = true
DebugPrint("Zone changed - zone filter will run on next user action (open map, move, or use /qlc filterzone)")
end
if not profile or not profile.enabled then
DebugPrint("Addon disabled or no profile found (skipping collapse/expand)")
return
end
DebugPrint("Zone change detected, checking instance status...")
-- Set a flag to indicate map system might be busy
mapSystemBusy = true
-- Reset the flag after a MUCH longer delay to be extra safe
-- The quest data provider needs 30+ seconds to fully initialize after zone change
C_Timer.After(30.0, function()
mapSystemBusy = false
-- Process any pending operations
if next(pendingOperations) then
DebugPrint("Processing pending tracker operations")
C_Timer.After(1.0, function()
for name, operation in pairs(pendingOperations) do
if not InCombatLockdown() and operation.tracker then
if operation.action == "collapse" then
DebugPrint("Executing pending collapse for " .. name)
SafeCollapseTracker(operation.tracker, name, true)
elseif operation.action == "expand" then
DebugPrint("Executing pending expand for " .. name)
SafeExpandTracker(operation.tracker, name)
end
end
end
pendingOperations = {} -- Clear pending operations
end)
end
end)
-- Add an even longer delay to ensure all Blizzard systems are fully initialized
-- Wait 30+ seconds after zone change before touching quest log
-- This is critical to avoid interfering with quest system initialization
C_Timer.After(30.0, function()
-- Double-check that we're not in combat before proceeding
if InCombatLockdown() then
DebugPrint("Skipping zone change handling - in combat")
return
end
-- Additional check to avoid interference during map operations
if mapSystemBusy then
DebugPrint("Map system may be busy, deferring tracker operations")
C_Timer.After(5.0, function()
if not InCombatLockdown() then
local inInstance, instanceType = IsInInstance()
DebugPrint("Deferred instance check: inInstance=" .. tostring(inInstance) .. ", type=" .. tostring(instanceType))
if IsInDungeon() then
DebugPrint("Entered instance - collapsing configured sections (deferred)")
CollapseQuestLog()
else
DebugPrint("Left instance - expanding all collapsed sections (deferred)")
ExpandQuestLog()
end
end
end)
return
end
local inInstance, instanceType = IsInInstance()
DebugPrint("Instance check: inInstance=" .. tostring(inInstance) .. ", type=" .. tostring(instanceType))
if IsInDungeon() then
DebugPrint("Entered instance - collapsing configured sections")
CollapseQuestLog()
else
DebugPrint("Left instance - expanding all collapsed sections")
ExpandQuestLog()
end
end)
end
local function OnAddonLoaded(addonName)
if addonName ~= "QuestLogCollapse" then
return
end
-- Basic initialization - detailed config handled by config file
for key, value in pairs(defaults) do
if QuestLogCollapseDB[key] == nil then
QuestLogCollapseDB[key] = value
end
end
print("|cff00ff00QuestLogCollapse|r v1.2.8 loaded. Type |cffff0000/qlc config|r for options.")
print("|cffff9900[QuestLogCollapse]|r Note: Some trackers (Quests, Bonus Objectives, World Quests) are disabled by default to prevent UI taint. Enable at your own risk.")
-- Check initial state with a MUCH longer delay to avoid conflicts during addon loading
-- Give the map system and all other Blizzard systems plenty of time to fully initialize
-- At least 30+ seconds is needed to ensure quest system is ready
C_Timer.After(30.0, function()
if IsInDungeon() then
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if profile and profile.enabled and not InCombatLockdown() then
DebugPrint("Initial state check: in dungeon, applying collapse")
CollapseQuestLog()
end
end
end)
end
local function OnCombatStateChanged(event)
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if not profile or not profile.enabled then
DebugPrint("Addon disabled or no profile found")
return
end
-- Make sure we're not in a dungeon to avoid conflicts
if not IsInDungeon() then
if event == "PLAYER_REGEN_DISABLED" then
-- Check if combat collapse is enabled
local settings = ns.GetCurrentInstanceSettings and ns.GetCurrentInstanceSettings()
if settings and settings.enabled then
DebugPrint("PLAYER_REGEN_DISABLED fired - attempting immediate collapse")
-- Try to collapse immediately. SafeCollapseTracker can't run during
-- combat, so this direct path is the only one that can take effect
-- before InCombatLockdown blocks further frame manipulation.
local collapsed = 0
-- Attempt immediate collapse of each enabled tracker
-- Trackers in TAINT_BLACKLIST are skipped here too, mirroring SafeCollapseTracker.
-- Without this gate, the immediate path bypasses the blacklist on collapse while
-- SafeExpandTracker still honors it on expand, leaving blacklisted trackers stuck
-- collapsed forever. See TAINT_BLACKLIST comments for the underlying taint chains.
local immediates = {
{ settings.collapseQuests, QuestObjectiveTracker, "Quest" },
{ settings.collapseAchievements, AchievementObjectiveTracker, "Achievement" },
{ settings.collapseBonusObjectives, BonusObjectiveTracker, "Bonus objectives" },
{ settings.collapseCampaigns, CampaignQuestObjectiveTracker, "Campaign" },
{ settings.collapseWorldQuests, WorldQuestObjectiveTracker, "World quest" },
}
for _, def in ipairs(immediates) do
local wanted, tracker, name = def[1], def[2], def[3]
if wanted and tracker then
if TAINT_BLACKLIST[name] then
DebugPrint("Skipping " .. name .. " immediate collapse - blacklisted (causes UI taint)")
else
local ok, err = pcall(function()
if tracker.SetCollapsed then
tracker:SetCollapsed(true)
collapsed = collapsed + 1
end
end)
if ok then
DebugPrint(name .. " tracker immediately collapsed in combat")
else
DebugPrint("Failed to immediately collapse " .. name .. " tracker: " .. tostring(err))
end
end
end
end
if collapsed > 0 then
DebugPrint("Successfully collapsed " .. collapsed .. " trackers immediately in combat")
-- Mark that we successfully collapsed and need to expand on combat end
combatStateQueue.enteredCombatOutsideInstance = true
combatStateQueue.shouldCollapseOnCombatEnd = false
combatStateQueue.shouldExpandOnCombatEnd = false
combatStateQueue.trackersWereCollapsedInCombat = true
else
DebugPrint("No trackers could be collapsed immediately - queuing for after combat")
-- Queue the operation for after combat ends
combatStateQueue.enteredCombatOutsideInstance = true
combatStateQueue.shouldCollapseOnCombatEnd = true
combatStateQueue.shouldExpandOnCombatEnd = false
combatStateQueue.trackersWereCollapsedInCombat = false
DebugPrint("Queuing remaining collapse operations for when combat ends")
end
else
DebugPrint("Combat collapse not enabled for this profile")
-- Still mark that we entered combat outside instance in case user manually interacts
combatStateQueue.enteredCombatOutsideInstance = true
combatStateQueue.shouldCollapseOnCombatEnd = false
combatStateQueue.shouldExpandOnCombatEnd = false
end
elseif event == "PLAYER_REGEN_ENABLED" then
DebugPrint("Leaving combat - checking queued operations and quest log state")
if combatStateQueue.enteredCombatOutsideInstance and combatStateQueue.shouldCollapseOnCombatEnd then
DebugPrint("Applying queued collapse operation after combat")
CollapseQuestLog()
combatStateQueue.shouldCollapseOnCombatEnd = false
combatStateQueue.trackersWereCollapsedInCombat = true
elseif combatStateQueue.shouldExpandOnCombatEnd then
DebugPrint("Applying queued expand operation after combat")
ExpandQuestLog()
combatStateQueue.shouldExpandOnCombatEnd = false
elseif combatStateQueue.enteredCombatOutsideInstance and combatStateQueue.trackersWereCollapsedInCombat then
-- If we were in combat outside instances and trackers were collapsed,
-- expand the quest log when combat ends to restore original state
local settings = ns.GetCurrentInstanceSettings and ns.GetCurrentInstanceSettings()
if settings and settings.enabled then
DebugPrint("Combat ended outside instance - expanding quest log to restore original state")
ExpandQuestLog()
else
DebugPrint("Combat collapse not enabled - no expansion needed")
end
elseif combatStateQueue.enteredCombatOutsideInstance then
DebugPrint("Combat ended outside instance but no trackers were collapsed - no expansion needed")
end
-- Reset combat tracking
combatStateQueue.enteredCombatOutsideInstance = false
combatStateQueue.trackersWereCollapsedInCombat = false
end
else
DebugPrint("In dungeon/instance - skipping combat state change handling")
end
end
-- Event handler
QuestLogCollapse:SetScript("OnEvent", function(self, event, ...)
if event == "ADDON_LOADED" then
local addonName = ...
if addonName == "QuestLogCollapse" then
SetMapSystemBusy(15)
end
elseif event == "PLAYER_ENTERING_WORLD" then
-- Mark as fully loaded after player enters world
isFullyLoaded = true
DebugPrint("Player entered world - addon fully loaded")
elseif event == "ZONE_CHANGED_NEW_AREA" then
SetMapSystemBusy(30)
OnZoneChanged()
elseif event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" then
-- Handle combat options
OnCombatStateChanged(event)
elseif event == "PLAYER_STARTED_MOVING" then
-- Player movement - check for pending zone filter
-- This is typically hardware-initiated (WASD keys)
if needsZoneFilter and not InCombatLockdown() then
DebugPrint("Player started moving - running pending zone filter")
FilterQuestsByZone()
end
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
local unitTarget, castGUID, spellID = ...
-- Only respond to player's own spells
if unitTarget == "player" and needsZoneFilter and not InCombatLockdown() then
DebugPrint("Player cast spell/ability - running pending zone filter")
FilterQuestsByZone()
end
elseif event == "PLAYER_MOUNT_DISPLAY_CHANGED" then
-- Player mounted/dismounted - check for pending zone filter
-- Mounting is always hardware-initiated (button press)
if needsZoneFilter and not InCombatLockdown() then
DebugPrint("Player mount state changed - running pending zone filter")
FilterQuestsByZone()
end
end
end)
-- Periodic garbage collection during low-activity periods
local gcFrame = CreateFrame("Frame")
local lastGCTime = GetTime()
gcFrame:SetScript("OnUpdate", function()
local currentTime = GetTime()
-- Run garbage collection every 5 minutes (300 seconds) if not in combat/instance
if currentTime - lastGCTime > 300 then
if not InCombatLockdown() and not IsInDungeon() then
DebugPrint("Running periodic garbage collection")
collectgarbage("collect")
lastGCTime = currentTime
end
end
end)
-- Slash command handler
SLASH_QUESTLOGCOLLAPSE1 = "/qlc"
SLASH_QUESTLOGCOLLAPSE2 = "/questlogcollapse"
function SlashCmdList.QUESTLOGCOLLAPSE(msg)
local args = {}
for word in msg:gmatch("%S+") do
table.insert(args, word:lower())
end
if #args == 0 or args[1] == "help" then
print("|cff00ff00QuestLogCollapse Commands:|r")
print("|cffff0000/qlc toggle|r - Toggle addon on/off")
print("|cffff0000/qlc debug|r - Toggle debug messages")
print("|cffff0000/qlc status|r - Show current status and combat queue")
print("|cffff0000/qlc collapse|r - Manually collapse configured sections")
print("|cffff0000/qlc expand|r - Manually expand all collapsed sections")
print("|cffff0000/qlc filterzone|r - Filter quests by current zone (manual)")
print("|cffff0000/qlc test|r - Test objective tracker detection")
print("|cffff0000/qlc testcombat|r - Test combat collapse behavior")
print("|cffff0000/qlc clearpending|r - Clear pending tracker operations")
print("|cffff0000/qlc config|r - Open configuration panel")
print("")
print("|cff00ff00Combat Behavior:|r")
print("• Quest trackers collapse on PLAYER_REGEN_DISABLED (combat start)")
print("• Trackers in the runtime taint blacklist are skipped")
print("• Quest trackers automatically expand when combat ends (outside instances)")
print("• If immediate collapse fails, operations are queued for when combat ends")
print("• Use |cffff0000/qlc expand|r during combat to cancel queued operations")
print("")
print("|cff00ff00Zone Filtering:|r")
print("• When enabled, zone filtering triggers automatically when you:")
print(" - Interact with the quest tracker (minimize/expand)")
print(" - Start moving after a zone change")
print(" - Cast any spell/ability (including dynamic flight)")
print(" - Mount or dismount")
print("• You can also manually trigger with |cffff0000/qlc filterzone|r")
print("Available sections: quests, achievements, bonus, scenarios,")
print("campaigns, professions, monthly, widgets, adventuremaps")
elseif args[1] == "toggle" then
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if profile then
profile.enabled = not profile.enabled
print("|cff00ff00QuestLogCollapse|r " .. (profile.enabled and "enabled" or "disabled"))
end
elseif args[1] == "debug" then
local profile = (ns.GetCurrentQLCProfile and ns.GetCurrentQLCProfile()) or QuestLogCollapseDB
if profile then