diff --git a/Conditionals.lua b/Conditionals.lua index 5d667e1..5b08b5c 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -727,33 +727,43 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- Case B: Numeric/stack condition exists. if hasNumCheck then - -- If aura was found, compare stacks or time-left. - if found then - if args.checkStacks then - -- Compare stacks (treat nil as 0) + -- Stacks compare path + if args.checkStacks then + if found then return cmp[args.operator](stacks or 0, args.amount) else - if unit == "player" then - -- Player auras already have 'remaining' - return cmp[args.operator](remaining or 0, args.amount) - else - -- NEW: Non-player units → try pfUI libdebuff time-left. - local tl = _pfui_timeleft(unit, args.name) - if tl ~= nil then - return cmp[args.operator](tl or 0, args.amount) + return cmp[args.operator](0, args.amount) + end + end + + -- Time-left compare path + if unit == "player" then + -- Player auras already have 'remaining' + local tl = found and (remaining or 0) or 0 + return cmp[args.operator](tl, args.amount) + else + -- Non-player: try pfUI → internal libdebuff → 0s + local tl = _pfui_timeleft(unit, args.name) + if tl ~= nil then + return cmp[args.operator](tl or 0, args.amount) + end + + if CleveRoids.libdebuff and CleveRoids.libdebuff.UnitDebuff then + local atl = nil + for idx = 1, 16 do + local effect, _, _, _, _, duration, timeleft = CleveRoids.libdebuff:UnitDebuff(unit, idx) + if effect == args.name then + atl = (timeleft and timeleft >= 0) and timeleft or 0 + break end - -- No pfUI time: fall back to old behavior (treat as existence only) - return true + end + if atl ~= nil then + return cmp[args.operator](atl, args.amount) end end - else - -- Aura NOT found: stacks=0, time-left=0 by definition. - if args.checkStacks then - return cmp[args.operator](0, args.amount) - else - -- Time compare with missing aura → 0s vs amount - return cmp[args.operator](0, args.amount) - end + + -- No timers at all: treat missing/unknown as 0s and compare + return cmp[args.operator](0, args.amount) end end @@ -800,24 +810,67 @@ function CleveRoids.GetSpellCooldown(spellName, ignoreGCD) end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed -function CleveRoids.GetItemCooldown(itemName, ignoreGCD) - if not itemName then return 0 end +-- Hardened item cooldown resolver (Vanilla 1.12.1 / Lua 5.0) +-- Returns: remainingSeconds, totalDuration, enabled +function CleveRoids.GetItemCooldown(item) + local start, duration, enable = nil, nil, nil - local item = CleveRoids.GetItem(itemName) - if not item then return 0 end + local function _norm(s, d, e) + s = tonumber(s) or 0 + d = tonumber(d) or 0 + e = tonumber(e) or 0 + if d <= 0 or s <= 0 then + return 0, 0, e + end + local rem = (s + d) - GetTime() + if rem < 0 then rem = 0 end + return rem, d, e + end - local start, cd, expires - if item.inventoryID then - start, cd = GetInventoryItemCooldown("player", item.inventoryID) - elseif item.bagID then - start, cd = GetContainerItemCooldown(item.bagID, item.slot) + -- Case A: numeric itemID passed directly to the WoW API + if type(item) == "number" then + start, duration, enable = GetItemCooldown(item) + return _norm(start, duration, enable) + end + + -- Case B: slot indices "13"/"14" etc. (as string or number) + -- Some macro engines pass "13"|"14" to mean trinket slots. + local slot = tonumber(item) + if slot then + start, duration, enable = GetInventoryItemCooldown("player", slot) + return _norm(start, duration, enable) + end + + -- Case C: item name -> try equipped slots first + if type(item) == "string" and item ~= "" then + -- scan a few common equipment slots; expand if your engine needs more + local slots = { 13, 14, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 } -- trinkets first + for i = 1, table.getn(slots) do + local s = slots[i] + local link = GetInventoryItemLink("player", s) + if link and string.find(link, item, 1, true) then + start, duration, enable = GetInventoryItemCooldown("player", s) + return _norm(start, duration, enable) + end end - if ignoreGCD and cd and cd > 0 and cd == 1.5 then - return 0 - else - return (start + cd) + -- Case D: search bags for the named item + for bag = 0, 4 do + local size = GetContainerNumSlots(bag) + if size and size > 0 then + for slotIndex = 1, size do + local link = GetContainerItemLink(bag, slotIndex) + if link and string.find(link, item, 1, true) then + start, duration, enable = GetContainerItemCooldown(bag, slotIndex) + return _norm(start, duration, enable) + end + end + end end + end + + -- Fallback: unknown item → no cooldown + return 0, 0, 0 end function CleveRoids.IsReactive(name) diff --git a/Utility.lua b/Utility.lua index f413ba5..051dbe6 100644 --- a/Utility.lua +++ b/Utility.lua @@ -5,6 +5,67 @@ local _G = _G or getfenv(0) local CleveRoids = _G.CleveRoids or {} -- redundant since we're loading first but peace of mind if another file is added top of chain +-- Vanilla 1.12.1 compatibility: hooksecurefunc polyfill +if type(hooksecurefunc) ~= "function" then + function hooksecurefunc(arg1, arg2, arg3) + local tgt, fname, post + + -- Variant A: hooksecurefunc("GlobalFuncName", postHook) + if type(arg1) == "string" and type(arg2) == "function" and arg3 == nil then + fname, post = arg1, arg2 + local orig = _G[fname] + if type(orig) ~= "function" then return end + _G[fname] = function(...) + local r = { orig(unpack(arg)) } -- Lua 5.0 varargs + post(unpack(arg)) + return unpack(r) + end + return + end + + -- Variant B: hooksecurefunc(table, "MethodName", postHook) + if type(arg1) == "table" and type(arg2) == "string" and type(arg3) == "function" then + tgt, fname, post = arg1, arg2, arg3 + local orig = tgt[fname] + if type(orig) ~= "function" then return end + tgt[fname] = function(...) + local r = { orig(unpack(arg)) } + post(unpack(arg)) + return unpack(r) + end + return + end + -- If neither signature matches, do nothing (fail-safe) + end +end + +-- Lua 5.0 compatibility shims (WoW 1.12): string.match / string.gmatch +if type(string.match) ~= "function" then + function string.match(s, pattern, init) + if s == nil or pattern == nil then return nil end + local i, j, c1, c2, c3, c4, c5 = string.find(s, pattern, init) + if not i then return nil end + -- If the pattern has captures, return them (first 5 for simplicity). + if c1 ~= nil then return c1, c2, c3, c4, c5 end + -- Otherwise return the matched substring (Lua 5.1+ behavior). + return string.sub(s, i, j) + end +end + +if type(string.gmatch) ~= "function" then + -- Simple gmatch built on repeated string.find + function string.gmatch(s, pattern) + local pos = 1 + return function() + if s == nil or pattern == nil then return nil end + local i, j, c1, c2, c3, c4, c5 = string.find(s, pattern, pos) + if not i then return nil end + pos = j + 1 + if c1 ~= nil then return c1, c2, c3, c4, c5 end + return string.sub(s, i, j) + end + end +end function CleveRoids.Seq(_, i) return (i or 0) + 1 @@ -263,3 +324,222 @@ CleveRoids.comparators["~="] = CleveRoids.comparators.ne _G["CleveRoids"] = CleveRoids + +-- === CleveRoids.libdebuff (Vanilla 1.12.1 / Lua 5.0) ======================= +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids or {} +_G.CleveRoids = CleveRoids + +-- create table +CleveRoids.libdebuff = CleveRoids.libdebuff or {} +local lib = CleveRoids.libdebuff + +-- storage modeled after pfUI: objects[name][level][effect] = {start,duration,caster} +lib.objects = lib.objects or {} +lib.pending = lib.pending or {} -- {unit, level, effect, duration, caster} +local lastspell + +-- configurable durations (extend as needed) +lib.durations = lib.durations or { + ["Sunder Armor"] = 30, + ["Expose Armor"] = 30, + -- ["Faerie Fire"] = 40, + -- ["Rend"] = 21, +} + +-- Utility: effect duration lookup (can be expanded with rank/talent logic later) +function lib:GetDuration(effect, rank) + return self.durations[effect] or 0 +end + +-- Basic add/refresh of a debuff record +function lib:AddEffect(unitName, unitLevel, effect, duration, caster) + if not unitName or not effect then return end + unitLevel = unitLevel or 0 + duration = duration or lib:GetDuration(effect) + if duration <= 0 then return end + + lib.objects[unitName] = lib.objects[unitName] or {} + lib.objects[unitName][unitLevel] = lib.objects[unitName][unitLevel] or {} + lib.objects[unitName][unitLevel][effect] = lib.objects[unitName][unitLevel][effect] or {} + + local rec = lib.objects[unitName][unitLevel][effect] + lastspell = rec + + rec.effect = effect + rec.start_old = rec.start + rec.start = GetTime() + rec.duration = duration + rec.caster = caster + + -- no UI refresh hook needed; your UI reads via conditionals +end + +function lib:RevertLastAction() + if lastspell and lastspell.start_old then + lastspell.start = lastspell.start_old + lastspell.start_old = nil + end +end + +function lib:AddPending(unitName, unitLevel, effect, duration, caster) + if not unitName or not effect then return end + if (duration or 0) <= 0 then duration = lib:GetDuration(effect) end + if duration <= 0 then return end + + lib.pending[1] = unitName + lib.pending[2] = unitLevel or 0 + lib.pending[3] = effect + lib.pending[4] = duration + lib.pending[5] = caster +end + +function lib:RemovePending() + lib.pending[1] = nil + lib.pending[2] = nil + lib.pending[3] = nil + lib.pending[4] = nil + lib.pending[5] = nil +end + +function lib:PersistPending(effect) + -- if effect matches the pending one (or nil meaning “persist whatever is pending”) + if lib.pending[3] and (effect == nil or effect == lib.pending[3]) then + lib:AddEffect(lib.pending[1], lib.pending[2], lib.pending[3], lib.pending[4], lib.pending[5]) + end + lib:RemovePending() +end + +-- Read API similar to pfUI: returns effect, rank(nil), texture, stacks, dtype, duration, timeleft, caster +function lib:UnitDebuff(unit, id) + local unitName = UnitName(unit) + local unitLevel = UnitLevel(unit) or 0 + local texture, stacks, dtype = UnitDebuff(unit, id) + local duration, timeleft, caster = nil, -1, nil + local effect + + if texture then + -- Try to resolve a stable effect name using SuperWoW SpellInfo when available + local _, _, spellID + if CleveRoids.hasSuperwow then + -- Use tooltip scan alternative via UnitDebuff’s texture->name mapping if you have it, + -- or rely on GameTooltip scanning if desired (omitted for performance). + -- The most stable way here: read the name off the debuff button tooltip: + -- (Left out to keep it lightweight; we fetch name by scanning by index below) + end + + -- Prefer reading name by setting the tooltip on this debuff index (cheap enough on access) + -- We avoid a full scanner dependency to keep this self-contained: + -- Build a temporary tooltip to fetch line 1 as the effect name. + if not lib._tt then + lib._tt = CreateFrame("GameTooltip", "CleveRoidsLibDebuffTT", UIParent, "GameTooltipTemplate") + lib._tt:SetOwner(UIParent, "ANCHOR_NONE") + end + local tt = lib._tt + tt:ClearLines() + tt:SetUnitDebuff(unit, id) + effect = (getglobal("CleveRoidsLibDebuffTTTextLeft1") and getglobal("CleveRoidsLibDebuffTTTextLeft1"):GetText()) or "" + end + + -- read level-scoped storage, try exact level then 0 fallback (pfUI pattern) + local data = lib.objects[unitName] and (lib.objects[unitName][unitLevel] or lib.objects[unitName][0]) + local rec = data and effect and data[effect] + + if rec and rec.duration and rec.start and (rec.duration + rec.start > GetTime()) then + duration = rec.duration + timeleft = rec.duration + rec.start - GetTime() + caster = rec.caster + elseif rec then + -- cleanup expired + data[effect] = nil + end + + return effect, nil, texture, stacks, dtype, duration, timeleft, caster +end + +-- Seed timers by scanning the current target/player auras (to avoid relying only on casts) +local function SeedUnit(unit) + if not UnitExists(unit) then return end + local unitName = UnitName(unit) + if not unitName or unitName == "" then return end + local unitLevel = UnitLevel(unit) or 0 + + for i=1, 16 do + local tex = (UnitDebuff(unit, i)) + if not tex then break end + + -- resolve effect name via tooltip line 1 + if not lib._tt then + lib._tt = CreateFrame("GameTooltip", "CleveRoidsLibDebuffTT", UIParent, "GameTooltipTemplate") + lib._tt:SetOwner(UIParent, "ANCHOR_NONE") + end + local tt = lib._tt + tt:ClearLines() + tt:SetUnitDebuff(unit, i) + local effect = (getglobal("CleveRoidsLibDebuffTTTextLeft1") and getglobal("CleveRoidsLibDebuffTTTextLeft1"):GetText()) or "" + + if effect ~= "" and lib.durations[effect] then + -- don’t overwrite a valid running timer + local data = lib.objects[unitName] and (lib.objects[unitName][unitLevel] or lib.objects[unitName][0]) + if not (data and data[effect]) then + lib:AddEffect(unitName, unitLevel, effect, lib:GetDuration(effect)) + end + end + end +end + +-- Events & hooks (modeled after pfUI, simplified to avoid extra libs) +local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent) +ev:RegisterEvent("PLAYER_TARGET_CHANGED") +ev:RegisterEvent("UNIT_AURA") +ev:RegisterEvent("SPELLCAST_STOP") +ev:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER") +ev:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") + +-- (Optional) add more combat log events if you later want to detect “aura applied” via text + +ev:SetScript("OnEvent", function() + if event == "PLAYER_TARGET_CHANGED" then + SeedUnit("target") + + elseif event == "UNIT_AURA" and arg1 == "target" then + SeedUnit("target") + + elseif event == "SPELLCAST_STOP" then + lib:PersistPending() -- accept any pending on cast stop + + elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + -- best-effort pending cleanup/revert patterns can be added here if you parse failure strings + -- for now, we just drop the pending on generic failure patterns being detected elsewhere + end +end) + +-- Hooks to capture “pending” when you cast +hooksecurefunc("CastSpell", function(id, bookType) + -- try to resolve spell name via SuperWoW or vanilla + local name, rank + if type(SpellInfo) == "function" then + name, rank = SpellInfo(id) + elseif type(GetSpellName) == "function" then + name, rank = GetSpellName(id, bookType) + end + if name then + local dur = lib:GetDuration(name) + lib:AddPending(UnitName("target"), UnitLevel("target"), name, dur, "player") + end +end) + +hooksecurefunc("CastSpellByName", function(spell) + local name = spell + -- accept “Name(Rank X)” format; trim rank for table match + local base = string.match(spell, "^(.-)%s*%(") or spell + local dur = lib:GetDuration(base) + lib:AddPending(UnitName("target"), UnitLevel("target"), base, dur, "player") +end) + +-- Simple action use hook (optional; safe no-op if Action API differs) +hooksecurefunc("UseAction", function(slot) + if GetActionText(slot) or not IsCurrentAction(slot) then return end + -- we can’t reliably get the effect name without a tooltip scanner; skip to keep it lightweight +end) +-- === end CleveRoids.libdebuff ==============================================