Files
OEVGandClaude Opus 5.5 0f803aca4d Optional ClassicAPI support
When ClassicAPI is installed and "Use ClassicAPI" is on (Mods tab, reload):
- Detect ClassicAPI and each feature separately; show it in the Mods tab
- Read auras with C_UnitAuras.GetAuraSlots (fill form) + UnitAuraBySlot,
  reusing entries, with ClassicAPI durations, expirations and casters
- UNIT_AURA_DELTA: refresh-only events update stacks and timers in place
  without rebuilding the aura icons or the health bar
- Aura timers without SuperWoW; native Cooldown frames with one shared
  text driver instead of a Model and OnUpdateModel per icon
- UNIT_IN_RANGE_UPDATE, UNIT_DISTANCE_CHECK_UPDATE, UNIT_CONNECTION and
  INCOMING_RESURRECT_CHANGED; UnitHasIncomingResurrection in res checks
- HealPredict learns heals from COMBAT_LOG_EVENT_UNFILTERED when the
  GUID formats match SuperWoW's
- Distance and line of sight when UnitXP SP3 and SuperWoW are missing

Without ClassicAPI, or with the option off, behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwwTK1hwTEWHqsan2mV
2026-11-02 19:36:30 +00:00

1786 lines
52 KiB
Lua

-- Contains standalone utility functions that cause no side effects and don't require data from other files, other than the unit proxy
PTUtil = {}
function PTUtil.SetEnvironment(t, index)
setmetatable(t, {__index = index or PTUnitProxy or getfenv(1)})
setfenv(2, t)
end
local _G = getfenv(0)
PTUtil.SetEnvironment(PTUtil)
local getn = table.getn
Classes = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "SHAMAN", "MAGE", "WARLOCK", "DRUID"}
HealerClasses = {"PRIEST", "DRUID", "SHAMAN", "PALADIN"}
UnitXPSP3 = pcall(UnitXP, "inSight", "player", "player") -- WTB better way to check for UnitXP SP3
UnitXPSP3_Version = -1
if UnitXPSP3 and pcall(UnitXP, "version", "coffTimeDateStamp") then
UnitXPSP3_Version = UnitXP("version", "coffTimeDateStamp") or -1
end
UnitXPSP3Latest = 0
SuperWoW = SpellInfo ~= nil
SuperWoWFeatureLevel = 0
SuperWoW_v1_2 = 1
SuperWoW_v1_3 = 2
SuperWoW_v1_4 = 3
SuperWoWLatest = SuperWoW_v1_4
if SUPERWOW_VERSION then
if SUPERWOW_VERSION == "1.2" then
SuperWoWFeatureLevel = SuperWoW_v1_2
elseif SUPERWOW_VERSION == "1.3" then
SuperWoWFeatureLevel = SuperWoW_v1_3
else -- Anything newer than 1.3 is considered as 1.4 feature set
SuperWoWFeatureLevel = SuperWoW_v1_4
end
end
Nampower = QueueSpellByName ~= nil
NampowerFeatureLevel = 0
Nampower_v3_0 = 1
NampowerLatest = Nampower_v3_0
if GetNampowerVersion then
local major, minor, patch = GetNampowerVersion()
if major >= 3 then
NampowerFeatureLevel = Nampower_v3_0
end
end
VanillaUtils = pcall(UnitXP, "unitPosition", "player")
-- ClassicAPI (github.com/brues-code/ClassicAPI). Every feature is detected on its own, so an older
-- ClassicAPI only loses the parts it lacks. CLASSIC_API_VERSION is X*10000 + Y*100 + Z (0 on dev builds).
ClassicAPI = CLASSIC_API_VERSION ~= nil
ClassicAPIRecommended = 11810 -- 1.18.10: the unit events used below
do
local function eventValid(event)
return C_EventUtils ~= nil and C_EventUtils.IsEventValid ~= nil and C_EventUtils.IsEventValid(event) and true or false
end
local auras = C_UnitAuras
ClassicAPIFeatures = {
-- C_UnitAuras slot enumeration; the fill-a-table form is checked on first use (see UseClassicAuraSlots)
AuraSlots = ClassicAPI and auras ~= nil and auras.GetAuraSlots ~= nil and auras.UnitAuraBySlot ~= nil,
AuraDelta = ClassicAPI and eventValid("UNIT_AURA_DELTA"),
Cooldown = ClassicAPI, -- Native Cooldown frames are checked on first use (see CreateNativeCooldown)
Distance = ClassicAPI and UnitDistanceSquared ~= nil,
Sight = ClassicAPI and UnitInLineOfSight ~= nil,
RangeEvent = ClassicAPI and eventValid("UNIT_IN_RANGE_UPDATE"),
ConnectionEvent = ClassicAPI and eventValid("UNIT_CONNECTION"),
VisibilityEvent = ClassicAPI and eventValid("UNIT_DISTANCE_CHECK_UPDATE"),
Resurrection = ClassicAPI and UnitHasIncomingResurrection ~= nil and eventValid("INCOMING_RESURRECT_CHANGED"),
CombatLog = ClassicAPI and CombatLogGetCurrentEventInfo ~= nil and eventValid("COMBAT_LOG_EVENT_UNFILTERED")
}
end
-- Set by ApplyClassicAPIOptions once saved options are loaded; false until then and when turned off
ClassicAPIEnabled = false
EnabledMods = {
["SuperWoW"] = SuperWoW and SuperWoWFeatureLevel,
["Nampower"] = Nampower and NampowerFeatureLevel,
["UnitXP SP3"] = UnitXPSP3 and 0,
["VanillaUtils"] = VanillaUtils and 0,
["ClassicAPI"] = ClassicAPI and (CLASSIC_API_VERSION or 0)
}
-- Providers are ordered by preference
ModFeatures = {
{
Name = "GUID Units",
Description = "Better tracking of players/mobs. Grants the ability to add players/mobs to a separate Focus frame.",
Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}}
},
{
Name = "Aura Timers",
Description = "See timers on buffs and debuffs, with Nampower providing many more than SuperWoW.",
Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}, {"ClassicAPI"}}
},
{
Name = "Friendly Distance",
Description = "See precise distance to friendly players, with UnitXP SP3 being more accurate.",
Providers = {{"UnitXP SP3"}, {"SuperWoW"}, {"ClassicAPI"}}
},
{
Name = "Enemy Distance",
Description = "See precise distance to enemies.",
Providers = {{"UnitXP SP3"}, {"ClassicAPI"}}
},
{
Name = "Friendly Position",
Hidden = true,
Providers = {{"SuperWoW"}, {"VanillaUtils"}}
},
{
Name = "Enemy Position",
Hidden = true,
Providers = {{"VanillaUtils"}}
},
{
Name = "Heal Predictions",
Description = "See incoming healing from players that do not have HealComm and predict more accurate numbers.",
Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}}
},
{
Name = "Mouseover",
Description = "Set your actual mouseover target when hovering over unit frames.",
Providers = {{"SuperWoW"}}
},
{
Name = "Direct Casting",
Description = "Directly cast spells rather than using split-second target switching tricks.",
Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}}
},
{
Name = "LOS",
Description = "See when someone is out of your line-of-sight.",
Providers = {{"UnitXP SP3"}, {"ClassicAPI"}}
},
{
Name = "Spell Queue",
Description = "Queue spell casts like in modern versions of WoW, drastically increasing casting efficiency.",
Providers = {{"Nampower"}}
}
}
EnabledModFeatures = {}
for _, feature in ipairs(ModFeatures) do
for _, providerEntry in ipairs(feature.Providers) do
local modInfo = EnabledMods[providerEntry[1]]
if modInfo and (not providerEntry[2] or modInfo >= providerEntry[2]) then
feature.ChosenProvider = providerEntry[1]
EnabledModFeatures[feature.Name] = providerEntry[1]
break
end
end
end
function GetModState(present, upToDate)
if not present then
return "Not Detected"
end
if not upToDate then
return "Outdated"
end
return "Detected"
end
function IsSuperWowPresent()
return SuperWoW
end
function GetSuperWoWVersionText()
if not SuperWoW then
return ""
end
return "Version "..(SUPERWOW_VERSION or "1.1 or older")
end
function IsSuperWoWUpToDate()
return SuperWoWFeatureLevel >= SuperWoWLatest
end
function GetSuperWoWVersionState()
return GetModState(IsSuperWowPresent(), IsSuperWoWUpToDate())
end
function IsUnitXPSP3Present()
return UnitXPSP3
end
function GetUnitXPSP3VersionText()
if not UnitXPSP3 then
return ""
end
return UnitXPSP3_Version > -1 and "Version "..date("%x", UnitXPSP3_Version) or "Old Version"
end
function IsUnitXPSP3UpToDate()
return UnitXPSP3_Version >= UnitXPSP3Latest
end
function GetUnitXPSP3VersionState()
return GetModState(IsUnitXPSP3Present(), IsUnitXPSP3UpToDate())
end
-- Doesn't detect Namreeb's Nampower
function IsNampowerPresent()
return Nampower
end
function GetNampowerVersionText()
if not Nampower then
return ""
end
if not GetNampowerVersion then
return "Old Version"
end
local major, minor, patch = GetNampowerVersion()
return "Version "..major.."."..minor.."."..patch
end
function IsNampowerUpToDate()
return NampowerFeatureLevel >= NampowerLatest
end
function GetNampowerVersionState()
return GetModState(IsNampowerPresent(), IsNampowerUpToDate())
end
function IsClassicAPIPresent()
return ClassicAPI
end
function GetClassicAPIVersionText()
if not ClassicAPI then
return ""
end
local version = CLASSIC_API_VERSION or 0
if version <= 0 then
return "Development Build"
end
return "Version "..math.floor(version / 10000).."."..math.mod(math.floor(version / 100), 100).."."..math.mod(version, 100)
end
function IsClassicAPIUpToDate()
local version = CLASSIC_API_VERSION or 0
return version <= 0 or version >= ClassicAPIRecommended
end
function GetClassicAPIVersionState()
return GetModState(IsClassicAPIPresent(), IsClassicAPIUpToDate())
end
function GetModVersionText(mod)
if mod == "ClassicAPI" then
return GetClassicAPIVersionText()
elseif mod == "SuperWoW" then
return GetSuperWoWVersionText()
elseif mod == "UnitXP SP3" then
return GetUnitXPSP3VersionText()
elseif mod == "Nampower" then
return GetNampowerVersionText()
elseif mod == "VanillaUtils" then
return "Unknown"
end
return "Invalid Mod: "..tostring(mod)
end
function GetModVersionState(mod)
if mod == "ClassicAPI" then
return GetClassicAPIVersionState()
elseif mod == "SuperWoW" then
return GetSuperWoWVersionState()
elseif mod == "UnitXP SP3" then
return GetUnitXPSP3VersionState()
elseif mod == "Nampower" then
return GetNampowerVersionState()
elseif mod == "VanillaUtils" then
return GetModState(true, true)
end
return "Invalid Mod: "..tostring(mod)
end
function HasModVersion(mod, version)
return EnabledMods[mod] and EnabledMods[mod] >= version
end
-- TURTLE_WOW_VERSION is the global the WoW 1.18.1 client defines, so it keeps its name
Client1181 = TURTLE_WOW_VERSION ~= nil
PowerColors = {
["mana"] = {0.1, 0.25, 1}, --{r = 0, g = 0, b = 0.882}, Not accurate, changed color to make brighter
["rage"] = {1, 0, 0},
["focus"] = {1, 0.5, 0.25},
["energy"] = {1, 1, 0}
}
ClassPowerTypes = {
["WARRIOR"] = "rage",
["PALADIN"] = "mana",
["HUNTER"] = "mana",
["ROGUE"] = "energy",
["PRIEST"] = "mana",
["SHAMAN"] = "mana",
["MAGE"] = "mana",
["WARLOCK"] = "mana",
["DRUID"] = "mana"
}
-- The power types IDs mapped in accordance to UnitPowerType
PowerTypeMap = {
[0] = "mana",
[1] = "rage",
[2] = "focus",
[3] = "energy"
}
ResurrectionSpells = {
["PRIEST"] = "Resurrection",
["PALADIN"] = "Redemption",
["SHAMAN"] = "Ancestral Spirit",
["DRUID"] = "Rebirth"
}
ResurrectionSpellsSet = {
["Resurrection"] = "PRIEST",
["Redemption"] = "PALADIN",
["Ancestral Spirit"] = "SHAMAN",
["Rebirth"] = "DRUID"
}
-- The default color Blizzard uses for text
DefaultTextColor = {1, 0.82, 0}
PartyUnits = {"player", "party1", "party2", "party3", "party4"}
PetUnits = {"pet", "partypet1", "partypet2", "partypet3", "partypet4"}
TargetUnits = {"target"}
RaidUnits = {}
for i = 1, MAX_RAID_MEMBERS do
RaidUnits[i] = "raid"..i
end
RaidPetUnits = {}
for i = 1, MAX_RAID_MEMBERS do
RaidPetUnits[i] = "raidpet"..i
end
CustomUnits = PTUnitProxy and PTUnitProxy.AllCustomUnits or {}
CustomUnitsSet = PTUnitProxy and PTUnitProxy.AllCustomUnitsSet or {}
FocusUnits = PTUnitProxy and PTUnitProxy.CustomUnitsMap["focus"] or {}
local unitArrays = {PartyUnits, PetUnits, RaidUnits, RaidPetUnits, TargetUnits}
AllUnits = {}
for _, unitArray in ipairs(unitArrays) do
for _, unit in ipairs(unitArray) do
table.insert(AllUnits, unit)
end
end
AllRealUnits = {}
for i, unit in ipairs(AllUnits) do
AllRealUnits[i] = unit
end
if PTUnitProxy then
for _, unit in ipairs(CustomUnits) do
table.insert(AllUnits, unit)
end
PTUnitProxy.RegisterUpdateListener(function()
local i = 1
for _, unit in ipairs(AllRealUnits) do
AllUnits[i] = unit
i = i + 1
end
for _, unit in ipairs(CustomUnits) do
AllUnits[i] = unit
i = i + 1
end
ClearTable(AllUnitsSet)
for k, v in pairs(ToSet(AllUnits)) do
AllUnitsSet[k] = v
end
end)
end
local assetsPath = "Interface\\AddOns\\Puppeteer\\assets\\"
function GetAssetsPath()
return assetsPath
end
-- Returns a new table with the elements of the given array being the keys with 1 being the value of all keys,
-- or the index if indexValue is true
function ToSet(array, indexValue, to)
local set = to or {}
for index, value in ipairs(array) do
set[value] = indexValue and index or 1
end
return set
end
-- Returns a new table with the keys of the given set being the values of the array
function ToArray(set)
local array = {}
for value, _ in pairs(set) do
table.insert(array, value)
end
return array
end
-- Adds the elements of otherArray to the array
function AppendArrayElements(array, otherArray)
for _, v in ipairs(otherArray) do
table.insert(array, v)
end
end
function IndexOf(table, value)
for i, v in ipairs(table) do
if v == value then
return i
end
end
return -1
end
function KeyOf(table, value)
for k, v in pairs(table) do
if v == value then
return k
end
end
end
function ArrayContains(table, value)
for _, v in ipairs(table) do
if v == value then
return true
end
end
return false
end
function RemoveElement(t, value)
table.remove(t, IndexOf(t, value))
end
function ReverseArray(t)
for i = 1, math.floor(getn(t) / 2) do
t[i], t[getn(t) - i + 1] = t[getn(t) - i + 1], t[i]
end
end
function CloneTable(table, deep)
local clone = {}
for k, v in pairs(table) do
if deep and type(v) == "table" then
clone[k] = CloneTable(v, true)
else
clone[k] = v
end
end
return clone
end
function ApplyTableDiffs(t, overrides)
for k, v in pairs(overrides) do
if t[k] ~= nil then
if type(v) == "table" then
if type(t[k]) == "table" then
ApplyTableDiffs(t[k], v)
else
t[k] = CloneTable(v, true)
end
else
t[k] = v
end
else
t[k] = type(v) == "table" and CloneTable(v, true) or v
end
end
end
local compost = PTPool
-- Recursively reclaims all tables this table contains
function CompostReclaim(t)
for k, v in pairs(t) do
if type(v) == "table" then
CompostReclaim(v)
end
end
compost:Reclaim(t)
end
function CloneTableCompost(t, deep)
local clone = compost:GetTable()
local n = 0
for k, v in pairs(t) do
if deep and type(v) == "table" then
clone[k] = CloneTableCompost(v, true)
else
clone[k] = v
end
n = n + 1
end
table.setn(clone, n)
return clone
end
function ClearTable(t)
for k, v in pairs(t) do
t[k] = nil
end
table.setn(t, 0)
end
function GetTableSize(t)
local size = 0
for _ in pairs(t) do
size = size + 1
end
return size
end
function IsTableEmpty(t)
for _ in pairs(t) do
return false
end
return true
end
-- Recursion not supported
function TableEquals(t1, t2)
-- Verify the tables have the same keys
for k, v in pairs(t1) do
if t2[k] == nil then
return false
end
end
for k, v in pairs(t2) do
if t1[k] == nil then
return false
end
end
-- Verify the tables have equal values
for k, v in pairs(t1) do
if type(v) == "table" then
if type(t2[k]) ~= "table" or not TableEquals(v, t2[k]) then
return false
end
else
if v ~= t2[k] then
return false
end
end
end
return true
end
function TraverseTable(v, k1, k2, k3, k4, k5)
-- Walks the keys directly rather than packing them into a temporary table
if k1 == nil then return v end
if type(v) ~= "table" then return nil, k1 end
v = v[k1]
if k2 == nil then return v end
if type(v) ~= "table" then return nil, k2 end
v = v[k2]
if k3 == nil then return v end
if type(v) ~= "table" then return nil, k3 end
v = v[k3]
if k4 == nil then return v end
if type(v) ~= "table" then return nil, k4 end
v = v[k4]
if k5 == nil then return v end
if type(v) ~= "table" then return nil, k5 end
return v[k5]
end
-- Courtesy of ChatGPT
function SplitString(str, delimiter)
local result = {}
local start_pos = 1
while true do
local end_pos = string.find(str, delimiter, start_pos, true)
if not end_pos then
table.insert(result, string.sub(str, start_pos))
break
end
table.insert(result, string.sub(str, start_pos, end_pos - 1))
start_pos = end_pos + string.len(delimiter)
end
return result
end
function StartsWith(str, starts)
return string.sub(str, 1, string.len(starts)) == starts
end
function RoundNumber(number, decimalPlaces)
decimalPlaces = decimalPlaces or 0
return math.floor(number * 10^decimalPlaces + 0.5) / 10^decimalPlaces
end
-- Courtesy of ChatGPT
function InterpolateColors(colors, t)
local r, g, b = InterpolateColorsNoTable(colors, t)
return {r, g, b}
end
function InterpolateColorsNoTable(colors, t)
local numColors = getn(colors)
-- Ensure t is between 0 and 1
t = math.max(0, math.min(1, t))
-- If there are fewer than 2 colors, just return the single color
if numColors < 2 then
local c = colors[1]
return c[1], c[2], c[3]
end
-- Determine the segment in which t falls
local scaledT = t * (numColors - 1) -- Scale t to cover the range of indices
local index = math.floor(scaledT)
local fraction = scaledT - index
-- Handle edge cases where index is out of bounds
if index >= numColors - 1 then
local c = colors[numColors]
return c[1], c[2], c[3]
end
local color1 = colors[index + 1]
local color2 = colors[index + 2]
-- Linear interpolation between color1 and color2
local r = color1[1] + (color2[1] - color1[1]) * fraction
local g = color1[2] + (color2[2] - color1[2]) * fraction
local b = color1[3] + (color2[3] - color1[3]) * fraction
return r, g, b
end
-- Color code prefixes ("|cFFrrggbb") are cached so colorizing doesn't format a new prefix string every call
local colorPrefixCache = {}
local colorPrefixCacheSize = 0
local MAX_COLOR_PREFIX_CACHE = 1024
local floor = math.floor
local format = string.format
function GetColorPrefix(r, g, b)
local ri, gi, bi = floor(r * 255), floor(g * 255), floor(b * 255)
local key = ri * 65536 + gi * 256 + bi
local prefix = colorPrefixCache[key]
if not prefix then
prefix = format("|cFF%02x%02x%02x", ri, gi, bi)
if colorPrefixCacheSize < MAX_COLOR_PREFIX_CACHE then
colorPrefixCache[key] = prefix
colorPrefixCacheSize = colorPrefixCacheSize + 1
end
end
return prefix
end
local GetColorPrefix = GetColorPrefix
function Colorize(text, r, g, b)
if type(r) == "table" then
local rgb = r
r = rgb[1]
g = rgb[2]
b = rgb[3]
end
return GetColorPrefix(r, g, b) .. text .. "|r"
end
function StripColors(text)
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
text = string.gsub(text, "|r", "")
return text
end
local coloredRoles = {
["Tank"] = Colorize("Tank", 0.3, 0.6, 1),
["Healer"] = Colorize("Healer", 0.2, 1, 0.2),
["Damage"] = Colorize("Damage", 1, 0.4, 0.4),
["No Role"] = "No Role"
}
function GetColoredRoleText(role)
if not role then
return coloredRoles["No Role"]
end
return coloredRoles[role]
end
function IsFeigning(unit)
local cache = PTUnit.Get(unit)
if not cache then
local unitClass = GetClass(unit)
if unitClass == "HUNTER" then
local superwow = IsSuperWowPresent()
for i = 1, 32 do
local texture, _, id = UnitBuff(unit, i)
if superwow then -- Use the ID if SuperWoW is present
if id == 5384 then -- 5384 is Feign Death
return true
end
else -- Use the texture otherwise
if texture == "Interface\\Icons\\Ability_Rogue_FeignDeath" then
return true
end
end
end
end
return false
end
return cache:HasBuffIDOrName(5384, "Feign Death")
end
function HasAura(unit, auraType, auraTexture, auraID)
local auraFunc = auraType == "Buff" and UnitBuff or UnitDebuff
local checkCount = auraType == "Buff" and 32 or 16
local superwow = IsSuperWowPresent()
for i = 1, checkCount do
local texture, _, id = auraFunc(unit, i)
if superwow and auraID then
if auraID == id then
return true
end
else
if texture == auraTexture then
return true
end
end
end
return false
end
function GetBagSlotInfo(bag, slot)
local link = GetContainerItemLink(bag, slot)
if not link then
return
end
local _, _, name = string.find(link, "%[(.*)%]")
local _, count = GetContainerItemInfo(bag, slot)
return name, count
end
-- Returns: Bag index, Slot index
function FindBagSlot(itemName)
local bestBag, bestSlot, lowestStackSize
for bag = 0, NUM_BAG_FRAMES do
for slot = 1, GetContainerNumSlots(bag) do
local name, count = GetBagSlotInfo(bag, slot)
if itemName == name then
if not lowestStackSize or lowestStackSize > count then
bestBag = bag
bestSlot = slot
lowestStackSize = count
end
end
end
end
return bestBag, bestSlot
end
-- Returns true if an item was found and attempted to be used
function UseItem(itemName)
local bag, slot = FindBagSlot(itemName)
if not bag then
return
end
UseContainerItem(bag, slot)
return true
end
function GetItemCount(itemName)
local total = 0
for bag = 0, NUM_BAG_FRAMES do
for slot = 1, GetContainerNumSlots(bag) do
local name, count = GetBagSlotInfo(bag, slot)
if itemName == name then
total = total + count
end
end
end
return total
end
function IsValidMacro(name)
return GetMacroIndexByName(name) ~= 0
end
function RunMacro(name, target)
if not IsValidMacro(name) then
return
end
if target then
_G.PT_MacroTarget = target
end
local _, _, body = GetMacroInfo(GetMacroIndexByName(name))
local commands = SplitString(body, "\n")
for i = 1, getn(commands) do
ChatFrameEditBox:SetText(commands[i])
ChatEdit_SendText(ChatFrameEditBox)
end
if target then
_G.PT_MacroTarget = nil
end
end
local ScanningTooltip = CreateFrame("GameTooltip", "PTScanningTooltip", nil, "GameTooltipTemplate");
ScanningTooltip:SetOwner(WorldFrame, "ANCHOR_NONE");
-- Allow tooltip SetX() methods to dynamically add new lines based on these
ScanningTooltip:AddFontStrings(
ScanningTooltip:CreateFontString( "$parentTextLeft1", nil, "GameTooltipText" ),
ScanningTooltip:CreateFontString( "$parentTextRight1", nil, "GameTooltipText" ) );
local spellRankString = RANK.." "
function ExtractSpellRank(spellname)
-- Find the starting position of "Rank "
local start_pos = string.find(spellname, spellRankString)
-- Check if "Rank " was found
if start_pos then
-- Adjust start_pos to point to the first digit
--start_pos = start_pos + 5 -- Move past "Rank "
-- Find the ending parenthesis
local end_pos = string.find(spellname, ")", start_pos)
-- Extract the number substring
if end_pos then
local number_str = string.sub(spellname, start_pos, end_pos - 1)
--local number = tonumber(number_str) -- Convert to a number
return number_str
end
end
return nil
end
local resourceCostPatterns = {[MANA_COST] = "mana", [RAGE_COST] = "rage", [ENERGY_COST] = "energy", [FOCUS_COST] = "focus"}
function ExtractResourceCost(costText)
for costPattern, resourceName in pairs(resourceCostPatterns) do
local cost = cmatch(costText, costPattern)
if cost then
return tonumber(cost), resourceName
end
end
return 0
end
function GetSpellID(spellname)
local id = 1
local matchingSpells = compost:GetTable()
local spellRank = ExtractSpellRank(spellname)
if spellRank ~= nil then
spellname = string.gsub(spellname, "%b()", "")
end
for i = 1, GetNumSpellTabs() do
local _, _, _, numSpells = GetSpellTabInfo(i)
for j = 1, numSpells do
local spellName, rank, realID = GetSpellName(id, "spell")
if spellName == spellname then
if rank == spellRank then -- If the rank is specified, then we can check if this is the right spell
return id
else
table.insert(matchingSpells, id)
end
end
id = id + 1
end
end
local foundID = matchingSpells[getn(matchingSpells)]
compost:Reclaim(matchingSpells)
return foundID
end
local costCache = {}
local costTypeCache = {}
local costCacheDirty = false
function MarkSpellCostCacheDirty()
costCacheDirty = true
end
-- Returns the numerical cost and the resource name; "unknown" if the spell is unknown; 0 if the spell is free
function GetResourceCost(spellName)
if costCacheDirty then
ClearTable(costCache)
ClearTable(costTypeCache)
costCacheDirty = false
end
if costCache[spellName] then
return costCache[spellName], costTypeCache[spellName]
end
ScanningTooltip:SetOwner(UIParent, "ANCHOR_NONE");
local spellID, bookType
if GetSpellSlotTypeIdForName then -- Nampower 2.6.0 function
spellID, bookType = GetSpellSlotTypeIdForName(spellName)
if bookType == "unknown" then
return "unknown"
end
if bookType ~= "spell" then
return 0
end
else
spellID = GetSpellID(spellName)
end
if not spellID then
return "unknown"
end
ScanningTooltip:SetSpell(spellID, "spell")
local leftText = _G["PTScanningTooltipTextLeft2"]
if leftText:GetText() then
costCache[spellName], costTypeCache[spellName] = ExtractResourceCost(leftText:GetText())
return costCache[spellName], costTypeCache[spellName]
end
costCache[spellName] = 0
return 0
end
if HasModVersion("Nampower", Nampower_v3_0) then
function GetResourceCost(spellName)
local spellID = GetSpellIdForName(spellName)
local cost = GetSpellRecField(spellID, "manaCost")
local powerType = GetSpellRecField(spellID, "powerType")
if powerType == 1 then -- Rage is 10x for some reason
cost = cost / 10
end
return cost or "unknown", PowerTypeMap[powerType]
end
end
-- Returns the aura's name and its school type
function ScanAuraInfo(unit, index, type)
-- Make these texts blank since they don't clear otherwise
local leftText = _G["PTScanningTooltipTextLeft1"]
leftText:SetText("")
local rightText = _G["PTScanningTooltipTextRight1"]
rightText:SetText("")
if type == "Buff" then
ScanningTooltip:SetUnitBuff(unit, index)
else
ScanningTooltip:SetUnitDebuff(unit, index)
end
return leftText:GetText() or "", rightText:GetText() or ""
end
if SuperWoW or Client1181 then
local auraNameCache = {}
local auraTypeCache = {}
function GetAuraInfo(unit, index, type, id)
if not id then
if type == "Buff" then
local _, _, i = UnitBuff(unit, index)
id = i
else
local _, _, _, i = UnitDebuff(unit, index)
id = i
end
if not id then -- Uh oh, the 1.18.1 client lost the ID
return ScanAuraInfo(unit, index, type)
end
end
if not auraNameCache[id] then
auraNameCache[id], auraTypeCache[id] = ScanAuraInfo(unit, index, type)
end
return auraNameCache[id], auraTypeCache[id]
end
function GetCachedAuraInfo(id)
return auraNameCache[id], auraTypeCache[id]
end
else
GetAuraInfo = ScanAuraInfo
end
function GetActionSlotName(slot)
_G["PTScanningTooltipTextLeft1"]:SetText("")
ScanningTooltip:SetAction(slot)
return _G["PTScanningTooltipTextLeft1"]:GetText() or ""
end
local actionCache = {}
function FindAction(name)
if actionCache[name] then
local data = actionCache[name]
if GetActionTexture(data.slot) == data.texture then
return data.slot
end
actionCache[name] = nil
end
for i = 1, 120 do
if GetActionTexture(i) then
local slotName = GetActionSlotName(i)
if slotName == name then
actionCache[name] = {
slot = i,
texture = GetActionTexture(i)
}
return i
end
end
end
end
function IsCurrentActionByName(name)
local slot = FindAction(name)
if slot then
return IsCurrentAction(slot)
end
end
function IsAutoRepeatActionByName(name)
local slot = FindAction(name)
if slot then
return IsAutoRepeatAction(slot)
end
end
-- Casts an action if it's not already being used. Very useful for auto attack abilities. They must be somewhere on your bars.
function CastActionByName(name, target)
if not (IsAutoRepeatActionByName(name) or IsCurrentActionByName(name)) then
CastSpellByName(name, target)
end
end
-- Returns an array of the units in the party number or the unit's raid group
function GetRaidPartyMembers(partyNumberOrUnit)
if not RAID_SUBGROUP_LISTS then
return compost:GetTable()
end
if type(partyNumberOrUnit) == "string" then
partyNumberOrUnit = FindUnitRaidGroup(partyNumberOrUnit)
end
local members = compost:GetTable()
if RAID_SUBGROUP_LISTS[partyNumberOrUnit] then
local n = 0
for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[partyNumberOrUnit]) do
n = n + 1
members[n] = RaidUnits[raidNumber]
end
table.setn(members, n)
end
return members
end
-- Returns the raid unit that this unit is, or nil if the unit is not in the raid
function FindRaidUnit(unit)
if not RAID_SUBGROUP_LISTS then
return nil
end
for party = 1, 8 do
if RAID_SUBGROUP_LISTS[party] then
for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[party]) do
local raidUnit = RaidUnits[raidNumber]
if UnitIsUnit(unit, raidUnit) then
return raidUnit
end
end
end
end
end
-- Returns the raid group number the unit is part of, or nil if the unit is not in the raid
function FindUnitRaidGroup(unit)
for party = 1, 8 do
if RAID_SUBGROUP_LISTS[party] then
for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[party]) do
local raidUnit = RaidUnits[raidNumber]
if UnitIsUnit(unit, raidUnit) then
return party
end
end
end
end
end
-- Requires SuperWoW
function GetSurroundingPartyMembers(player, range)
local units
if UnitInRaid("player") then
units = GetRaidPartyMembers(player)
else
units = CloneTableCompost(PartyUnits)
AppendArrayElements(units, PetUnits)
end
local inRange = GetUnitsInRange(player, units, range or 30)
compost:Reclaim(units)
return inRange
end
function GetSurroundingRaidMembers(player, range, checkPets)
local units
if UnitInRaid("player") then
units = CloneTableCompost(RaidUnits)
if checkPets then
AppendArrayElements(units, RaidPetUnits)
end
else
units = CloneTableCompost(PartyUnits)
if checkPets then
AppendArrayElements(units, PetUnits)
end
end
local inRange = GetUnitsInRange(player, units, range or 30)
compost:Reclaim(units)
return inRange
end
function GetUnitsInRange(center, units, range)
local inRange = compost:GetTable()
for _, unit in ipairs(units) do
local exists, guid = UnitExists(unit)
if exists and UnitIsConnected(unit) and not UnitIsDeadOrGhost(unit) and
GetDistanceBetween(center, unit) <= (range or 30) then
table.insert(inRange, guid)
end
end
return inRange
end
-- Blizzard's UI functions seem to get called referring to a global called "this" referring to the UI object.
-- This function calls a function on the object, emulating the "this" variable.
function CallWithThis(object, func)
local prevThis = _G.this
_G.this = object
func()
_G.this = prevThis
end
local function _FixFrameLevels(parent, ...)
local level = parent:GetFrameLevel() + 1
for i = 1, getn(arg) do
local child = arg[i]
-- Children of scroll frames can block components outside if they're layered above the scroll pane
if parent.GetScrollChild and parent:GetScrollChild() == child then
child:SetFrameLevel(level - 1)
else
child:SetFrameLevel(level)
end
_FixFrameLevels(child, child:GetChildren())
end
end
function FixFrameLevels(frame)
return _FixFrameLevels(frame, frame:GetChildren())
end
-- Modified ChatGPT function
function RotateTexture(texture, degrees, extent)
local angleRadians = math.rad(degrees)
local cos = math.cos(angleRadians)
local sin = math.sin(angleRadians)
extent = (extent or 1) / 2
local x1, y1 = -extent, extent -- UL
local x2, y2 = -extent, -extent -- LL
local x3, y3 = extent, extent -- UR
local x4, y4 = extent, -extent -- LR
local ULx, ULy = x1 * cos - y1 * sin + 0.5, x1 * sin + y1 * cos + 0.5
local LLx, LLy = x2 * cos - y2 * sin + 0.5, x2 * sin + y2 * cos + 0.5
local URx, URy = x3 * cos - y3 * sin + 0.5, x3 * sin + y3 * cos + 0.5
local LRx, LRy = x4 * cos - y4 * sin + 0.5, x4 * sin + y4 * cos + 0.5
texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
end
local PTTaskExecutor = CreateFrame("Frame", "PTTaskExecutor")
local taskQueue = {}
local offTaskQueue = {}
local PTTaskExecutor_OnUpdate = function()
local runningQueue = taskQueue
taskQueue = offTaskQueue
offTaskQueue = runningQueue
for _, task in ipairs(runningQueue) do
local ok, result = pcall(task)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("Puppeteer Task Error: "..result)
end
end
ClearTable(runningQueue)
if getn(taskQueue) == 0 then
PTTaskExecutor:SetScript("OnUpdate", nil)
end
end
function RunLater(func)
table.insert(taskQueue, func)
if PTTaskExecutor:GetScript("OnUpdate") == nil then
PTTaskExecutor:SetScript("OnUpdate", PTTaskExecutor_OnUpdate)
end
end
-- Returns the class without the first return variable fluff
function GetClass(unit)
local _, class = UnitClass(unit)
return class
end
function GetClasses()
return Classes
end
function GetRandomClass()
return Classes[math.random(1, 9)]
end
local healerClassesSet = ToSet(HealerClasses)
function IsHealerClass(unit)
return healerClassesSet[GetClass(unit)] == 1
end
local classColors = {
["DRUID"] = {1.0, 0.49, 0.04},
["HUNTER"] = {0.67, 0.83, 0.45},
["MAGE"] = {0.41, 0.8, 0.94},
["PALADIN"] = {0.96, 0.55, 0.73},
["PRIEST"] = {1.0, 1.0, 1.0},
["ROGUE"] = {1.0, 0.96, 0.41},
["SHAMAN"] = {0.14, 0.35, 1.0},
["WARLOCK"] = {0.58, 0.51, 0.79},
["WARRIOR"] = {0.78, 0.61, 0.43}
}
-- Shamans used the Paladin pink color in vanilla
local vanillaClassColors = {
["SHAMAN"] = {0.96, 0.55, 0.73}
}
local unknownClassColor = {0.7, 0.7, 0.7}
-- Exact palette from the supplied ModernClassColors reference. Keep this local
-- so choosing a health bar texture does not change names or other addons.
local modernClassColors = {
["DRUID"] = {1, 0.49, 0.04},
["HUNTER"] = {0.67, 0.83, 0.45},
["MAGE"] = {0.25, 0.78, 0.92},
["PALADIN"] = {0.96, 0.55, 0.73},
["PRIEST"] = {1, 1, 1},
["ROGUE"] = {1, 0.96, 0.41},
["SHAMAN"] = {0, 0.44, 0.87},
["WARLOCK"] = {0.53, 0.53, 0.93},
["WARRIOR"] = {0.78, 0.61, 0.43}
}
function GetClassColor(class, asArray, modern)
local color
if modern then
color = modernClassColors[class]
elseif PTOptions and PTOptions.VanillaClassColors and vanillaClassColors[class] then
color = vanillaClassColors[class]
else
color = classColors[class]
end
if not color then -- Unknown class
color = unknownClassColor
end
if asArray then
return color
end
return color[1], color[2], color[3]
end
-- Returns an array of spells starting with the string, ordered from highest rank to lowest.
-- Limit is 20 if nil, non-ranks not included. Adds non-ranked name unless specified otherwise.
function SearchSpells(startStr, limit, noNonRank)
startStr = string.upper(startStr)
limit = limit or 20
local matchingSpells = {}
local id = 1
for i = 1, GetNumSpellTabs() do
local breakOut
local tabName, _, _, numSpells = GetSpellTabInfo(i)
if tabName == "ZMounts" or tabName == "ZzCompanions" then -- No 1.18.1 client "spell" tabs
id = id + numSpells
else
for j = 1, numSpells do
local spellName, rank, realID = GetSpellName(id, "spell")
if not IsSpellPassive(id, "spell") then
local fullName = spellName
if rank ~= "" then
fullName = fullName.."("..rank..")"
end
if StartsWith(string.upper(fullName), startStr) then
table.insert(matchingSpells, fullName)
if getn(matchingSpells) >= limit then
breakOut = true
break
end
end
end
id = id + 1
end
end
if breakOut then
break
end
end
ReverseArray(matchingSpells)
if not noNonRank then
local alreadyFound = compost:GetTable()
local toInsert = compost:GetTable()
for i = 1, getn(matchingSpells) do
local rank = ExtractSpellRank(matchingSpells[i])
-- Don't add non rank if the user is explicitly typing out the rank already
if (string.len(matchingSpells[i]) - string.len(rank or "") - 2) < (string.len(startStr)) then
break
end
if rank then
local baseSpell = string.sub(matchingSpells[i], 1, string.len(matchingSpells[i]) - string.len(rank) - 2)
if not alreadyFound[baseSpell] then
alreadyFound[baseSpell] = true
table.insert(toInsert, compost:Acquire(i, baseSpell))
end
end
end
local offset = 0
for _, insertion in ipairs(toInsert) do
table.insert(matchingSpells, insertion[1] + offset, insertion[2])
offset = offset + 1
end
compost:Reclaim(alreadyFound)
compost:Reclaim(toInsert, 1)
end
return matchingSpells
end
function SearchMacros(startStr, limit)
startStr = string.upper(startStr)
limit = limit or 20
local matchingMacros = {}
for i = 1, GetNumMacros() do
local name = GetMacroInfo(i)
if StartsWith(string.upper(name), startStr) then
table.insert(matchingMacros, name)
if getn(matchingMacros) >= limit then
break
end
end
end
return matchingMacros
end
function SearchItems(startStr, limit)
startStr = string.upper(startStr)
limit = limit or 20
local alreadyFound = compost:GetTable()
local matchingItems = {}
for bag = 0, NUM_BAG_FRAMES do
for slot = 1, GetContainerNumSlots(bag) do
local name = GetBagSlotInfo(bag, slot)
if name and not alreadyFound[name] and StartsWith(string.upper(name), startStr) then
table.insert(matchingItems, name)
alreadyFound[name] = true
if getn(matchingItems) >= limit then
break
end
end
end
end
compost:Reclaim(alreadyFound)
return matchingItems
end
-- Checks for feign death as well
function IsDeadFriend(unit)
return (UnitIsDead(unit) or UnitIsCorpse(unit)) and UnitIsFriend("player", unit) and not IsFeigning(unit)
end
local keyModifiers = {"None", "Shift", "Control", "Alt", "Shift+Control", "Shift+Alt", "Control+Alt", "Shift+Control+Alt"}
function GetKeyModifiers()
return keyModifiers
end
-- L1: Shift
-- L2: Control
-- L3: Alt
KeyModifierMap = {}
do
-- That moment when trying to be more concise goes terribly wrong
local keys = {{"Shift", "S", {0.4, 1, 0.4}}, {"Control", "C", {0.4, 0.4, 1}}, {"Alt", "A", {1, 0.4, 0.4}}}
local states = {true, false}
for _, l1State in ipairs(states) do
local l1 = {}
KeyModifierMap[l1State] = l1
for _, l2State in ipairs(states) do
local l2 = {}
l1[l2State] = l2
for _, l3State in ipairs(states) do
local keyStr = ""
local keyStrColored = ""
local keyAbbStr = ""
local keyAbbStrColored = ""
for i = 1, 3 do
if (i == 1 and l1State) or (i == 2 and l2State) or (i == 3 and l3State) then
local key = keys[i]
if keyStr ~= "" then
keyStr = keyStr.."+"
keyStrColored = keyStrColored.."+"
keyAbbStr = keyAbbStr.."+"
keyAbbStrColored = keyAbbStrColored.."+"
end
keyStr = keyStr..key[1]
keyStrColored = keyStrColored..Colorize(key[1], key[3])
keyAbbStr = keyAbbStr..key[2]
keyAbbStrColored = keyAbbStrColored..Colorize(key[2], key[3])
end
end
if keyStr == "" then
keyStr = "None"
keyStrColored = "None"
keyAbbStr = "None"
keyAbbStrColored = "None"
end
l2[l3State] = {keyStr, keyStrColored, keyAbbStr, keyAbbStrColored}
end
end
end
end
function GetKeyModifier()
return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][1]
end
function GetColoredKeyModifier()
return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][2]
end
function GetAbbreviatedKeyModifier()
return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][3]
end
function GetColoredAbbreviatedKeyModifier()
return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][4]
end
function GetKeyModifierTypeByID(id)
return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][id]
end
local nameOverrides = {
["LeftButton"] = "Left Button",
["RightButton"] = "Right Button",
["MiddleButton"] = "Middle Button"
}
function GetButtonName(rawButton)
return nameOverrides[rawButton] or GetBindingText(rawButton, "KEY_")
end
local buttons = {"LeftButton", "MiddleButton", "RightButton", "Button4", "Button5"}
function GetAllButtons()
return buttons
end
local buttonsSet = ToSet(buttons, true)
function GetAllButtonsSet()
return buttonsSet
end
local upButtons = {}
for _, button in ipairs(buttons) do
table.insert(upButtons, button.."Up")
end
function GetUpButtons()
return upButtons
end
local downButtons = {}
for _, button in ipairs(buttons) do
table.insert(downButtons, button.."Down")
end
function GetDownButtons()
return downButtons
end
function GetCenterScreenPoint(componentWidth, componentHeight)
return "TOPLEFT", (GetScreenWidth() / 2) - (componentWidth / 2), -((GetScreenHeight() / 2) - (componentHeight / 2))
end
-- Keeps the frame at the current position, while modifying the anchor point
function ConvertAnchor(frame, anchor)
local leftX, rightX, topY, bottomY = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
local centerX, centerY = frame:GetCenter()
local x, y
if anchor == "TOPLEFT" then
x, y = leftX, topY
elseif anchor == "TOPRIGHT" then
x, y = rightX, topY
elseif anchor == "BOTTOMLEFT" then
x, y = leftX, bottomY
elseif anchor == "BOTTOMRIGHT" then
x, y = rightX, bottomY
elseif anchor == "TOP" then
x, y = centerX, topY
elseif anchor == "BOTTOM" then
x, y = centerX, bottomY
elseif anchor == "LEFT" then
x, y = leftX, centerY
elseif anchor == "RIGHT" then
x, y = rightX, centerY
elseif anchor == "CENTER" then
x, y = centerX, centerY
end
frame:ClearAllPoints()
frame:SetPoint(anchor, UIParent, "TOPLEFT", x, y - GetScreenHeight())
end
function GetPowerType(unit)
return PowerTypeMap[UnitPowerType(unit)]
end
function GetPowerColor(unit)
return PowerColors[GetPowerType(unit)]
end
-- You never really know these days
function IsReallyInInstance()
return IsInInstance() and not InstanceWorldZones[GetRealZoneText()]
end
-- Returns distance if UnitXP SP3 or SuperWoW is present;
-- 0 if unit is offline, or unit is enemy and SuperWoW is the distance provider;
-- 9999 if unit is not visible or UnitXP SP3 is not present.
-- Might try to do hacky stuff for people without mods later on.
function GetDistanceTo(unit)
return GetDistanceBetween("player", unit)
end
function GetDistanceBetween_SuperWow(unit1, unit2)
if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then
return 0
end
if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then
return 9999
end
local x1, z1, y1 = UnitPosition(unit1)
local x2, z2, y2 = UnitPosition(unit2)
if not x1 or not x2 then
return 0
end
local dx = x2 - x1
local dz = z2 - z1
local dy = y2 - y1
return math.sqrt(dx*dx + dz*dz + dy*dy)
end
function GetDistanceBetween_UnitXPSP3_Legacy(unit1, unit2)
if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then
return 0
end
if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then
return 9999
end
return math.max((UnitXP("distanceBetween", unit1, unit2) or (9999 + 3)) - 3, 0) -- UnitXP SP3 modded function
end
function GetDistanceBetween_UnitXPSP3(unit1, unit2)
if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then
return 0
end
if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then
return 9999
end
return math.max(UnitXP("distanceBetween", unit1, unit2) or 9999, 0) -- UnitXP SP3 modded function
end
function GetDistanceBetween_Vanilla(unit1, unit2)
if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then
return 0
end
if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then
return 9999
end
if unit1 == "player" then
if CheckInteractDistance(unit2, 3) then
return 9
end
if CheckInteractDistance(unit2, 4) then
return 27
end
end
return 28
end
if UnitXPSP3 then
if UnitXPSP3_Version > -1 then -- Newer versions have more accurate distances
GetDistanceBetween = GetDistanceBetween_UnitXPSP3
else -- Fall back to old distance calculation
GetDistanceBetween = GetDistanceBetween_UnitXPSP3_Legacy
end
elseif SuperWoW then
GetDistanceBetween = GetDistanceBetween_SuperWow
else -- sad
GetDistanceBetween = GetDistanceBetween_Vanilla
end
-- ClassicAPI: center-to-center distance from the player. Used only when neither UnitXP SP3 nor SuperWoW
-- provides distance (see ApplyClassicAPIOptions).
function GetDistanceBetween_ClassicAPI(unit1, unit2)
if unit1 ~= "player" then
return GetDistanceBetween_Vanilla(unit1, unit2)
end
if not UnitIsConnected(unit2) then
return 0
end
if not UnitIsVisible(unit2) then
return 9999
end
local distanceSquared, checked = UnitDistanceSquared(unit2)
if not checked then
return 9999
end
return math.sqrt(distanceSquared)
end
local classicDistance = false
-- SuperWoW cannot provide precise distance for enemies
function CanClientGetPreciseDistance(alsoEnemies)
return UnitXPSP3 or (SuperWoW and not alsoEnemies) or classicDistance
end
function GetUnitPosition(unit)
return nil
end
if VanillaUtils then
GetUnitPosition = function(unit)
return UnitXP("unitPosition", unit)
end
elseif SuperWoW then
GetUnitPosition = UnitPosition
end
-- Returns whether unit is in sight if UnitXP SP3 is present, otherwise always true.
IsInSight = function()
return true
end
do -- This is done to prevent crashes from checking sight too early
local sightEnableFrame = CreateFrame("Frame")
sightEnableFrame:RegisterEvent("ADDON_LOADED")
sightEnableFrame:SetScript("OnEvent", function()
if arg1 == "Puppeteer" then
if UnitXPSP3 then
IsInSight = function(unit)
return UnitXP("inSight", "player", unit) -- UnitXP SP3 modded function
end
end
sightEnableFrame:SetScript("OnEvent", nil)
end
end)
end
local classicSight = false
function CanClientSightCheck()
return UnitXPSP3 or classicSight
end
-- ClassicAPI features that are both available and turned on. Filled by ApplyClassicAPIOptions.
ClassicAPIActive = {}
-- Called once saved options are loaded, before any unit cache is created.
-- The "Use ClassicAPI" option needs a reload to change, since frames and caches are built around it.
function ApplyClassicAPIOptions()
ClassicAPIEnabled = ClassicAPI and PTOptions and PTOptions.UseClassicAPI and true or false
local active = ClassicAPIActive
for feature, available in pairs(ClassicAPIFeatures) do
active[feature] = ClassicAPIEnabled and available and true or nil
end
-- The fill-a-table form of GetAuraSlots writes the slot count to t.n; older versions return slot ids instead
if active.AuraSlots then
local probe = {}
local ok = pcall(C_UnitAuras.GetAuraSlots, "player", "HELPFUL", nil, nil, probe)
if not ok or probe.n == nil then
active.AuraSlots = nil
end
end
-- Aura deltas are only used together with the slot scan, which they refresh in place
if not active.AuraSlots then
active.AuraDelta = nil
end
if active.Distance and not UnitXPSP3 and not SuperWoW then
GetDistanceBetween = GetDistanceBetween_ClassicAPI
classicDistance = true
end
if active.Sight and not UnitXPSP3 then
IsInSight = function(unit)
return UnitInLineOfSight(unit) ~= false
end
classicSight = true
end
end
function IsClassicAPIActive(feature)
return ClassicAPIActive[feature] ~= nil
end
-- Whether aura icons can show timers: SuperWoW/Nampower tracking, or ClassicAPI aura durations
function HasAuraTimers()
return SuperWoW or ClassicAPIActive.AuraSlots ~= nil
end
-- From pfQuest
local minimaparrow = ({Minimap:GetChildren()})[9]
for k, v in pairs({Minimap:GetChildren()}) do
if v:IsObjectType("Model") and not v:GetName() then
if string.find(strlower(v:GetModel()), "interface\\minimap\\minimaparrow") then
minimaparrow = v
break
end
end
end
function GetPlayerFacing()
return minimaparrow:GetFacing()
end
function GetUnitDirection(from, to)
local sx, sz = GetUnitPosition(from)
local tx, tz = GetUnitPosition(to)
local dx = tx - sx
local dz = tz - sz
return math.atan2(dz, dx)
end
function GetFacingAngle(unit)
local angle = GetUnitDirection("player", unit) - GetPlayerFacing()
angle = modulo(angle, math.pi * 2)
return angle
end
if VanillaUtils then
function GetCameraFacing()
local sx, sy = GetUnitPosition("player")
local cx, cy = UnitXP("cameraPosition")
return math.atan2(cy - sy, cx - sx) + math.pi
end
else
GetCameraFacing = GetPlayerFacing -- Sad
end
function GetCameraFacingAngle(unit)
local cameraAngle = GetUnitDirection("player", unit) - GetCameraFacing()
cameraAngle = modulo(cameraAngle, math.pi * 2)
return cameraAngle
end
function CanClientGetAuraIDs()
return SuperWoW-- or Client1181 -- 1.18.1 client ID fetching is not reliable
end
function IsClient1181()
return Client1181
end
AllUnitsSet = ToSet(AllUnits)
AllRealUnitsSet = ToSet(AllRealUnits)
FocusUnitsSet = ToSet(FocusUnits)
RunLater(function()
PTLocale.Values(ResurrectionSpells)
PTLocale.Keys(ResurrectionSpellsSet)
end)
InstanceWorldZones = ToSet({"Winter Veil Vale"}) -- Why is this an instance??