Files
pfUI/api/api.lua
T
Meow fa7e4f8b40 Range Check: UnitXP mode
Added Range Check Mode dropdown (Vanilla / UnitXP) and UnitXP Range Threshold input field under the 40y-Range Check settings in the GUI
In UnitXP mode, UnitInRange bypasses librange entirely and queries UnitXP("distanceBetween") directly with the configured yard threshold
librange scan loop is disabled when UnitXP mode is active — no TargetUnit cycling, no spellbook scanning, no combat interruption
Fixed UnitInRange referencing librange as an undefined local instead of pfUI.api.librange, which caused silent nil returns
2026-03-04 02:27:11 +01:00

1512 lines
50 KiB
Lua

pfUI.api = { }
-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
-- [ DLL Detection Helpers ]
-- Detects presence of various DLL extensions for enhanced functionality
-- [ HasSuperWoW ]
-- Returns true if SuperWoW DLL is active
-- SuperWoW provides: UNIT_CASTEVENT, UnitPosition, SetMouseoverUnit, SpellInfo, etc.
function pfUI.api.HasSuperWoW()
return SUPERWOW_VERSION or (SetAutoloot and SpellInfo)
end
-- [ HasUnitXP ]
-- Returns true if UnitXP_SP3 DLL is active
-- UnitXP provides: distance, line of sight, behind detection, targeting helpers
function pfUI.api.HasUnitXP()
local success = pcall(UnitXP, "nop", "nop")
return success
end
-- [ HasNampower ]
-- Returns true if Nampower DLL is active
-- Nampower provides: spell queuing, GetCastInfo, GetSpellIdCooldown, IsSpellInRange, etc.
function pfUI.api.HasNampower()
return GetNampowerVersion and true or false
end
-- [ GetUnitDistance ]
-- Returns distance to unit using best available method
-- 'unit1' [string] first unit (default: "player")
-- 'unit2' [string] second unit
-- returns: [number] distance in yards, or nil if unavailable
function pfUI.api.GetUnitDistance(unit1, unit2)
if not unit2 then
unit2 = unit1
unit1 = "player"
end
if not UnitExists(unit2) then return nil end
-- Try UnitXP first (most accurate)
if pfUI.api.HasUnitXP() then
local success, distance = pcall(UnitXP, "distanceBetween", unit1, unit2)
if success and distance then return distance end
end
-- Try SuperWoW UnitPosition
if pfUI.api.HasSuperWoW() and UnitPosition then
local x1, y1, z1 = UnitPosition(unit1)
local x2, y2, z2 = UnitPosition(unit2)
if x1 and y1 and z1 and x2 and y2 and z2 then
return ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^0.5
end
end
return nil
end
-- [ UnitInLineOfSight ]
-- Returns true if unit1 has line of sight to unit2
-- Requires UnitXP_SP3
function pfUI.api.UnitInLineOfSight(unit1, unit2)
if not pfUI.api.HasUnitXP() then return nil end
local success, inSight = pcall(UnitXP, "inSight", unit1, unit2)
if success then return inSight end
return nil
end
-- [ UnitIsBehind ]
-- Returns true if unit1 is behind unit2
-- Requires UnitXP_SP3
function pfUI.api.UnitIsBehind(unit1, unit2)
if not pfUI.api.HasUnitXP() then return nil end
local success, behind = pcall(UnitXP, "behind", unit1, unit2)
if success then return behind end
return nil
end
-- Client API shortcuts
gfind = string.gmatch or string.gfind
mod = math.mod or mod
-- [ strsplit ]
-- Splits a string using a delimiter.
-- 'delimiter' [string] characters that will be interpreted as delimiter
-- characters (bytes) in the string.
-- 'subject' [string] String to split.
-- return: [list] a list of strings.
function pfUI.api.strsplit(delimiter, subject)
if not subject then return nil end
local delimiter, fields = delimiter or ":", {}
local pattern = string.format("([^%s]+)", delimiter)
string.gsub(subject, pattern, function(c) fields[table.getn(fields)+1] = c end)
return unpack(fields)
end
-- [ isempty ]
-- Returns true if a table is empty or not existing, otherwise false.
-- 'tbl' [table] the table that shall be checked
-- return: [boolean] result of the check.
function pfUI.api.isempty(tbl)
if not tbl then return true end
for k, v in pairs(tbl) do
return false
end
return true
end
-- [ checkversion ]
-- Compares a given version (major,minor,fix) and compares it to the current
-- 'chkmajor' [number] the major number to check
-- 'chkminor' [number] the minor number to check
-- 'chkfix' [number] the fix number to check
--
-- return: [boolean] true when the current version is smaller or equal
-- to the given value, otherwise returns nil.
local major, minor, fix = nil, nil, nil
function pfUI.api.checkversion(chkmajor, chkminor, chkfix)
if not major and not minor and not fix then
-- load and convert current version
major, minor, fix = pfUI.api.strsplit(".", tostring(pfUI_config.version))
major, minor, fix = tonumber(major) or 0, tonumber(minor) or 0, tonumber(fix) or 0
end
local chkversion = chkmajor + chkminor/100 + chkfix/10000
local curversion = major + minor/100 + fix/10000
return curversion <= chkversion and true or nil
end
-- [ UnitInRange ]
-- Returns whether a party/raid member is nearby.
-- It takes care of the rangecheck module if existing.
-- unit [string] A unit to query (string, unitID)
-- return: [bool] "1" if in range otherwise "nil"
local RangeCache = {}
function pfUI.api.UnitInRange(unit)
if not UnitExists(unit) or not UnitIsVisible(unit) then
return nil
elseif CheckInteractDistance(unit, 4) then
return 1
end
-- UnitXP precise mode: skip librange entirely, use direct distance check
if C.unitframes.rangecheck_mode == "unitxp" and _G.UnitXP then
local threshold = tonumber(C.unitframes.rangecheck_distance) or 40
local success, distance = pcall(_G.UnitXP, "distanceBetween", "player", unit)
if success and distance then
return distance <= threshold and 1 or nil
end
-- fallthrough to librange if UnitXP fails
end
if pfUI.api.librange then
return pfUI.api.librange:UnitInSpellRange(unit)
end
return nil
end
-- [ RunOOC ]
-- Runs a function once, as soon as the combat lockdown fades.
-- func [function] The function that shall run ooc.
-- return: [bool] true if the function was added,
-- nil if the function already exists in queue
local queue, frame = {}
function pfUI.api.RunOOC(func)
if not frame then
frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if InCombatLockdown and InCombatLockdown() then return end
for key, func in pairs(queue) do func(); queue[key] = nil end
end)
end
if not queue[tostring(func)] then
queue[tostring(func)] = func
return true
end
end
-- [ UnitHasBuff ]
-- Returns whether a unit has the given buff or not.
-- unit [string] A unit to query (string, unitID)
-- buff [string] The texture of the buff.
-- return: [bool] true if unit has buff otherwise "nil"
function pfUI.api.UnitHasBuff(unit, buff)
local hasbuff = nil
for i=1,32 do
if UnitBuff(unit, i) == buff then
hasbuff = true
break
end
end
return hasbuff
end
-- [[ GetUnitColor ]]
-- Returns an escape string for the unit aswell as the RGB values
-- unit [string] the unitstring
-- return: [table] string, r, g, b
function pfUI.api.GetUnitColor(unitstr)
local _, class = UnitClass(unitstr)
local r, g, b = .8, .8, .8
if RAID_CLASS_COLORS[class] then
r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b
end
return pfUI.api.rgbhex(r,g,b), r, g, b
end
-- [ strvertical ]
-- Creates vertical text using linebreaks. Multibyte char friendly.
-- 'str' [string] String to columnize.
-- return: [string] the string tranformed to a column.
function pfUI.api.strvertical(str)
local _, len = string.gsub(str,"[^\128-\193]", "")
if (len == string.len(str)) then
return string.gsub(str, "(.)", "%1\n")
else
return string.gsub(str,"([%z\1-\127\194-\244][\128-\191]*)", "%1\n")
end
end
-- [ round ]
-- Rounds a float number into specified places after comma.
-- 'input' [float] the number that should be rounded.
-- 'places' [int] amount of places after the comma.
-- returns: [float] rounded number.
function pfUI.api.round(input, places)
if not places then places = 0 end
if type(input) == "number" and type(places) == "number" then
local pow = 1
for i = 1, places do pow = pow * 10 end
return floor(input * pow + 0.5) / pow
end
end
-- [ clamp ]
-- Clamps a number between given range.
-- 'x' [number] the number that should be clamped.
-- 'min' [number] minimum value.
-- 'max' [number] maximum value.
-- returns: [number] clamped value: 'x', 'min' or 'max' value itself.
function pfUI.api.clamp(x, min, max)
if type(x) == "number" and type(min) == "number" and type(max) == "number" then
return x < min and min or x > max and max or x
else
return x
end
end
-- [ modf ]
-- Returns integral and fractional part of a number.
-- 'f' [float] the number to breakdown.
-- returns: [int],[float] whole and fractional part.
function pfUI.api.modf(f)
if modf then return modf(f) end
if f > 0 then
return math.floor(f), mod(f,1)
end
return math.ceil(f), mod(f,1)
end
-- [ GetSlashCommands ]
-- Lists all registeres slash commands
-- 'text' [string] optional, a specific command to find
-- return: [list] a list of all matching slash commands
function pfUI.api.GetSlashCommands(text)
local cmds
for k, v in pairs(_G) do
if strfind(k, "^SLASH_") and (not text or v == text) then
cmds = cmds or {}
cmds[k] = v
end
end
return cmds
end
-- [ RegisterSlashCommand ]
-- Lists all registeres slash commands
-- 'name' [string] name of the command
-- 'cmds' [table] table containing all slash command strings
-- 'func' [function] the function that should be assigned
-- 'force' [boolean] force assign the command even if aleady provided
-- by another function/addon.
function pfUI.api.RegisterSlashCommand(name, cmds, func, force)
local counter = 1
for _, cmd in pairs(cmds) do
if force or not pfUI.api.GetSlashCommands(cmd) then
_G["SLASH_"..name..counter] = cmd
counter = counter + 1
end
end
_G.SlashCmdList[name] = func
end
-- [ GetCaptures ]
-- Returns the indexes of a given regex pattern
-- 'pat' [string] unformatted pattern
-- returns: [numbers] capture indexes
local capture_cache = {}
function pfUI.api.GetCaptures(pat)
local r = capture_cache
if not r[pat] then
for a, b, c, d, e in gfind(gsub(pat, "%((.+)%)", "%1"), gsub(pat, "%d%$", "%%(.-)$")) do
r[pat] = { a, b, c, d, e}
end
r[pat] = r[pat] or {}
end
return r[pat][1], r[pat][2], r[pat][3], r[pat][4], r[pat][5]
end
-- [ SanitizePattern ]
-- Sanitizes and convert patterns into gfind compatible ones.
-- 'pattern' [string] unformatted pattern
-- returns: [string] simplified gfind compatible pattern
local sanitize_cache = {}
function pfUI.api.SanitizePattern(pattern)
if not sanitize_cache[pattern] then
local ret = pattern
-- escape magic characters
ret = gsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
-- remove capture indexes
ret = gsub(ret, "%d%$","")
-- catch all characters
ret = gsub(ret, "(%%%a)","%(%1+%)")
-- convert all %s to .+
ret = gsub(ret, "%%s%+",".+")
-- set priority to numbers over strings
ret = gsub(ret, "%(.%+%)%(%%d%+%)","%(.-%)%(%%d%+%)")
-- cache it
sanitize_cache[pattern] = ret
end
return sanitize_cache[pattern]
end
-- [ cmatch ]
-- Same as string.match but aware of capture indexes (up to 5)
-- 'str' [string] input string that should be matched
-- 'pat' [string] unformatted pattern
-- returns: [strings] matched string in capture order
local a, b, c, d, e
local _, va, vb, vc, vd, ve
local ra, rb, rc, rd, re
function pfUI.api.cmatch(str, pat)
-- read capture indexes
a, b, c, d, e = GetCaptures(pat)
_, _, va, vb, vc, vd, ve = string.find(str, pfUI.api.SanitizePattern(pat))
-- put entries into the proper return values
ra = e == 1 and ve or d == 1 and vd or c == 1 and vc or b == 1 and vb or va
rb = e == 2 and ve or d == 2 and vd or c == 2 and vc or a == 2 and va or vb
rc = e == 3 and ve or d == 3 and vd or a == 3 and va or b == 3 and vb or vc
rd = e == 4 and ve or a == 4 and va or c == 4 and vc or b == 4 and vb or vd
re = a == 5 and va or d == 5 and vd or c == 5 and vc or b == 5 and vb or ve
return ra, rb, rc, rd, re
end
-- [ GetItemLinkByName ]
-- Returns an itemLink for the given itemname
-- 'name' [string] name of the item
-- returns: [string] entire itemLink for the given item
function pfUI.api.GetItemLinkByName(name)
for itemID = 1, 25818 do
local itemName, hyperLink, itemQuality = GetItemInfo(itemID)
if (itemName and itemName == name) then
local _, _, _, hex = GetItemQualityColor(tonumber(itemQuality))
return hex.. "|H"..hyperLink.."|h["..itemName.."]|h|r"
end
end
end
-- [ GetItemCount ]
-- Returns information about how many of a given item the player has.
-- 'itemName' [string] name of the item
-- returns: [int] the number of the given item
function pfUI.api.GetItemCount(itemName)
local count = 0
for bag = 4, 0, -1 do
for slot = 1, GetContainerNumSlots(bag) do
local _, itemCount = GetContainerItemInfo(bag, slot)
if itemCount then
local itemLink = GetContainerItemLink(bag,slot)
local _, _, itemParse = strfind(itemLink, "(%d+):")
local queryName = GetItemInfo(itemParse)
if queryName and queryName ~= "" then
if queryName == itemName then
count = count + itemCount
end
end
end
end
end
return count
end
-- [ FindItem ]
-- Returns the bag and slot position of an item based on the name.
-- 'item' [string] name of the item
-- returns: [int] bag
-- [int] slot
function pfUI.api.FindItem(item)
for bag = 4, 0, -1 do
for slot = 1, GetContainerNumSlots(bag) do
local itemLink = GetContainerItemLink(bag,slot)
if itemLink then
local _, _, parse = strfind(itemLink, "(%d+):")
local query = GetItemInfo(parse)
if query and query ~= "" and string.lower(query) == string.lower(item) then
return bag, slot
end
end
end
end
return nil
end
-- [ GetBagFamily ]
-- Returns information about the type of a bag such as Soul Bags or Quivers.
-- Available bagtypes are "BAG", "KEYRING", "SOULBAG", "QUIVER" and "SPECIAL"
-- 'bag' [int] the bag id
-- returns: [string] the type of the bag, e.g "QUIVER"
function pfUI.api.GetBagFamily(bag)
if bag == -2 then return "KEYRING" end
if bag == 0 then return "BAG" end -- backpack
if bag == -1 then return "BAG" end -- bank
local _, _, id = strfind(GetInventoryItemLink("player", ContainerIDToInventoryID(bag)) or "", "item:(%d+)")
if id then
local _, _, _, _, _, itemType, subType = GetItemInfo(id)
local bagsubtype = L["bagtypes"][subType]
if bagsubtype == "DEFAULT" then return "BAG" end
if bagsubtype == "SOULBAG" then return "SOULBAG" end
if bagsubtype == "QUIVER" then return "QUIVER" end
if bagsubtype == nil then return "SPECIAL" end
end
return nil
end
-- [ Abbreviate ]
-- Abbreviates a number from 1234 to 1.23k
-- 'number' [number] the number that should be abbreviated
-- 'returns: [string] the abbreviated value
function pfUI.api.Abbreviate(number)
local mode = pfUI_config.unitframes.abbrevnum
-- mode "0" = disabled (full numbers)
-- mode "1" = 2 decimals (4250 -> 4.25k) [legacy/default]
-- mode "2" = 1 decimal (4250 -> 4.2k) - always rounds DOWN
if mode == "1" or mode == "2" then
local sign = number < 0 and -1 or 1
number = math.abs(number)
if number > 1000000 then
if mode == "2" then
-- 1 decimal, round DOWN: 4.18m -> 4.1m
return (floor(number/100000) / 10 * sign) .. "m"
else
return pfUI.api.round(number/1000000*sign, 2) .. "m"
end
elseif number > 1000 then
if mode == "2" then
-- 1 decimal, round DOWN: 4180 -> 4.1k (not 4.2k)
return (floor(number/100) / 10 * sign) .. "k"
else
return pfUI.api.round(number/1000*sign, 2) .. "k"
end
end
end
return math.floor(number)
end
-- [ SendChatMessageWide ]
-- Sends a message to widest audience the player can broadcast to
-- 'msg' [string] the message to send
function pfUI.api.SendChatMessageWide(msg)
local channel = "SAY"
if UnitInRaid("player") then
if ( IsRaidLeader() or IsRaidOfficer() ) then
channel = "RAID_WARNING"
else
channel = "RAID"
end
elseif UnitExists("party1") then
channel = "PARTY"
end
SendChatMessage(msg,channel)
end
-- [ GroupInfoByName ]
-- Gets the unit id by name
-- 'name' [string] party or raid member
-- 'group' [string] "raid" or "party"
-- returns: [table] {name='name',unitId='unitId',Id=Id,lclass='lclass',class='class'}
do -- create a scope so we don't have to worry about upvalue collisions
local party, raid, unitinfo = {}, {}, {}
party[0] = "player" -- fake unit
for i=1, MAX_PARTY_MEMBERS do
party[i] = "party"..i
end
for i=1, MAX_RAID_MEMBERS do
raid[i] = "raid"..i
end
function pfUI.api.GroupInfoByName(name,group)
unitinfo = pfUI.api.wipe(unitinfo)
if group == "party" then
for i=0, MAX_PARTY_MEMBERS do
local unitName = UnitName(party[i])
if unitName == name then
local lclass,class = UnitClass(party[i])
if not (lclass and class) then
lclass,class = _G.UNKNOWN, "UNKNOWN"
end
unitinfo.name,unitinfo.unitId,unitinfo.Id,unitinfo.lclass,unitinfo.class =
unitName,party[i],i,lclass,class
return unitinfo
end
end
elseif group == "raid" then
for i=1, MAX_RAID_MEMBERS do
local unitName = UnitName(raid[i])
if unitName == name then
local lclass,class = UnitClass(raid[i])
if not (lclass and class) then
lclass,class = _G.UNKNOWN, "UNKNOWN"
end
unitinfo.name,unitinfo.unitId,unitinfo.Id,unitinfo.lclass,unitinfo.class =
unitName,raid[i],i,lclass,class
return unitinfo
end
end
end
-- fallback for GetMasterLootCandidate not updating immediately for leavers
unitinfo.lclass,unitinfo.class = _G.UNKNOWN, "UNKNOWN"
return unitinfo
end
end
-- [ HookScript ]
-- Securely post-hooks a script handler.
-- 'f' [frame] the frame which needs a hook
-- 'script' [string] the handler to hook
-- 'func' [function] the function that should be added
function HookScript(f, script, func)
local prev = f:GetScript(script)
f:SetScript(script, function(a1,a2,a3,a4,a5,a6,a7,a8,a9)
if prev then prev(a1,a2,a3,a4,a5,a6,a7,a8,a9) end
func(a1,a2,a3,a4,a5,a6,a7,a8,a9)
end)
end
-- [ HookAddonOrVariable ]
-- Sets a function to be called automatically once an addon gets loaded
-- 'addon' [string] addon or variable name
-- 'func' [function] function that should run
function pfUI.api.HookAddonOrVariable(addon, func)
local lurker = CreateFrame("Frame", nil)
lurker.func = func
lurker:RegisterEvent("ADDON_LOADED")
lurker:RegisterEvent("VARIABLES_LOADED")
lurker:RegisterEvent("PLAYER_ENTERING_WORLD")
lurker:SetScript("OnEvent",function()
-- only run when config is available
if event == "ADDON_LOADED" and not this.foundConfig then
return
elseif event == "VARIABLES_LOADED" then
this.foundConfig = true
end
if IsAddOnLoaded(addon) or _G[addon] then
this:func()
this:UnregisterAllEvents()
end
end)
end
-- [ QueueFunction ]
-- Add functions to a FIFO queue for execution after a short delay.
-- '...' [vararg] function, [arguments]
local timer
function pfUI.api.QueueFunction(a1,a2,a3,a4,a5,a6,a7,a8,a9)
if not timer then
timer = CreateFrame("Frame")
timer.queue = {}
timer.interval = TOOLTIP_UPDATE_TIME
timer.DeQueue = function()
local item = table.remove(timer.queue,1)
if item then
item[1](item[2],item[3],item[4],item[5],item[6],item[7],item[8],item[9])
end
if table.getn(timer.queue) == 0 then
timer:Hide() -- no need to run the OnUpdate when the queue is empty
end
end
timer:SetScript("OnUpdate",function()
this.sinceLast = (this.sinceLast or 0) + arg1
while (this.sinceLast > this.interval) do
this.DeQueue()
this.sinceLast = this.sinceLast - this.interval
end
end)
end
table.insert(timer.queue,{a1,a2,a3,a4,a5,a6,a7,a8,a9})
timer:Show() -- start the OnUpdate
end
-- [ Create Gold String ]
-- Transforms a amount of copper into a fully fledged gold string
-- 'money' [int] the amount of coppy (GetMoney())
-- return: [string] a colorized string which is split into
-- gold,silver and copper values.
function pfUI.api.CreateGoldString(money)
if type(money) ~= "number" then return "-" end
local gold = floor(money/ 100 / 100)
local silver = floor(mod((money/100),100))
local copper = floor(mod(money,100))
local string = ""
if gold > 0 then string = string .. "|cffffffff" .. gold .. "|cffffd700g" end
if silver > 0 or gold > 0 then string = string .. "|cffffffff " .. silver .. "|cffc7c7cfs" end
string = string .. "|cffffffff " .. copper .. "|cffeda55fc"
return string
end
-- [ Enable Movable ]
-- Set all necessary functions to make a already existing frame movable.
-- 'name' [frame/string] Name of the Frame that should be movable
-- 'addon' [string] Addon that must be loaded before being able to access the frame
-- 'blacklist' [table] A list of frames that should be deactivated for mouse usage
local function OnDragStart() this:StartMoving() end
local function OnDragStop() this:StopMovingOrSizing() end
function pfUI.api.EnableMovable(name, addon, blacklist)
if addon then
local scan = CreateFrame("Frame")
scan:RegisterEvent("ADDON_LOADED")
scan:SetScript("OnEvent", function()
if arg1 == addon then
local frame = _G[name]
if blacklist then
for _, disable in pairs(blacklist) do
_G[disable]:EnableMouse(false)
end
end
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", OnDragStart)
frame:SetScript("OnDragStop", OnDragStop)
this:UnregisterAllEvents()
end
end)
else
if blacklist then
for _, disable in pairs(blacklist) do
_G[disable]:EnableMouse(false)
end
end
local frame = name
if type(name) == "string" then frame = _G[name] end
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", OnDragStart)
frame:SetScript("OnDragStop", OnDragStop)
end
end
-- [ Copy Table ]
-- By default a table assignment only will be a reference instead of a copy.
-- This is used to create a replicate of the actual table.
-- 'src' [table] the table that should be copied.
-- return: [table] the replicated table.
function pfUI.api.CopyTable(src)
local lookup_table = {}
local function _copy(src)
if type(src) ~= "table" then
return src
elseif lookup_table[src] then
return lookup_table[src]
end
local new_table = {}
lookup_table[src] = new_table
for index, value in pairs(src) do
new_table[_copy(index)] = _copy(value)
end
return setmetatable(new_table, getmetatable(src))
end
return _copy(src)
end
-- [ Wipe Table ]
-- Empties a table and returns it
-- 'src' [table] the table that should be emptied.
-- return: [table] the emptied table.
function pfUI.api.wipe(src)
-- notes: table.insert, table.remove will have undefined behavior
-- when used on tables emptied this way because Lua removes nil
-- entries from tables after an indeterminate time.
-- Instead of table.insert(t,v) use t[table.getn(t)+1]=v as table.getn collapses nil entries.
-- There are no issues with hash tables, t[k]=v where k is not a number behaves as expected.
local mt = getmetatable(src) or {}
if mt.__mode == nil or mt.__mode ~= "kv" then
mt.__mode = "kv"
src=setmetatable(src,mt)
end
for k in pairs(src) do
src[k] = nil
end
return src
end
-- [ Load Movable ]
-- Loads the positions of a Frame.
-- 'frame' [frame] the frame that should be positioned.
-- 'init' [bool] treats the current position as initial data
function pfUI.api.LoadMovable(frame, init)
-- update position data
if not frame.posdata or init then
frame.posdata = { scale = frame:GetScale(), pos = {} }
for i=1,frame:GetNumPoints() do
frame.posdata.pos[i] = { frame:GetPoint(i) }
end
end
if pfUI_config["position"][frame:GetName()] then
if pfUI_config["position"][frame:GetName()]["parent"] then
frame:SetParent(_G[pfUI_config["position"][frame:GetName()]["parent"]])
end
if pfUI_config["position"][frame:GetName()]["scale"] then
frame:SetScale(pfUI_config["position"][frame:GetName()].scale)
end
if pfUI_config["position"][frame:GetName()]["xpos"] then
local anchor = pfUI_config["position"][frame:GetName()]["anchor"] or "TOPLEFT"
frame:ClearAllPoints()
frame:SetPoint(anchor, pfUI_config["position"][frame:GetName()].xpos, pfUI_config["position"][frame:GetName()].ypos)
end
elseif frame.posdata and frame.posdata.pos[1] then
frame:ClearAllPoints()
frame:SetScale(frame.posdata.scale)
for id, point in pairs(frame.posdata.pos) do
local a, b, c, d, e = unpack(point)
if a and b then frame:SetPoint(a,b,c,d,e) end
end
end
end
-- [ Save Movable ]
-- Save the positions of a Frame.
-- 'frame' [frame] the frame that should be saved.
function pfUI.api.SaveMovable(frame, scale)
local anchor, _, _, xpos, ypos = frame:GetPoint()
C.position[frame:GetName()] = C.position[frame:GetName()] or {}
C.position[frame:GetName()]["xpos"] = round(xpos)
C.position[frame:GetName()]["ypos"] = round(ypos)
C.position[frame:GetName()]["anchor"] = anchor
C.position[frame:GetName()]["parent"] = frame:GetParent() and frame:GetParent():GetName() or nil
if scale then
C.position[frame:GetName()]["scale"] = frame:GetScale()
end
end
-- [ Update Movable ]
-- Loads and update the configured position of the specified frame.
-- It also creates an entry in the movables table.
-- 'frame' [frame] the frame that should be updated.
-- 'init' [bool] treats the current position as initial data
function pfUI.api.UpdateMovable(frame, init)
local name = frame:GetName()
if pfUI_config.global.offscreen == "0" then
frame:SetClampedToScreen(true)
end
if not pfUI.movables[name] then
pfUI.movables[name] = frame
end
LoadMovable(frame, init)
end
-- [ Remove Movable ]
-- Removes a Frame from the movable list.
-- 'frame' [frame] the frame that should be removed.
function pfUI.api.RemoveMovable(frame)
local name = frame:GetName()
pfUI.movables[name] = nil
end
-- [ AlignToPosition ]
-- Sets a frame to the selected anchor
-- 'frame' [frame] the frame that should be aligned
-- 'anchor' [frame] the frame where it should be aligned to
-- 'position' [string] where it should appear, takes the following:
-- "TOP", "RIGHT", "BOTTOM", "LEFT"
function pfUI.api.AlignToPosition(frame, anchor, position, spacing)
if frame == anchor then return end
frame:ClearAllPoints()
if position == "TOP" and anchor then
frame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, (spacing or 0))
frame:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, (spacing or 0))
elseif position == "RIGHT" and anchor then
frame:SetPoint("TOPLEFT", anchor, "TOPRIGHT", (spacing or 0), 0)
frame:SetPoint("BOTTOMLEFT", anchor, "BOTTOMRIGHT", (spacing or 0), 0)
elseif position == "BOTTOM" and anchor then
frame:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -(spacing or 0))
frame:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -(spacing or 0))
elseif position == "LEFT" and anchor then
frame:SetPoint("TOPRIGHT", anchor, "TOPLEFT", -(spacing or 0), 0)
frame:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMLEFT", -(spacing or 0), 0)
else
frame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", 0, 0)
frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", 0, 0)
end
end
-- [ SetAutoPoint ]
-- Automatically places the frame according to screen position of the parent.
-- 'frame' [frame] the frame that should be moved.
-- 'parent' [frame] the frame's anchor point
-- 'spacing' [number] the padding that should be used between the
-- frame and its parent frame
function pfUI.api.SetAutoPoint(frame, parent, spacing)
--[[
a b max
+-----------------+
| 1 | 2 | 3 |
|-----+-----+-----| c
| 4 | 5 | 6 |
|-----+-----+-----| d
| 7 | 8 | 9 |
+-----------------+
0
]]--
local a = GetScreenWidth() / 3
local b = GetScreenWidth() / 3 * 2
local c = GetScreenHeight() / 3 * 2
local d = GetScreenHeight() / 3
local x, y = parent:GetCenter()
if not x or not y then return end
local off = spacing or 0
frame:ClearAllPoints()
if x < a and y > c then
-- TOPLEFT
frame:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 0, -off)
elseif x > a and x < b and y > c then
-- TOP
frame:SetPoint("TOP", parent, "BOTTOM", 0, -off)
elseif x > b and y > c then
-- TOPRIGHT
frame:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, -off)
elseif x < a and y > d and y < c then
-- LEFT
frame:SetPoint("LEFT", parent, "RIGHT", off, 0)
elseif x > a and x < b and y > d and y < c then
-- CENTER
frame:SetPoint("BOTTOM", parent, "TOP", 0, off)
elseif x > b and y > d and y < c then
-- RIGHT
frame:SetPoint("RIGHT", parent, "LEFT", -off, 0)
elseif x < a and y < d then
-- BOTTOMLEFT
frame:SetPoint("BOTTOMLEFT", parent, "TOPLEFT", 0, off)
elseif x > a and x < b and y < d then
-- BOTTOM
frame:SetPoint("BOTTOM", parent, "TOP", 0, off)
elseif x > b and y < d then
-- BOTTOMRIGHT
frame:SetPoint("BOTTOMRIGHT", parent, "TOPRIGHT", 0, off)
end
end
-- [ GetBestAnchor ]
-- Returns the best anchor of a frame, based on its position
-- 'self' [frame] the frame that should be checked
-- returns: [string] the name of the best anchor
function pfUI.api.GetBestAnchor(self)
local scale = self:GetScale()
local x, y = self:GetCenter()
local a = GetScreenWidth() / scale / 3
local b = GetScreenWidth() / scale / 3 * 2
local c = GetScreenHeight() / scale / 3 * 2
local d = GetScreenHeight() / scale / 3
if not x or not y then return end
if x < a and y > c then
return "TOPLEFT"
elseif x > a and x < b and y > c then
return "TOP"
elseif x > b and y > c then
return "TOPRIGHT"
elseif x < a and y > d and y < c then
return "LEFT"
elseif x > a and x < b and y > d and y < c then
return "CENTER"
elseif x > b and y > d and y < c then
return "RIGHT"
elseif x < a and y < d then
return "BOTTOMLEFT"
elseif x > a and x < b and y < d then
return "BOTTOM"
elseif x > b and y < d then
return "BOTTOMRIGHT"
end
end
-- [ ConvertFrameAnchor ]
-- Converts a frame anchor into another one while preserving the frame position
-- 'self' [frame] the frame that should get another anchor.
-- 'anchor' [string] the new anchor that shall be used
-- returns: anchor, x, y can directly be used in SetPoint()
function pfUI.api.ConvertFrameAnchor(self, anchor)
local scale, x, y, _ = self:GetScale(), nil, nil, nil
if anchor == "CENTER" then
x, y = self:GetCenter()
x, y = x - GetScreenWidth()/2/scale, y - GetScreenHeight()/2/scale
elseif anchor == "TOPLEFT" then
x, y = self:GetLeft(), self:GetTop() - GetScreenHeight()/scale
elseif anchor == "TOP" then
x, _ = self:GetCenter()
x, y = x - GetScreenWidth()/2/scale, self:GetTop() - GetScreenHeight()/scale
elseif anchor == "TOPRIGHT" then
x, y = self:GetRight() - GetScreenWidth()/scale, self:GetTop() - GetScreenHeight()/scale
elseif anchor == "RIGHT" then
_, y = self:GetCenter()
x, y = self:GetRight() - GetScreenWidth()/scale, y - GetScreenHeight()/2/scale
elseif anchor == "BOTTOMRIGHT" then
x, y = self:GetRight() - GetScreenWidth()/scale, self:GetBottom()
elseif anchor == "BOTTOM" then
x, _ = self:GetCenter()
x, y = x - GetScreenWidth()/2/scale, self:GetBottom()
elseif anchor == "BOTTOMLEFT" then
x, y = self:GetLeft(), self:GetBottom()
elseif anchor == "LEFT" then
_, y = self:GetCenter()
x, y = self:GetLeft(), y - GetScreenHeight()/2/scale
end
return anchor, round(x, 2), round(y, 2)
end
-- [ GetStringColor ]
-- Queries the pfUI setting strings and extract its color codes
-- returns r,g,b,a
local color_cache = {}
function pfUI.api.GetStringColor(colorstr)
if not color_cache[colorstr] then
local r, g, b, a = pfUI.api.strsplit(",", colorstr)
color_cache[colorstr] = { r, g, b, a }
end
return unpack(color_cache[colorstr])
end
-- [ rgbhex ]
-- Returns color format from color info
-- 'r' [table | number] color table or r color component
-- 'g' [number] optional g color component
-- 'b' [number] optional b color component
-- 'a' [number] optional alpha component
-- returns color string in the form of '|caarrggbb'
local _r, _g, _b, _a
function pfUI.api.rgbhex(r, g, b, a)
if type(r) == "table" then
if r.r then
_r, _g, _b, _a = r.r, r.g, r.b, (r.a or 1)
elseif table.getn(r) >= 3 then
_r, _g, _b, _a = r[1], r[2], r[3], (r[4] or 1)
end
elseif tonumber(r) then
_r, _g, _b, _a = r, g, b, (a or 1)
end
if _r and _g and _b and _a then
-- limit values to 0-1
_r = _r + 0 > 1 and 1 or _r + 0
_g = _g + 0 > 1 and 1 or _g + 0
_b = _b + 0 > 1 and 1 or _b + 0
_a = _a + 0 > 1 and 1 or _a + 0
return string.format("|c%02x%02x%02x%02x", _a*255, _r*255, _g*255, _b*255)
end
return ""
end
-- [ GetBorderSize ]
-- Returns the configure value of a border and its pixel scaled version.
-- 'pref' allows to specifiy a custom border (i.e unitframes, panel)
function pfUI.api.GetBorderSize(pref)
if not pfUI.borders then pfUI.borders = {} end
-- set to default border if accessing a wrong border type
if not pref or not pfUI_config.appearance.border[pref] or pfUI_config.appearance.border[pref] == "-1" then
pref = "default"
end
if pfUI.borders[pref] then
-- return already cached values
return pfUI.borders[pref][1], pfUI.borders[pref][2]
else
-- add new borders to the pfUI tree
local raw = tonumber(pfUI_config.appearance.border[pref])
if raw == -1 then raw = 3 end
local scaled = raw * GetPerfectPixel()
pfUI.borders[pref] = { raw, scaled }
return raw, scaled
end
end
-- [ GetPerfectPixel ]
-- Returns a number that equals a real pixel on regular scaled frames.
-- Respects the current UI-scale and calculates a real pixel based on
-- the screen resolution and the 768px sized drawlayer.
function pfUI.api.GetPerfectPixel()
if pfUI.pixel then return pfUI.pixel end
if pfUI_config.appearance.border.pixelperfect == "1" then
local scale = GetCVar("uiScale")
local resolution = GetCVar("gxResolution")
local _, _, screenwidth, screenheight = strfind(resolution, "(.+)x(.+)")
pfUI.pixel = 768 / screenheight / scale
pfUI.pixel = pfUI.pixel > 1 and 1 or pfUI.pixel
-- autodetect and zoom for HiDPI displays
if pfUI_config.appearance.border.hidpi == "1" then
pfUI.pixel = pfUI.pixel < .5 and pfUI.pixel * 2 or pfUI.pixel
end
else
pfUI.pixel = .7
end
pfUI.backdrop = {
bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0,
edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = pfUI.pixel,
insets = {left = -pfUI.pixel, right = -pfUI.pixel, top = -pfUI.pixel, bottom = -pfUI.pixel},
}
pfUI.backdrop_thin = {
bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0,
edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = pfUI.pixel,
insets = {left = 0, right = 0, top = 0, bottom = 0},
}
return pfUI.pixel
end
-- [ Create Backdrop ]
-- Creates a pfUI compatible frame as backdrop element
-- 'f' [frame] the frame which should get a backdrop.
-- 'inset' [int] backdrop inset, defaults to border size.
-- 'legacy' [bool] use legacy backdrop instead of creating frames.
-- 'transp' [number] set default transparency
local backdrop, b, level, rawborder, border, br, bg, bb, ba, er, eg, eb, ea
function pfUI.api.CreateBackdrop(f, inset, legacy, transp, backdropSetting)
-- exit if now frame was given
if not f then return end
-- load raw and pixel perfect scaled border
rawborder, border = GetBorderSize()
-- load custom border if existing
if inset then
rawborder = inset / GetPerfectPixel()
border = inset
end
-- detect if blizzard backdrops shall be used
local blizz = C.appearance.border.force_blizz == "1" and true or nil
backdrop = blizz and pfUI.backdrop_blizz_full or rawborder == 1 and pfUI.backdrop_thin or pfUI.backdrop
border = blizz and math.max(border, 3) or border
-- get the color settings
br, bg, bb, ba = pfUI.api.GetStringColor(pfUI_config.appearance.border.background)
er, eg, eb, ea = pfUI.api.GetStringColor(pfUI_config.appearance.border.color)
if transp and transp < tonumber(ba) then ba = transp end
-- use legacy backdrop handling
if legacy then
if backdropSetting then f:SetBackdrop(backdropSetting) end
f:SetBackdrop(backdrop)
f:SetBackdropColor(br, bg, bb, ba)
f:SetBackdropBorderColor(er, eg, eb , ea)
else
-- increase clickable area if available
if f.SetHitRectInsets and ( not InCombatLockdown or not InCombatLockdown()) then
f:SetHitRectInsets(-border,-border,-border,-border)
end
-- use new backdrop behaviour
if not f.backdrop then
if f:GetBackdrop() then f:SetBackdrop(nil) end
local b = CreateFrame("Frame", nil, f)
level = f:GetFrameLevel()
if level < 1 then
b:SetFrameLevel(level)
else
b:SetFrameLevel(level - 1)
end
f.backdrop = b
end
f.backdrop:SetPoint("TOPLEFT", f, "TOPLEFT", -border, border)
f.backdrop:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", border, -border)
f.backdrop:SetBackdrop(backdrop)
f.backdrop:SetBackdropColor(br, bg, bb, ba)
f.backdrop:SetBackdropBorderColor(er, eg, eb , ea)
if blizz then
if not f.backdrop_border then
local border = CreateFrame("Frame", nil, f.backdrop)
border:SetFrameLevel(level + 2)
f.backdrop_border = border
local hookSetBackdropBorderColor = f.backdrop.SetBackdropBorderColor
f.backdrop.SetBackdropBorderColor = function(self, r, g, b, a)
f.backdrop_border:SetBackdropBorderColor(r, g, b, a)
hookSetBackdropBorderColor(f.backdrop, r, g, b, a)
end
end
f.backdrop_border:SetAllPoints(f.backdrop)
f.backdrop_border:SetBackdrop(pfUI.backdrop_blizz_border)
f.backdrop_border:SetBackdropBorderColor(er, eg, eb , ea)
end
end
end
-- [ Create Shadow ]
-- Creates a pfUI compatible frame as shadow element
-- 'f' [frame] the frame which should get a backdrop.
function pfUI.api.CreateBackdropShadow(f)
-- exit if now frame was given
if not f then return end
if f.backdrop_shadow or pfUI_config.appearance.border.shadow ~= "1" then
return
end
local anchor = f.backdrop or f
f.backdrop_shadow = CreateFrame("Frame", nil, anchor)
f.backdrop_shadow:SetFrameStrata("BACKGROUND")
f.backdrop_shadow:SetFrameLevel(1)
f.backdrop_shadow:SetPoint("TOPLEFT", anchor, "TOPLEFT", -5, 5)
f.backdrop_shadow:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 5, -5)
f.backdrop_shadow:SetBackdrop(pfUI.backdrop_shadow)
f.backdrop_shadow:SetBackdropBorderColor(0, 0, 0, tonumber(pfUI_config.appearance.border.shadow_intensity))
end
-- [ Bar Layout Options ] --
-- 'barsize' size of bar in number of buttons
-- returns: array of options as strings for pfUI.gui.bar
function pfUI.api.BarLayoutOptions(barsize)
assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarLayoutOptions: barsize "..tostring(barsize).." is invalid")
local options = {}
for i,layout in ipairs(pfGridmath[barsize]) do
options[i] = string.format("%d x %d",layout[1],layout[2])
end
return options
end
-- [ Bar Layout Formfactor ] --
-- 'option' string option as used in pfUI_config.bars[bar].option
-- returns: integer formfactor
local formfactors = {}
setmetatable(formfactors, {__mode = "v"}) -- weak table so values not referenced are collected on next gc
function pfUI.api.BarLayoutFormfactor(option)
if formfactors[option] then
return formfactors[option]
else
for barsize,_ in ipairs(pfGridmath) do
local options = pfUI.api.BarLayoutOptions(barsize)
for i,opt in ipairs(options) do
if opt == option then
formfactors[option] = i
return formfactors[option]
end
end
end
end
end
-- [ Bar Layout Size ] --
-- 'bar' frame reference,
-- 'barsize' integer number of buttons,
-- 'formfactor' string formfactor in cols x rows,
-- 'padding' the spacing between buttons
function pfUI.api.BarLayoutSize(bar,barsize,formfactor,iconsize,bordersize,padding)
assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarLayoutSize: barsize "..tostring(barsize).." is invalid")
local formfactor = pfUI.api.BarLayoutFormfactor(formfactor)
local cols, rows = unpack(pfGridmath[barsize][formfactor])
local width = (iconsize + bordersize*2+padding) * cols + padding
local height = (iconsize + bordersize*2+padding) * rows + padding
bar._size = {width,height}
return bar._size
end
-- [ Bar Button Anchor ] --
-- 'button' frame reference
-- 'basename' name of button frame without index
-- 'buttonindex' index number of button on bar
-- 'formfactor' string formfactor in cols x rows
-- 'iconsize' size of the button
-- 'bordersize' default bordersize
-- 'padding' the spacing between buttons
function pfUI.api.BarButtonAnchor(button,basename,buttonindex,barsize,formfactor,iconsize,bordersize,padding)
assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarButtonAnchor: barsize "..tostring(barsize).." is invalid")
local formfactor = pfUI.api.BarLayoutFormfactor(formfactor)
local parent = button:GetParent()
local cols, rows = unpack(pfGridmath[barsize][formfactor])
if buttonindex == 1 then
button._anchor = {"TOPLEFT", parent, "TOPLEFT", bordersize+padding, -bordersize-padding}
else
local col = buttonindex-((math.ceil(buttonindex/cols)-1)*cols)
button._anchor = col==1 and {"TOP",_G[basename..(buttonindex-cols)],"BOTTOM",0,-(bordersize*2+padding)} or {"LEFT",_G[basename..(buttonindex-1)],"RIGHT",(bordersize*2+padding),0}
end
return button._anchor
end
-- [ Enable Autohide ] --
-- 'frame' the frame that should be hidden
function pfUI.api.EnableAutohide(frame, timeout, combat)
if not frame then return end
local timeout = timeout
frame.hover = frame.hover or CreateFrame("Frame", frame:GetName() .. "Autohide", frame)
frame.hover:SetParent(frame)
frame.hover:SetAllPoints(frame)
frame.hover.parent = frame
frame.hover:Show()
if combat then
frame.hover:RegisterEvent("PLAYER_REGEN_ENABLED")
frame.hover:RegisterEvent("PLAYER_REGEN_DISABLED")
frame.hover:SetScript("OnEvent", function()
if event == "PLAYER_REGEN_DISABLED" then
this.parent:SetAlpha(1)
this.activeTo = "keep"
elseif event == "PLAYER_REGEN_ENABLED" then
this.activeTo = GetTime() + timeout
end
end)
end
frame.hover:SetScript("OnUpdate", function()
-- throttle to 0.05s
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.05
if this.activeTo == "keep" then return end
if MouseIsOver(this, 10, -10, -10, 10) then
this.activeTo = GetTime() + timeout
this.parent:SetAlpha(1)
elseif this.activeTo then
if this.activeTo < GetTime() and this.parent:GetAlpha() > 0 then
local fps = (60 / math.max(GetFramerate(), 1))
this.parent:SetAlpha(this.parent:GetAlpha() - 0.05*fps)
end
else
this.activeTo = GetTime() + timeout
end
end)
end
-- [ Disable Autohide ] --
-- 'frame' the frame that should get the autohide removed
function pfUI.api.DisableAutohide(frame)
if not frame then return end
if not frame.hover then return end
frame.hover:SetScript("OnEvent", nil)
frame.hover:SetScript("OnUpdate", nil)
frame.hover:Hide()
frame:SetAlpha(1)
end
-- [ GetColoredTime ] --
-- 'remaining' the time in seconds that should be converted
-- return a colored string including a time unit (m/h/d)
local color_day, color_hour, color_minute, color_low, color_normal
function pfUI.api.GetColoredTimeString(remaining)
if not remaining then return "" end
-- Show days if remaining is > 99 Hours (99 * 60 * 60)
if remaining > 356400 then
if not color_day then
local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.daycolor)
color_day = pfUI.api.rgbhex(r,g,b)
end
return color_day .. round(remaining / 86400) .. "|rd"
-- Show hours if remaining is > 99 Minutes (99 * 60)
elseif remaining > 5940 then
if not color_hour then
local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.hourcolor)
color_hour = pfUI.api.rgbhex(r,g,b)
end
return color_hour .. round(remaining / 3600) .. "|rh"
-- Show minutes if remaining is > 99 Seconds (99)
elseif remaining > 99 then
if not color_minute then
local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.minutecolor)
color_minute = pfUI.api.rgbhex(r,g,b)
end
return color_minute .. round(remaining / 60) .. "|rm"
-- Show milliseconds on low
elseif remaining <= 5 and pfUI_config.appearance.cd.milliseconds == "1" then
if not color_low then
local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.lowcolor)
color_low = pfUI.api.rgbhex(r,g,b)
end
return color_low .. string.format("%.1f", round(remaining,1))
-- Show seconds on low
elseif remaining <= 5 then
if not color_low then
local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.lowcolor)
color_low = pfUI.api.rgbhex(r,g,b)
end
return color_low .. round(remaining)
-- Show seconds on normal
elseif remaining >= 0 then
if not color_normal then
local r, g, b, a = pfUI.api.GetStringColor(C.appearance.cd.normalcolor)
color_normal = pfUI.api.rgbhex(r,g,b)
end
return color_normal .. round(remaining)
-- Return empty
else
return ""
end
end
-- [ GetColorGradient ] --
-- 'perc' percentage (0-1)
-- return r,g,b and hexcolor
local gradientcolors = {}
function pfUI.api.GetColorGradient(perc)
perc = perc > 1 and 1 or perc
perc = perc < 0 and 0 or perc
perc = floor(perc*100)/100
local index = perc
if not gradientcolors[index] then
local r1, g1, b1, r2, g2, b2
if perc <= 0.5 then
perc = perc * 2
r1, g1, b1 = 1, 0, 0
r2, g2, b2 = 1, 1, 0
else
perc = perc * 2 - 1
r1, g1, b1 = 1, 1, 0
r2, g2, b2 = 0, 1, 0
end
local r = round(r1 + (r2 - r1) * perc, 4)
local g = round(g1 + (g2 - g1) * perc, 4)
local b = round(b1 + (b2 - b1) * perc, 4)
local h = pfUI.api.rgbhex(r,g,b)
gradientcolors[index] = {}
gradientcolors[index].r = r
gradientcolors[index].g = g
gradientcolors[index].b = b
gradientcolors[index].h = h
end
return gradientcolors[index].r,
gradientcolors[index].g,
gradientcolors[index].b,
gradientcolors[index].h
end
-- [ GetNoNameObject ] --
-- 'frame' [string] parent frame
-- 'scantype' [string] scanning Region or Children
-- 'objtype' [string]
-- 'layer' [string]
-- 'arg1' [string]
-- return object
function pfUI.api.GetNoNameObject(frame, objtype, layer, arg1, arg2)
local arg1 = arg1 and gsub(arg1, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
local arg2 = arg2 and gsub(arg2, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
local objects
if objtype == "Texture" or objtype == "FontString" then
objects = {frame:GetRegions()}
else
objects = {frame:GetChildren()}
end
for _, object in ipairs(objects) do
local check = true
if object:GetObjectType() ~= objtype or (layer and object:GetDrawLayer() ~= layer) then check = false end
if check then
if objtype == "Texture" and object.SetTexture and object:GetTexture() ~= "Interface\\BUTTONS\\WHITE8X8" then
if arg1 then
local texture = object:GetTexture()
if texture and not string.find(texture, arg1, 1) then check = false end
end
if check then return object end
elseif objtype == "FontString" and object.SetText then
if arg1 then
local text = object:GetText()
if text and not string.find(text, arg1, 1) then check = false end
end
if check then return object end
elseif objtype == "Button" and object.GetNormalTexture and object:GetNormalTexture() then
if arg1 then
local texture = object:GetNormalTexture():GetTexture()
if texture and not string.find(texture, arg1, 1) then check = false end
end
if arg2 then
local text = object:GetText()
if text and not string.find(text, arg2, 1) then check = false end
end
if check then return object end
end
end
end
end
-- [ TryMemoizedFuncLoadstringForSpellCasts ]
-- Memoizes lua function strings for spell casts to improve performance.
-- Supports both string functions and direct function values.
-- 'funcOrStr' [function|string] Either a function or a lua string to execute
-- return: [function|nil] The function to execute, or nil on error
local memoizedFuncs = {}
function pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(funcOrStr)
-- If it's already a function, return it directly
if type(funcOrStr) == "function" then
return funcOrStr
end
-- If it's a string, try to memoize it
if type(funcOrStr) == "string" then
-- Check if we've already compiled this string
if memoizedFuncs[funcOrStr] then
return memoizedFuncs[funcOrStr]
end
-- Try to compile the string
local func = loadstring(funcOrStr)
if func then
memoizedFuncs[funcOrStr] = func
return func
end
end
return nil
end