Repository navigation
Expand file tree
/
Copy pathfilter.lua
More file actions
executable file
·330 lines (283 loc) · 11.5 KB
/
Copy pathfilter.lua
File metadata and controls
executable file
·330 lines (283 loc) · 11.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
if not Cursive.superwow then
return
end
-- Local-cache frequently used globals
local UnitCanAttack = UnitCanAttack
local UnitIsPlayer = UnitIsPlayer
local UnitAffectingCombat = UnitAffectingCombat
local UnitIsDead = UnitIsDead
local UnitIsEnemy = UnitIsEnemy
local UnitExists = UnitExists
local UnitName = UnitName
local UnitClassification = UnitClassification
local GetRaidTargetIndex = GetRaidTargetIndex
local CheckInteractDistance = CheckInteractDistance
local getn = table.getn
local strfind = string.find
local strlower = string.lower
local GetTime = GetTime
local filter = {
}
filter.attackable = function(unit)
if CursiveTestOverlay_UnitCanAttack and CursiveTestOverlay_UnitCanAttack(unit) then return true end
return UnitCanAttack("player", unit) and true or false
end
filter.player = function(unit)
return UnitIsPlayer(unit) and true or false
end
filter.notplayer = function(unit)
return not UnitIsPlayer(unit) and true or false
end
filter.infight = function(unit)
if CursiveTestOverlay_IsTestGuid and CursiveTestOverlay_IsTestGuid(unit) then return true end
-- v4.1.4: If the player is not in combat, the "In Combat" filter is a no-op
-- (all attackable mobs pass through). This matches the pre-regression behavior
-- where the list showed everything in range while scouting / setting up pulls,
-- and only started filtering once the player actually engaged.
-- The ShouldDisplayGuid comment at line ~281 already described this intent
-- ("Use both the unit's AND the player's combat state") but the code only
-- checked the unit's state, which made the list go empty out of combat.
if not UnitAffectingCombat("player") then return true end
return UnitAffectingCombat(unit) and true or false
end
filter.hascurse = function(unit)
return Cursive.curses:HasAnyCurse(unit) and true or false
end
filter.alive = function(unit)
if CursiveTestOverlay_IsTestGuid and CursiveTestOverlay_IsTestGuid(unit) then return true end
return not UnitIsDead(unit) and true or false
end
-- v4.0.4: Class-appropriate range spells per talent tree
-- IsSpellInRange auto-applies talent range bonuses (Grim Reach, Destructive Reach, etc.)
-- Multiple spells per class cover different talent trees — in range if ANY spell reaches
local _, playerClass = UnitClass("player")
local classRangeSpells = {
WARLOCK = { "Corruption", "Immolate" }, -- Grim Reach (Affli) / Destructive Reach (Destro)
MAGE = { "Frostbolt", "Fireball", "Arcane Missiles" }, -- Arctic Reach / Flame Throwing / Magic Attunement
PRIEST = { "Shadow Word: Pain", "Smite" }, -- Shadow Reach / Holy Reach
DRUID = { "Moonfire" }, -- Nature's Reach (all Balance)
HUNTER = { "Auto Shot" }, -- Hawk Eye (all ranged)
SHAMAN = { "Lightning Bolt" }, -- No range talents
}
local rangeSpells = classRangeSpells[playerClass]
-- v4.1.3: Max spell-range per class (base + best talent) for OOR-stripes. This is
-- a DIFFERENT semantic from filter.range (which is 120y for "Within Range" filter).
-- IsSpellInRange is unreliable for non-target GUIDs (returns 1 even at 600+ yards
-- when called on a GUID that's not the current target), so we use UnitXP distanceBetween.
local classMaxSpellRange = {
WARLOCK = 42, -- Corruption 30y + Grim Reach 40%
MAGE = 41, -- Frostbolt 30y + Arctic Reach 33% / Magic Attunement
PRIEST = 40, -- Shadow Word: Pain 30y + Shadow Reach 33%
DRUID = 35, -- Moonfire 25y + Nature's Reach 40%
HUNTER = 41, -- Auto Shot 35y + Hawk Eye 17%
SHAMAN = 30, -- Lightning Bolt 30y
PALADIN = 30, -- Judgement 30y
WARRIOR = 10, -- Melee — unlikely to use Cursive for tracking
ROGUE = 10, -- Melee
}
local MAX_SPELL_RANGE = classMaxSpellRange[playerClass] or 40
-- v4.0.6: Range check cache — avoids redundant calls across
-- ShouldDisplayGuid (10Hz x all GUIDs) AND BarUpdate (20Hz x displayed bars)
local rangeCache = {} -- guid -> { result = bool, expires = time } — 120y filter
local spellRangeCache = {} -- v4.1.3: separate cache for spell-range stripes (class max)
local RANGE_CACHE_TTL = 0.25 -- v4.0.6: 250ms TTL — balanced between perf and responsiveness
filter.range = function(unit)
-- v4.1.1: TestOverlay range override. Gate on string prefix directly instead of
-- CursiveTestOverlay_IsTestGuid — that returns false when TestOverlay is disabled,
-- which allowed stale test-GUIDs (seen briefly during Enable/Disable transitions or
-- when cached by call-sites) to fall through to CheckInteractDistance(), throwing
-- "Unknown unit name: CURSIVE_TEST_*" errors. Prefix check is state-independent.
if type(unit) == "string" and strfind(unit, "CURSIVE_TEST_", 1, true) then
if CursiveTestOverlay_IsOutOfRange and CursiveTestOverlay_IsOutOfRange(unit) then
return false
end
return true
end
-- v4.0.6: Return cached result if fresh
local now = GetTime()
local cached = rangeCache[unit]
if cached and cached.expires > now then
return cached.result
end
-- v4.1.2: Primary path — UnitXP("distanceBetween") gives precise yards.
-- "Within Range" now means a fixed 120-yard radius, not class-spell-range
-- (~30-42y). When this filter is disabled (ShouldDisplayGuid skips the call),
-- all UnitExists-trackable targets are shown (server-range ~300y).
local result = false
if UnitXP then
local ok, dist = pcall(UnitXP, "distanceBetween", "player", unit)
if ok and type(dist) == "number" then
result = dist <= 120
end
end
-- Fallback path (UnitXP unavailable or returned nil): IsSpellInRange + CheckInteractDistance
if not result and IsSpellInRange and rangeSpells then
for i = 1, getn(rangeSpells) do
-- pcall: IsSpellInRange errors if spell is not in spellbook
local ok, inRange = pcall(IsSpellInRange, rangeSpells[i], unit)
if ok and inRange == 1 then
result = true
break
end
end
if not result then
result = CheckInteractDistance(unit, 4) and true or false
end
end
-- Store in cache
rangeCache[unit] = rangeCache[unit] or {}
rangeCache[unit].result = result
rangeCache[unit].expires = now + RANGE_CACHE_TTL
return result
end
-- v4.1.3: Spell-range check for OOR-stripe rendering (not the "Within Range"
-- filter, which uses filter.range at 120y). Uses UnitXP distanceBetween against
-- a class-specific max spell range. Rationale: IsSpellInRange is unreliable for
-- non-target GUIDs (verified 2026-04-17: returns 1 at 686y for passive-acquired
-- GUIDs), so the pre-v4.1.2 stripe logic effectively never showed stripes for
-- those units. distanceBetween gives the real yard value and lets us use the
-- player class's talent-accounted max range as the threshold.
filter.inSpellRange = function(unit)
-- TestOverlay override — same gate as filter.range
if type(unit) == "string" and strfind(unit, "CURSIVE_TEST_", 1, true) then
if CursiveTestOverlay_IsOutOfRange and CursiveTestOverlay_IsOutOfRange(unit) then
return false
end
return true
end
local now = GetTime()
local cached = spellRangeCache[unit]
if cached and cached.expires > now then
return cached.result
end
local result = false
if UnitXP then
local ok, dist = pcall(UnitXP, "distanceBetween", "player", unit)
if ok and type(dist) == "number" then
result = dist <= MAX_SPELL_RANGE
end
end
spellRangeCache[unit] = spellRangeCache[unit] or {}
spellRangeCache[unit].result = result
spellRangeCache[unit].expires = now + RANGE_CACHE_TTL
return result
end
-- v4.0.6: Periodic cache cleanup (called from main UI loop)
-- v4.1.3: Also clean spellRangeCache
filter.cleanRangeCache = function()
local now = GetTime()
for guid, entry in pairs(rangeCache) do
if entry.expires < now then
rangeCache[guid] = nil
end
end
for guid, entry in pairs(spellRangeCache) do
if entry.expires < now then
spellRangeCache[guid] = nil
end
end
end
filter.icon = function(unit)
if CursiveTestOverlay_GetRaidTargetIndex and CursiveTestOverlay_GetRaidTargetIndex(unit) then return true end
return GetRaidTargetIndex(unit) and true or false
end
filter.normal = function(unit)
local elite = UnitClassification(unit)
return elite == "normal" and true or false
end
filter.elite = function(unit)
local elite = UnitClassification(unit)
return (elite == "elite" or elite == "rareelite") and true or false
end
filter.hostile = function(unit)
return UnitIsEnemy("player", unit) and true or false
end
filter.notignored = function(unit)
if not Cursive.db.profile.ignorelist or getn(Cursive.db.profile.ignorelist) == 0 then
return true
end
local unitName = UnitName(unit)
if not unitName then
return true
end
local lowerName = strlower(unitName)
for _, str in ipairs(Cursive.db.profile.ignorelist) do
if strfind(lowerName, strlower(str), nil, not Cursive.db.profile.ignorelistuseregex) then
return false
end
end
return true
end
Cursive.filter = filter
function Cursive:ShouldDisplayGuid(guid)
-- v3.2.1: Test Overlay GUIDs always display
if CursiveTestOverlay_IsTestGuid and CursiveTestOverlay_IsTestGuid(guid) then
return true
end
-- never display units that don't exist
if not UnitExists(guid) then
return false
end
-- never display dead units
if not Cursive.filter.alive(guid) then
return false
end
-- v3.2.2: Never display mind-controlled players
-- MC'd players are UnitIsPlayer=true but UnitCanAttack=true (hostile)
-- Must be checked before priority shortcuts (target/raidmark) that bypass filters
if UnitIsPlayer(guid) and UnitCanAttack("player", guid) then
return false
end
local _, targetGuid = UnitExists("target")
-- FILTER TARGET: only show current target, hide everything else
if Cursive.db.profile.filtertarget then
if targetGuid and targetGuid == guid then
return true
end
return false
end
-- always show target if attackable
if (targetGuid == guid) and filter.attackable(guid) then
return true
end
-- v4.1.1 FIX (Bug #3): Raid-mark bypass only applies when "Has Raid Mark"
-- filter is explicitly enabled. Pre-fix: stale-marked OOC bosses leaked past
-- combat/range filters regardless of user settings. Users who want the old
-- "always show marked mobs" behavior must enable the "Has Raid Mark" checkbox.
if Cursive.db.profile.filterraidmark and filter.icon(guid) and filter.attackable(guid) then
return true
end
-- v3.2.1 FIX: Combat filter — show all mobs in combat
-- Note: UnitAffectingCombat can briefly return false for freshly-pulled mobs
-- Use both the unit's AND the player's combat state as fallback
if Cursive.db.profile.filterincombat then
if not filter.infight(guid) then
return false
end
end
if Cursive.db.profile.filterhascurse and not filter.hascurse(guid) then
return false
end
if Cursive.db.profile.filterhostile and not filter.hostile(guid) then
return false
end
if Cursive.db.profile.filterattackable and not filter.attackable(guid) then
return false
end
if Cursive.db.profile.filterrange and not filter.range(guid) then
return false
end
if Cursive.db.profile.filterraidmark and not filter.icon(guid) then
return false
end
if Cursive.db.profile.filterplayer and not filter.player(guid) then
return false
end
if Cursive.db.profile.filternotplayer and not filter.notplayer(guid) then
return false
end
if Cursive.db.profile.filterignored and not filter.notignored(guid) then
return false
end
return true
end