--[[ Author: Dennis Werner Garske (DWG) / brian / Mewtiny License: MIT License ]] 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(...) -- Capture arguments into a local table to avoid arg scope issues local args = arg or {} local n = (type(args) == "table" and args.n) or 0 local r = { orig(unpack(args, 1, n)) } post(unpack(args, 1, n)) return unpack(r, 1, table.getn(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(...) -- Capture arguments into a local table to avoid arg scope issues local args = arg or {} local n = (type(args) == "table" and args.n) or 0 local r = { orig(unpack(args, 1, n)) } post(unpack(args, 1, n)) return unpack(r, 1, table.getn(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 end -- Trims any leading or trailing white space characters from the given string -- str: The string to trim -- returns: The trimmed string function CleveRoids.Trim(str) if not str then return nil end return string.gsub(str,"^%s*(.-)%s*$", "%1") end do local _G = _G or getfenv(0) local CleveRoids = _G.CleveRoids or {} _G.CleveRoids = CleveRoids CleveRoids.__mo = CleveRoids.__mo or { sources = {}, current = nil } local PRIORITY = { pfui = 3, -- unitframe hovers should win blizz = 3, -- same level as pfui tooltip = 1, -- lowest; don’t stomp frames } local function getBest() local bestSource, bestUnit, bestP = nil, nil, -1 for source, unit in CleveRoids.__mo.sources do if unit and unit ~= "" then local p = PRIORITY[source] or 0 if p > bestP then bestP, bestSource, bestUnit = p, source, unit end end end return bestSource, bestUnit end local function apply(unit) -- SuperWoW path if available; otherwise internal fallback if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then _G.SetMouseoverUnit(unit) -- nil clears else CleveRoids.mouseoverUnit = unit end if CleveRoids.QueueActionUpdate then CleveRoids.QueueActionUpdate() end end function CleveRoids.SetMouseoverFrom(source, unit) -- set/refresh a source’s unit if not source then return end CleveRoids.__mo.sources[source] = unit local _, bestUnit = getBest() if bestUnit ~= CleveRoids.__mo.current then CleveRoids.__mo.current = bestUnit apply(bestUnit) end end function CleveRoids.ClearMouseoverFrom(source, unitIfMatch) if not source then return end if unitIfMatch and CleveRoids.__mo.sources[source] ~= unitIfMatch then return -- don’t clear if it was replaced meanwhile end CleveRoids.__mo.sources[source] = nil local _, bestUnit = getBest() if bestUnit ~= CleveRoids.__mo.current then CleveRoids.__mo.current = bestUnit apply(bestUnit) end end end -- TODO: Get rid of one Split function. CleveRoids.splitString is ~10% slower function CleveRoids.Split(s, p, trim) local r, o = {}, 1 if not p or p == "" then if trim then s = CleveRoids.Trim(s) end for i = 1, string.len(s) do table.insert(r, string.sub(s, i, 1)) end return r end repeat local b, e = string.find(s, p, o) if b == nil then local sub = string.sub(s, o) table.insert(r, trim and CleveRoids.Trim(sub) or sub) return r end if b > 1 then local sub = string.sub(s, o, b - 1) table.insert(r, trim and CleveRoids.Trim(sub) or sub) else table.insert(r, "") end o = e + 1 until false end -- Splits the given string into a list of sub-strings -- str: The string to split -- seperatorPattern: The seperator between sub-string. May contain patterns -- returns: A list of sub-strings function CleveRoids.splitString(str, seperatorPattern) local tbl = {} if not str then return tbl end local pattern = "(.-)" .. seperatorPattern local lastEnd = 1 local s, e, cap = string.find(str, pattern, 1) while s do if s ~= 1 or cap ~= "" then table.insert(tbl,cap) end lastEnd = e + 1 s, e, cap = string.find(str, pattern, lastEnd) end if lastEnd <= string.len(str) then cap = string.sub(str, lastEnd) table.insert(tbl, cap) end return tbl end function CleveRoids.splitStringIgnoringQuotes(str, separator) local result = {} local temp = "" local insideQuotes = false local separators = {} if type(separator) == "table" then for _, s in separator do separators[s] = s end else separators[separator or ";"] = separator or ";" end for i = 1, string.len(str) do local char = string.sub(str, i, i) if char == "\"" then insideQuotes = not insideQuotes temp = temp .. char elseif char == separators[char] and not insideQuotes then temp = CleveRoids.Trim(temp) if temp ~= "" then table.insert(result, temp) end temp = "" else temp = temp .. char end end -- Add the last segment if it exists if temp ~= "" then temp = CleveRoids.Trim(temp) table.insert(result, temp) end -- if nothing was found, return the empty string return (next(result) and result or {""}) end -- Prints all the given arguments into WoW's default chat frame function CleveRoids.Print(...) local c = "|cFF4477FFCleveR|r|cFFFFFFFFoid :: |r" local out = "" for i=1, arg.n, 1 do out = out..tostring(arg[i]).." " end if WowLuaFrameOutput then WowLuaFrameOutput:AddMessage(out) else if not DEFAULT_CHAT_FRAME:IsVisible() then FCF_SelectDockFrame(DEFAULT_CHAT_FRAME) end DEFAULT_CHAT_FRAME:AddMessage(c..out) end end function CleveRoids.PrintI(msg, depth) depth = depth or 0 msg = string.rep(" ", depth) .. tostring(msg) CleveRoids.Print(msg) end function CleveRoids.PrintT(t, depth) depth = depth or 0 local cs = "|cffc8c864" local ce = "|r" if t == nil or type(t) ~= "table" then CleveRoids.PrintI(t, depth) else for k, v in pairs(t) do if type(v) == "table" then CleveRoids.PrintI(cs..tostring(k)..":"..ce.." ", depth) CleveRoids.PrintT(v, depth + 1) else CleveRoids.PrintI(cs..tostring(k)..ce..": "..tostring(v), depth) end end end end CleveRoids.kmods = { ctrl = IsControlKeyDown, alt = IsAltKeyDown, shift = IsShiftKeyDown, mod = function() return (IsControlKeyDown() or IsAltKeyDown() or IsShiftKeyDown()) end, nomod = function() return (not IsControlKeyDown() and not IsAltKeyDown() and not IsShiftKeyDown()) end, } CleveRoids.operators = { ["<"] = "lt", ["lt"] = "<", [">"] = "gt", ["gt"] = ">", ["="] = "eq", ["eq"] = "=", ["<="] = "lte", ["lte"] = "<=", [">="] = "gte", ["gte"] = ">=", ["~="] = "ne", ["ne"] = "~=", } CleveRoids.comparators = { lt = function(a, b) return (a < b) end, gt = function(a, b) return (a > b) end, eq = function(a, b) return (a == b) end, lte = function(a, b) return (a <= b) end, gte = function(a, b) return (a >= b) end, ne = function(a, b) return (a ~= b) end, } CleveRoids.comparators["<"] = CleveRoids.comparators.lt CleveRoids.comparators[">"] = CleveRoids.comparators.gt CleveRoids.comparators["="] = CleveRoids.comparators.eq CleveRoids.comparators["<="] = CleveRoids.comparators.lte CleveRoids.comparators[">="] = CleveRoids.comparators.gte 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 local function GenerateUniqueTooltipName() local base = "CleveRoidsLibDebuffTT" local suffix = string.format("%d%d", GetTime() * 1000, math.random(1000, 9999)) return base .. suffix 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", nil, UIParent, "GameTooltipTemplate") lib._tt:SetOwner(UIParent, "ANCHOR_NONE") lib._L1 = lib._tt:CreateFontString(nil, nil, "GameTooltipText") lib._R1 = lib._tt:CreateFontString(nil, nil, "GameTooltipText") lib._tt:AddFontStrings(lib._L1, lib._R1) end local tt = lib._tt tt:ClearLines() tt:SetUnitDebuff(unit, id) effect = lib._L1 and lib._L1: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 ==============================================