Files
SuperCleveRoidMacros/Utility.lua
T
2025-10-05 14:24:46 -04:00

561 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[
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.." <TABLE>", 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 ==============================================