diff --git a/.pkgmeta b/.pkgmeta new file mode 100644 index 0000000..91d1d6d --- /dev/null +++ b/.pkgmeta @@ -0,0 +1,10 @@ +package-as: LibRangeCheck-3.0 +enable-nolib-creation: no + +externals: + LibStub-1.0: + url: https://repos.wowace.com/wow/libstub/trunk + tag: latest + CallbackHandler-1.0: + url: https://repos.wowace.com/wow/callbackhandler/trunk/CallbackHandler-1.0 + tag: latest diff --git a/LICENSE b/LICENSE index 3a7b91f..5cab793 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 WeakAuras +Copyright (c) 2023 The WoWUIDev Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LibRangeCheck-3.0.toc b/LibRangeCheck-3.0.toc new file mode 100644 index 0000000..93ec379 --- /dev/null +++ b/LibRangeCheck-3.0.toc @@ -0,0 +1,13 @@ +## Interface: 100105 +## Title: Lib: RangeCheck-3.0 +## Notes: A library to determine estimated range. +## Author: The WoWUIDev Community +## Version: @project-version@ +## LoadOnDemand: 1 +## X-Category: Library +## X-Credits: mitch0 +## X-License: MIT + +LibStub-1.0\LibStub.lua +CallbackHandler-1.0\CallbackHandler-1.0.xml +LibRangeCheck-2.0\LibRangeCheck-2.0.lua diff --git a/LibRangeCheck-3.0/LibRangeCheck-3.0.lua b/LibRangeCheck-3.0/LibRangeCheck-3.0.lua new file mode 100644 index 0000000..db0d7b8 --- /dev/null +++ b/LibRangeCheck-3.0/LibRangeCheck-3.0.lua @@ -0,0 +1,1597 @@ +--[[ +Name: LibRangeCheck-3.0 +Author(s): mitch0, WoWUI Dev community +Website: http://www.wowace.com/projects/librangecheck-2-0/ +Description: A range checking library based on interact distances and spell ranges +Dependencies: LibStub +License: MIT +]] + +--- LibRangeCheck-3.0 provides an easy way to check for ranges and get suitable range checking functions for specific ranges.\\ +-- The checkers use spell and item range checks, or interact based checks for special units where those two cannot be used.\\ +-- The lib handles the refreshing of checker lists in case talents / spells change and in some special cases when equipment changes (for example some of the mage pvp gloves change the range of the Fire Blast spell), and also handles the caching of items used for item-based range checks.\\ +-- A callback is provided for those interested in checker changes. +-- @usage +-- local rc = LibStub("LibRangeCheck-3.0") +-- +-- rc.RegisterCallback(self, rc.CHECKERS_CHANGED, function() print("need to refresh my stored checkers") end) +-- +-- local minRange, maxRange = rc:GetRange('target') +-- if not minRange then +-- print("cannot get range estimate for target") +-- elseif not maxRange then +-- print("target is over " .. minRange .. " yards") +-- else +-- print("target is between " .. minRange .. " and " .. maxRange .. " yards") +-- end +-- +-- local meleeChecker = rc:GetFriendMaxChecker(rc.MeleeRange) or rc:GetFriendMinChecker(rc.MeleeRange) -- use the closest checker (MinChecker) if no valid Melee checker is found +-- for i = 1, 4 do +-- -- TODO: check if unit is valid, etc +-- if meleeChecker("party" .. i) then +-- print("Party member " .. i .. " is in Melee range") +-- end +-- end +-- +-- local safeDistanceChecker = rc:GetHarmMinChecker(30) +-- -- negate the result of the checker! +-- local isSafelyAway = not safeDistanceChecker('target') +-- +-- @class file +-- @name LibRangeCheck-3.0 +local MAJOR_VERSION = "LibRangeCheck-3.0" +local MINOR_VERSION = 1 + +local lib, oldminor = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION) +if not lib then return end + +local isRetail = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE +local isWrath = WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC + +-- GLOBALS: LibStub, CreateFrame, C_Map, FriendColor (??), HarmColor (??) +local _G = _G +local next = next +local sort = sort +local type = type +local wipe = wipe +local print = print +local pairs = pairs +local ipairs = ipairs +local tinsert = tinsert +local tremove = tremove +local tostring = tostring +local setmetatable = setmetatable +local BOOKTYPE_SPELL = BOOKTYPE_SPELL +local GetSpellInfo = GetSpellInfo +local GetSpellBookItemName = GetSpellBookItemName +local GetNumSpellTabs = GetNumSpellTabs +local GetSpellTabInfo = GetSpellTabInfo +local GetItemInfo = GetItemInfo +local UnitCanAttack = UnitCanAttack +local UnitCanAssist = UnitCanAssist +local UnitExists = UnitExists +local UnitIsUnit = UnitIsUnit +local UnitGUID = UnitGUID +local UnitIsDeadOrGhost = UnitIsDeadOrGhost +local CheckInteractDistance = CheckInteractDistance +local IsSpellInRange = IsSpellInRange +local IsItemInRange = IsItemInRange +local UnitClass = UnitClass +local UnitRace = UnitRace +local GetInventoryItemLink = GetInventoryItemLink +local GetTime = GetTime +local HandSlotId = GetInventorySlotInfo("HandsSlot") +local math_floor = math.floor +local UnitIsVisible = UnitIsVisible + +-- << STATIC CONFIG + +local UpdateDelay = .5 +local ItemRequestTimeout = 10.0 + +-- interact distance based checks. ranges are based on my own measurements (thanks for all the folks who helped me with this) +local DefaultInteractList = { +-- [1] = 28, -- Compare Achievements +-- [2] = 9, -- Trade + [3] = 8, -- Duel + [4] = 28, -- Follow +-- [5] = 7, -- unknown +} + +-- interact list overrides for races +local InteractLists = { + Tauren = { + -- [2] = 7, + [3] = 6, + [4] = 25, + }, + Scourge = { + -- [2] = 8, + [3] = 7, + [4] = 27, + }, +} + +local MeleeRange = 2 +local FriendSpells, HarmSpells, ResSpells, PetSpells = {}, {}, {}, {} + +for _, n in ipairs({ 'EVOKER', 'DEATHKNIGHT', 'DEMONHUNTER', 'DRUID', 'HUNTER', 'SHAMAN', 'MAGE', 'PALADIN', 'PRIEST', 'WARLOCK', 'WARRIOR', 'MONK', 'ROGUE' }) do + FriendSpells[n], HarmSpells[n], ResSpells[n], PetSpells[n] = {}, {}, {}, {} +end + +-- Evoker +tinsert(HarmSpells.EVOKER, 369819) -- Disintegrate (25 yards) + +tinsert(FriendSpells.EVOKER, 361469) -- Living Flame (25 yards) +tinsert(FriendSpells.EVOKER, 360823) -- Naturalize (Preservation) (30 yards) + +tinsert(ResSpells.EVOKER, 361227) -- Return (40 yards) + +-- Death Knights +tinsert(HarmSpells.DEATHKNIGHT, 49576) -- Death Grip (30 yards) +tinsert(HarmSpells.DEATHKNIGHT, 47541) -- Death Coil (Unholy) (40 yards) + +tinsert(ResSpells.DEATHKNIGHT, 61999) -- Raise Ally (40 yards) + +-- Demon Hunters +tinsert(HarmSpells.DEMONHUNTER, 185123) -- Throw Glaive (Havoc) (30 yards) +tinsert(HarmSpells.DEMONHUNTER, 183752) -- Consume Magic (20 yards) +tinsert(HarmSpells.DEMONHUNTER, 204021) -- Fiery Brand (Vengeance) (30 yards) + +-- Druids +tinsert(FriendSpells.DRUID, 8936) -- Regrowth (40 yards, level 3) +tinsert(FriendSpells.DRUID, 774) -- Rejuvenation (Restoration) (40 yards, level 10) +tinsert(FriendSpells.DRUID, 2782) -- Remove Corruption (Restoration) (40 yards, level 19) +tinsert(FriendSpells.DRUID, 88423) -- Natures Cure (Restoration) (40 yards, level 19) + +if not isRetail then + tinsert(FriendSpells.DRUID, 5185) -- Healing Touch (40 yards, level 1, rank 1) +end + +tinsert(HarmSpells.DRUID, 5176) -- Wrath (40 yards) +tinsert(HarmSpells.DRUID, 339) -- Entangling Roots (35 yards) +tinsert(HarmSpells.DRUID, 6795) -- Growl (30 yards) +tinsert(HarmSpells.DRUID, 33786) -- Cyclone (20 yards) +tinsert(HarmSpells.DRUID, 22568) -- Ferocious Bite (Melee Range) +tinsert(HarmSpells.DRUID, 8921) -- Moonfire (40 yards, level 2) + +tinsert(ResSpells.DRUID, 50769) -- Revive (40 yards, level 14) +tinsert(ResSpells.DRUID, 20484) -- Rebirth (40 yards, level 29) + +-- Hunters +tinsert(HarmSpells.HUNTER, 75) -- Auto Shot (40 yards) + +if not isRetail then + tinsert(HarmSpells.HUNTER, 2764) -- Throw (30 yards, level 1) +end + +tinsert(PetSpells.HUNTER, 136) -- Mend Pet (45 yards) + +-- Mages +tinsert(FriendSpells.MAGE, 1459) -- Arcane Intellect (40 yards, level 8) +tinsert(FriendSpells.MAGE, 475) -- Remove Curse (40 yards, level 28) + +if not isRetail then + tinsert(FriendSpells.MAGE, 130) -- Slow Fall (40 yards, level 12) +end + +tinsert(HarmSpells.MAGE, 44614) -- Flurry (40 yards) +tinsert(HarmSpells.MAGE, 5019) -- Shoot (30 yards) +tinsert(HarmSpells.MAGE, 118) -- Polymorph (30 yards) +tinsert(HarmSpells.MAGE, 116) -- Frostbolt (40 yards) +tinsert(HarmSpells.MAGE, 133) -- Fireball (40 yards) +tinsert(HarmSpells.MAGE, 44425) -- Arcane Barrage (40 yards) + +-- Monks +tinsert(FriendSpells.MONK, 115450) -- Detox (40 yards) +tinsert(FriendSpells.MONK, 115546) -- Provoke (30 yards) +tinsert(FriendSpells.MONK, 116670) -- Vivify (40 yards) + +tinsert(HarmSpells.MONK, 115546) -- Provoke (30 yards) +tinsert(HarmSpells.MONK, 115078) -- Paralysis (20 yards) +tinsert(HarmSpells.MONK, 100780) -- Tiger Palm (Melee Range) +tinsert(HarmSpells.MONK, 117952) -- Crackling Jade Lightning (40 yards) + +tinsert(ResSpells.MONK, 115178) -- Resuscitate (40 yards, level 13) + +-- Paladins +tinsert(FriendSpells.PALADIN, 19750) -- Flash of Light (40 yards, level 4) +tinsert(FriendSpells.PALADIN, 85673) -- Word of Glory (40 yards, level 7) +tinsert(FriendSpells.PALADIN, 4987) -- Cleanse (Holy) (40 yards, level 12) +tinsert(FriendSpells.PALADIN, 213644) -- Cleanse Toxins (Protection, Retribution) (40 yards, level 12) + +if not isRetail then + tinsert(FriendSpells.PALADIN, 635) -- Holy Light (40 yards, level 1, rank 1) +end + +tinsert(HarmSpells.PALADIN, 853) -- Hammer of Justice (10 yards) +tinsert(HarmSpells.PALADIN, 35395) -- Crusader Strike (Melee Range) +tinsert(HarmSpells.PALADIN, 62124) -- Hand of Reckoning (30 yards) +tinsert(HarmSpells.PALADIN, 183218) -- Hand of Hindrance (30 yards) +tinsert(HarmSpells.PALADIN, 20271) -- Judgement (30 yards) +tinsert(HarmSpells.PALADIN, 20473) -- Holy Shock (40 yards) + +tinsert(ResSpells.PALADIN, 7328) -- Redemption (40 yards) + +-- Priests +if isRetail then + tinsert(FriendSpells.PRIEST, 21562) -- Power Word: Fortitude (40 yards, level 6) [use first to fix kyrian boon/fae soulshape] + tinsert(FriendSpells.PRIEST, 17) -- Power Word: Shield (40 yards, level 4) +else -- PWS is group only in classic, use lesser heal as main spell check + tinsert(FriendSpells.PRIEST, 2050) -- Lesser Heal (40 yards, level 1, rank 1) +end + +tinsert(FriendSpells.PRIEST, 527) -- Purify / Dispel Magic (40 yards retail, 30 yards tbc, level 18, rank 1) +tinsert(FriendSpells.PRIEST, 2061) -- Flash Heal (40 yards, level 3 retail, level 20 tbc) + +tinsert(HarmSpells.PRIEST, 589) -- Shadow Word: Pain (40 yards) +tinsert(HarmSpells.PRIEST, 585) -- Smite (40 yards) +tinsert(HarmSpells.PRIEST, 5019) -- Shoot (30 yards) + +if not isRetail then + tinsert(HarmSpells.PRIEST, 8092) -- Mindblast (30 yards, level 10) +end + +tinsert(ResSpells.PRIEST, 2006) -- Resurrection (40 yards, level 10) + +-- Rogues +if isRetail then + tinsert(FriendSpells.ROGUE, 36554) -- Shadowstep (Assassination, Subtlety) (25 yards, level 18) -- works on friendly in retail + tinsert(FriendSpells.ROGUE, 921) -- Pick Pocket (10 yards, level 24) -- this works for range, keep it in friendly aswell for retail but on classic this is melee range and will return min 0 range 0 +end + +tinsert(HarmSpells.ROGUE, 2764) -- Throw (30 yards) +tinsert(HarmSpells.ROGUE, 36554) -- Shadowstep (Assassination, Subtlety) (25 yards, level 18) +tinsert(HarmSpells.ROGUE, 185763) -- Pistol Shot (Outlaw) (20 yards) +tinsert(HarmSpells.ROGUE, 2094) -- Blind (15 yards) +tinsert(HarmSpells.ROGUE, 921) -- Pick Pocket (10 yards, level 24) + +-- Shamans +tinsert(FriendSpells.SHAMAN, 546) -- Water Walking (30 yards) +tinsert(FriendSpells.SHAMAN, 8004) -- Healing Surge (Resto, Elemental) (40 yards) +tinsert(FriendSpells.SHAMAN, 188070) -- Healing Surge (Enhancement) (40 yards) + +if not isRetail then + tinsert(FriendSpells.SHAMAN, 331) -- Healing Wave (40 yards, level 1, rank 1) + tinsert(FriendSpells.SHAMAN, 526) -- Cure Poison (40 yards, level 16) + tinsert(FriendSpells.SHAMAN, 2870) -- Cure Disease (40 yards, level 22) +end + +tinsert(HarmSpells.SHAMAN, 370) -- Purge (30 yards) +tinsert(HarmSpells.SHAMAN, 188196) -- Lightning Bolt (40 yards) +tinsert(HarmSpells.SHAMAN, 73899) -- Primal Strike (Melee Range) + +if not isRetail then + tinsert(HarmSpells.SHAMAN, 403) -- Lightning Bolt (30 yards, level 1, rank 1) + tinsert(HarmSpells.SHAMAN, 8042) -- Earth Shock (20 yards, level 4, rank 1) +end + +tinsert(ResSpells.SHAMAN, 2008) -- Ancestral Spirit (40 yards, level 13) + +-- Warriors +tinsert(HarmSpells.WARRIOR, 355) -- Taunt (30 yards) +tinsert(HarmSpells.WARRIOR, 5246) -- Intimidating Shout (Arms, Fury) (8 yards) +tinsert(HarmSpells.WARRIOR, 100) -- Charge (Arms, Fury) (8-25 yards) + +if not isRetail then + tinsert(HarmSpells.WARRIOR, 2764) -- Throw (30 yards, level 1, 5-30 range) +end + +-- Warlocks +tinsert(FriendSpells.WARLOCK, 5697) -- Unending Breath (30 yards) +tinsert(FriendSpells.WARLOCK, 20707) -- Soulstone (40 yards) ~ this can be precasted so leave it in friendly aswell as res + +if isRetail then + tinsert(FriendSpells.WARLOCK, 132) -- Detect Invisibility (30 yards, level 26) +end + +tinsert(HarmSpells.WARLOCK, 5019) -- Shoot (30 yards) +tinsert(HarmSpells.WARLOCK, 234153) -- Drain Life (40 yards, level 9) +tinsert(HarmSpells.WARLOCK, 198590) -- Drain Soul (40 yards, level 15) +tinsert(HarmSpells.WARLOCK, 686) -- Shadow Bolt (Demonology, Affliction) (40 yards) +tinsert(HarmSpells.WARLOCK, 232670) -- Shadow Bolt (40 yards) +tinsert(HarmSpells.WARLOCK, 5782) -- Fear (30 yards) + +if not isRetail then + tinsert(HarmSpells.WARLOCK, 172) -- Corruption (30 yards, level 4, rank 1) + tinsert(HarmSpells.WARLOCK, 348) -- Immolate (30 yards, level 1, rank 1) + tinsert(HarmSpells.WARLOCK, 17877) -- Shadowburn (Destruction) (20 yards) + tinsert(HarmSpells.WARLOCK, 18223) -- Curse of Exhaustion (Affliction) (30/33/36/35/38/42 yards) +end + +tinsert(ResSpells.WARLOCK, 20707) -- Soulstone (40 yards) + +tinsert(PetSpells.WARLOCK, 755) -- Health Funnel (45 yards) + +-- Items [Special thanks to Maldivia for the nice list] + +local FriendItems = { + [2] = { + 37727, -- Ruby Acorn + }, + [3] = { + 42732, -- Everfrost Razor + }, + [5] = { + 8149, -- Voodoo Charm + 136605, -- Solendra's Compassion + 63427, -- Worgsaw + }, + [8] = { + 34368, -- Attuned Crystal Cores + 33278, -- Burning Torch + }, + [10] = { + 32321, -- Sparrowhawk Net + 17626, -- Frostwolf Muzzle + }, + [15] = { + 1251, -- Linen Bandage + 2581, -- Heavy Linen Bandage + 3530, -- Wool Bandage + 3531, -- Heavy Wool Bandage + 6450, -- Silk Bandage + 6451, -- Heavy Silk Bandage + 8544, -- Mageweave Bandage + 8545, -- Heavy Mageweave Bandage + 14529, -- Runecloth Bandage + 14530, -- Heavy Runecloth Bandage + 21990, -- Netherweave Bandage + 21991, -- Heavy Netherweave Bandage + 34721, -- Frostweave Bandage + 34722, -- Heavy Frostweave Bandage + --38643, -- Thick Frostweave Bandage (uncomment for Wotlk) + --38640, -- Dense Frostweave Bandage (uncomment for Wotlk) + }, + [20] = { + 21519, -- Mistletoe + }, + [25] = { + 31463, -- Zezzak's Shard + 13289, -- Egan's Blaster + }, + [30] = { + 1180, -- Scroll of Stamina + 1478, -- Scroll of Protection II + 3012, -- Scroll of Agility + 1712, -- Scroll of Spirit II + 2290, -- Scroll of Intellect II + 1711, -- Scroll of Stamina II + 34191, -- Handful of Snowflakes + }, + [35] = { + 18904, -- Zorbin's Ultra-Shrinker + }, + [40] = { + 34471, -- Vial of the Sunwell + }, + [45] = { + 32698, -- Wrangling Rope + }, + [60] = { + 32825, -- Soul Cannon + 37887, -- Seeds of Nature's Wrath + }, + [70] = { + 41265, -- Eyesore Blaster + }, + [80] = { + 35278, -- Reinforced Net + }, + [100] = { + 41058, -- Hyldnir Harpoon + }, + [150] = { + 46954, -- Flaming Spears + }, +} + +if isRetail then + FriendItems[1] = { + 90175, -- Gin-Ji Knife Set -- doesn't seem to work for pets (always returns nil) + } + FriendItems[4] = { + 129055, -- Shoe Shine Kit + } + FriendItems[7] = { + 61323, -- Ruby Seeds + } + FriendItems[38] = { + 140786, -- Ley Spider Eggs + } + FriendItems[55] = { + 74637, -- Kiryn's Poison Vial + } + FriendItems[50] = { + 116139, -- Haunting Memento + } + FriendItems[90] = { + 133925, -- Fel Lash + } + FriendItems[200] = { + 75208, -- Rancher's Lariat + } +end + +local HarmItems = { + [1] = { + }, + [2] = { + 37727, -- Ruby Acorn + }, + [3] = { + 42732, -- Everfrost Razor + }, + [5] = { + 8149, -- Voodoo Charm + 136605, -- Solendra's Compassion + 63427, -- Worgsaw + }, + [8] = { + 34368, -- Attuned Crystal Cores + 33278, -- Burning Torch + }, + [10] = { + 32321, -- Sparrowhawk Net + 17626, -- Frostwolf Muzzle + }, + [15] = { + 33069, -- Sturdy Rope + }, + [20] = { + 10645, -- Gnomish Death Ray + }, + [25] = { + 24268, -- Netherweave Net + 41509, -- Frostweave Net + 31463, -- Zezzak's Shard + 13289, -- Egan's Blaster + }, + [30] = { + 835, -- Large Rope Net + 7734, -- Six Demon Bag + 34191, -- Handful of Snowflakes + }, + [35] = { + 24269, -- Heavy Netherweave Net + 18904, -- Zorbin's Ultra-Shrinker + }, + [40] = { + 28767, -- The Decapitator + }, + [45] = { + --32698, -- Wrangling Rope + 23836, -- Goblin Rocket Launcher + }, + [60] = { + 32825, -- Soul Cannon + 37887, -- Seeds of Nature's Wrath + }, + [70] = { + 41265, -- Eyesore Blaster + }, + [80] = { + 35278, -- Reinforced Net + }, + [100] = { + 33119, -- Malister's Frost Wand + }, + [150] = { + 46954, -- Flaming Spears + }, +} + +if isRetail then + HarmItems[4] = { + 129055, -- Shoe Shine Kit + } + HarmItems[7] = { + 61323, -- Ruby Seeds + } + HarmItems[38] = { + 140786, -- Ley Spider Eggs + } + HarmItems[50] = { + 116139, -- Haunting Memento + } + HarmItems[55] = { + 74637, -- Kiryn's Poison Vial + } + HarmItems[90] = { + 133925, -- Fel Lash + } + HarmItems[200] = { + 75208, -- Rancher's Lariat + } +end + +-- This could've been done by checking player race as well and creating tables for those, but it's easier like this +for _, v in pairs(FriendSpells) do + tinsert(v, 28880) -- Gift of the Naaru (40 yards) +end + +-- >> END OF STATIC CONFIG + +-- temporary stuff + +local pendingItemRequest +local itemRequestTimeoutAt +local foundNewItems +local cacheAllItems +local friendItemRequests +local harmItemRequests +local lastUpdate = 0 + +-- minRangeCheck is a function to check if spells with minimum range are really out of range, or fail due to range < minRange. See :init() for its setup +local minRangeCheck = function(unit) return CheckInteractDistance(unit, 2) end + +local checkers_Spell = setmetatable({}, { + __index = function(t, spellIdx) + local func = function(unit) + if IsSpellInRange(spellIdx, BOOKTYPE_SPELL, unit) == 1 then + return true + end + end + t[spellIdx] = func + return func + end +}) +local checkers_SpellWithMin = setmetatable({}, { + __index = function(t, spellIdx) + local func = function(unit) + if IsSpellInRange(spellIdx, BOOKTYPE_SPELL, unit) == 1 then + return true + elseif minRangeCheck(unit) then + return true, true + end + end + t[spellIdx] = func + return func + end +}) +local checkers_Item = setmetatable({}, { + __index = function(t, item) + local func = function(unit) + return IsItemInRange(item, unit) + end + t[item] = func + return func + end +}) +local checkers_Interact = setmetatable({}, { + __index = function(t, index) + local func = function(unit) + if CheckInteractDistance(unit, index) then + return true + end + end + t[index] = func + return func + end +}) + +-- helper functions +local function copyTable(src, dst) + if type(dst) ~= "table" then dst = {} end + if type(src) == "table" then + for k, v in pairs(src) do + if type(v) == "table" then + v = copyTable(v, dst[k]) + end + dst[k] = v + end + end + return dst +end + +local function initItemRequests(cacheAll) + friendItemRequests = copyTable(FriendItems) + harmItemRequests = copyTable(HarmItems) + cacheAllItems = cacheAll + foundNewItems = nil +end + +local function getNumSpells() + local _, _, offset, numSpells = GetSpellTabInfo(GetNumSpellTabs()) + return offset + numSpells +end + +-- return the spellIndex of the given spell by scanning the spellbook +local function findSpellIdx(spellName) + if not spellName or spellName == "" then + return nil + end + for i = 1, getNumSpells() do + local spell = GetSpellBookItemName(i, BOOKTYPE_SPELL) + if spell == spellName then + return i + end + end + return nil +end + +-- minRange should be nil if there's no minRange, not 0 +local function addChecker(t, range, minRange, checker, info) + local rc = { ["range"] = range, ["minRange"] = minRange, ["checker"] = checker, ["info"] = info } + for i = 1, #t do + local v = t[i] + if rc.range == v.range then return end + if rc.range > v.range then + tinsert(t, i, rc) + return + end + end + tinsert(t, rc) +end + +local function createCheckerList(spellList, itemList, interactList) + local res = {} + if itemList then + for range, items in pairs(itemList) do + for i = 1, #items do + local item = items[i] + if GetItemInfo(item) then + addChecker(res, range, nil, checkers_Item[item], "item:" .. item) + break + end + end + end + end + + if spellList then + for i = 1, #spellList do + local sid = spellList[i] + local name, _, _, _, minRange, range = GetSpellInfo(sid) + local spellIdx = findSpellIdx(name) + if spellIdx and range then + minRange = math_floor(minRange + 0.5) + range = math_floor(range + 0.5) + + -- print("### spell: " .. tostring(name) .. ", " .. tostring(minRange) .. " - " .. tostring(range)) + + if minRange == 0 then -- getRange() expects minRange to be nil in this case + minRange = nil + end + + if range == 0 then + range = MeleeRange + end + + if minRange then + addChecker(res, range, minRange, checkers_SpellWithMin[spellIdx], "spell:" .. sid .. ":" .. tostring(name)) + else + addChecker(res, range, minRange, checkers_Spell[spellIdx], "spell:" .. sid .. ":" .. tostring(name)) + end + end + end + end + + if interactList and not next(res) then + for index, range in pairs(interactList) do + addChecker(res, range, nil, checkers_Interact[index], "interact:" .. index) + end + end + + return res +end + +local rangeCache = {} + +local function resetRangeCache() + wipe(rangeCache) +end + +local function invalidateRangeCache(maxAge) + local currentTime = GetTime() + for k, v in pairs(rangeCache) do + -- if the entry is older than maxAge, clear this data from the cache + if v.updateTime + maxAge < currentTime then + rangeCache[k] = nil + end + end +end + +-- returns minRange, maxRange or nil +local function getRangeWithCheckerList(unit, checkerList) + local lo, hi = 1, #checkerList + while lo <= hi do + local mid = math_floor((lo + hi) / 2) + local rc = checkerList[mid] + if rc.checker(unit) then + lo = mid + 1 + else + hi = mid - 1 + end + end + if lo > #checkerList then + return 0, checkerList[#checkerList].range + elseif lo <= 1 then + return checkerList[1].range, nil + else + return checkerList[lo].range, checkerList[lo - 1].range + end +end + +local function getRange(unit, noItems) + local canAssist = UnitCanAssist("player", unit) + if UnitIsDeadOrGhost(unit) then + if canAssist then + return getRangeWithCheckerList(unit, lib.resRC) + else + return getRangeWithCheckerList(unit, lib.miscRC) + end + end + + if UnitCanAttack("player", unit) then + return getRangeWithCheckerList(unit, noItems and lib.harmNoItemsRC or lib.harmRC) + elseif UnitIsUnit("pet", unit) then + local minRange, maxRange = getRangeWithCheckerList(unit, noItems and lib.friendNoItemsRC or lib.friendRC) + if minRange or maxRange then + return minRange, maxRange + else + return getRangeWithCheckerList(unit, lib.petRC) + end + elseif canAssist then + return getRangeWithCheckerList(unit, noItems and lib.friendNoItemsRC or lib.friendRC) + else + return getRangeWithCheckerList(unit, lib.miscRC) + end +end + +local function getCachedRange(unit, noItems, maxCacheAge) + -- maxCacheAge has a default of 0.1 and a maximum of 1 second + maxCacheAge = maxCacheAge or 0.1; + maxCacheAge = maxCacheAge > 1 and 1 or maxCacheAge; + + -- compose cache key out of unit guid and noItems + local guid = UnitGUID(unit) + local cacheKey = guid .. (noItems and "-1" or "-0") + local cacheItem = rangeCache[cacheKey] + + local currentTime = GetTime() + + -- if then cache item is valid return it + if cacheItem and cacheItem.updateTime + maxCacheAge > currentTime then + return cacheItem.minRange, cacheItem.maxRange + end + + -- otherwise create a new or update the exisitng cache item + local result = cacheItem or {} + result.minRange, result.maxRange = getRange(unit, noItems) + result.updateTime = currentTime + rangeCache[cacheKey] = result + return result.minRange, result.maxRange +end + +local function updateCheckers(origList, newList) + if #origList ~= #newList then + wipe(origList) + copyTable(newList, origList) + return true + end + for i = 1, #origList do + if origList[i].range ~= newList[i].range or origList[i].checker ~= newList[i].checker then + wipe(origList) + copyTable(newList, origList) + return true + end + end +end + +local function rcIterator(checkerList) + local curr = #checkerList + return function() + local rc = checkerList[curr] + if not rc then + return nil + end + curr = curr - 1 + return rc.range, rc.checker + end +end + +local function getMinChecker(checkerList, range) + local checker, checkerRange + for i = 1, #checkerList do + local rc = checkerList[i] + if rc.range < range then + return checker, checkerRange + end + checker, checkerRange = rc.checker, rc.range + end + return checker, checkerRange +end + +local function getMaxChecker(checkerList, range) + for i = 1, #checkerList do + local rc = checkerList[i] + if rc.range <= range then + return rc.checker, rc.range + end + end +end + +local function getChecker(checkerList, range) + for i = 1, #checkerList do + local rc = checkerList[i] + if rc.range == range then + return rc.checker + end + end +end + +local function null() +end + +local function createSmartChecker(friendChecker, harmChecker, miscChecker) + miscChecker = miscChecker or null + friendChecker = friendChecker or miscChecker + harmChecker = harmChecker or miscChecker + return function(unit) + if not UnitExists(unit) then + return nil + end + if UnitIsDeadOrGhost(unit) then + return miscChecker(unit) + end + if UnitCanAttack("player", unit) then + return harmChecker(unit) + elseif UnitCanAssist("player", unit) then + return friendChecker(unit) + else + return miscChecker(unit) + end + end +end + +local minItemChecker = function(item) + if GetItemInfo(item) then + return function(unit) + return IsItemInRange(item, unit) + end + end +end + +-- OK, here comes the actual lib + +-- pre-initialize the checkerLists here so that we can return some meaningful result even if +-- someone manages to call us before we're properly initialized. miscRC should be independent of +-- race/class/talents, so it's safe to initialize it here +-- friendRC and harmRC will be properly initialized later when we have all the necessary data for them +lib.checkerCache_Spell = lib.checkerCache_Spell or {} +lib.checkerCache_Item = lib.checkerCache_Item or {} +lib.miscRC = createCheckerList(nil, nil, DefaultInteractList) +lib.friendRC = createCheckerList(nil, nil, DefaultInteractList) +lib.harmRC = createCheckerList(nil, nil, DefaultInteractList) +lib.resRC = createCheckerList(nil, nil, DefaultInteractList) +lib.petRC = createCheckerList(nil, nil, DefaultInteractList) +lib.friendNoItemsRC = createCheckerList(nil, nil, DefaultInteractList) +lib.harmNoItemsRC = createCheckerList(nil, nil, DefaultInteractList) + +lib.failedItemRequests = {} + +-- << Public API + +--@do-not-package@ +-- this is here just for .docmeta +--- A checker function. This type of function is returned by the various Get*Checker() calls. +-- @param unit the unit to check range to. +-- @return **true** if the unit is within the range for this checker. +local function checker(unit) +end + +--@end-do-not-package@ +--- The callback name that is fired when checkers are changed. +-- @field +lib.CHECKERS_CHANGED = "CHECKERS_CHANGED" +-- "export" it, maybe someone will need it for formatting +--- Constant for Melee range (2yd). +-- @field +lib.MeleeRange = MeleeRange + +function lib:findSpellIndex(spell) + if type(spell) == 'number' then + spell = GetSpellInfo(spell) + end + return findSpellIdx(spell) +end + +-- returns the range estimate as a string +-- deprecated, use :getRange(unit) instead and build your own strings +-- @param checkVisible if set to true, then a UnitIsVisible check is made, and **nil** is returned if the unit is not visible +function lib:getRangeAsString(unit, checkVisible, showOutOfRange) + local minRange, maxRange = self:getRange(unit, checkVisible) + if not minRange then return nil end + if not maxRange then + return showOutOfRange and minRange .. " +" or nil + end + return minRange .. " - " .. maxRange +end + +-- initialize RangeCheck if not yet initialized or if "forced" +function lib:init(forced) + if self.initialized and (not forced) then + return + end + self.initialized = true + local _, playerClass = UnitClass("player") + local _, playerRace = UnitRace("player") + + minRangeCheck = nil + + -- first try to find a nice item we can use for minRangeCheck + local harmItems = HarmItems[15] + if harmItems then + for i = 1, #harmItems do + local minCheck = minItemChecker(harmItems[i]) + if minCheck then + minRangeCheck = minCheck + break + end + end + end + + if not minRangeCheck then -- fall back to interact distance checks + if playerClass == "HUNTER" or playerRace == "Tauren" then + -- for Hunters: use interact4 as it's safer + -- for Taurens: interact4 is actually closer than 25yd and interact3 is closer than 8yd, so we can't use that + minRangeCheck = checkers_Interact[4] + else + minRangeCheck = checkers_Interact[3] + end + end + + local interactList = InteractLists[playerRace] or DefaultInteractList + self.handSlotItem = GetInventoryItemLink("player", HandSlotId) + local changed = false + if updateCheckers(self.friendRC, createCheckerList(FriendSpells[playerClass], FriendItems, interactList)) then + changed = true + end + if updateCheckers(self.harmRC, createCheckerList(HarmSpells[playerClass], HarmItems, interactList)) then + changed = true + end + if updateCheckers(self.friendNoItemsRC, createCheckerList(FriendSpells[playerClass], nil, interactList)) then + changed = true + end + if updateCheckers(self.harmNoItemsRC, createCheckerList(HarmSpells[playerClass], nil, interactList)) then + changed = true + end + if updateCheckers(self.miscRC, createCheckerList(nil, nil, interactList)) then + changed = true + end + if updateCheckers(self.resRC, createCheckerList(ResSpells[playerClass], nil, interactList)) then + changed = true + end + if updateCheckers(self.petRC, createCheckerList(PetSpells[playerClass], nil, interactList)) then + changed = true + end + if changed and self.callbacks then + self.callbacks:Fire(self.CHECKERS_CHANGED) + end +end + +--- Return an iterator for checkers usable on friendly units as (**range**, **checker**) pairs. +function lib:GetFriendCheckers() + return rcIterator(self.friendRC) +end + +--- Return an iterator for checkers usable on enemy units as (**range**, **checker**) pairs. +function lib:GetHarmCheckers() + return rcIterator(self.harmRC) +end + +--- Return an iterator for checkers usable on miscellaneous units as (**range**, **checker**) pairs. These units are neither enemy nor friendly, such as people in sanctuaries or corpses. +function lib:GetMiscCheckers() + return rcIterator(self.miscRC) +end + +--- Return a checker suitable for out-of-range checking on friendly units, that is, a checker whose range is equal or larger than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetFriendMinChecker(range) + return getMinChecker(self.friendRC, range) +end + +--- Return a checker suitable for out-of-range checking on enemy units, that is, a checker whose range is equal or larger than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetHarmMinChecker(range) + return getMinChecker(self.harmRC, range) +end + +--- Return a checker suitable for out-of-range checking on miscellaneous units, that is, a checker whose range is equal or larger than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetMiscMinChecker(range) + return getMinChecker(self.miscRC, range) +end + +--- Return a checker suitable for in-range checking on friendly units, that is, a checker whose range is equal or smaller than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetFriendMaxChecker(range) + return getMaxChecker(self.friendRC, range) +end + +--- Return a checker suitable for in-range checking on enemy units, that is, a checker whose range is equal or smaller than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetHarmMaxChecker(range) + return getMaxChecker(self.harmRC, range) +end + +--- Return a checker suitable for in-range checking on miscellaneous units, that is, a checker whose range is equal or smaller than the requested range. +-- @param range the range to check for. +-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for. +function lib:GetMiscMaxChecker(range) + return getMaxChecker(self.miscRC, range) +end + +--- Return a checker for the given range for friendly units. +-- @param range the range to check for. +-- @return **checker** function or **nil** if no suitable checker is available. +function lib:GetFriendChecker(range) + return getChecker(self.friendRC, range) +end + +--- Return a checker for the given range for enemy units. +-- @param range the range to check for. +-- @return **checker** function or **nil** if no suitable checker is available. +function lib:GetHarmChecker(range) + return getChecker(self.harmRC, range) +end + +--- Return a checker for the given range for miscellaneous units. +-- @param range the range to check for. +-- @return **checker** function or **nil** if no suitable checker is available. +function lib:GetMiscChecker(range) + return getChecker(self.miscRC, range) +end + +--- Return a checker suitable for out-of-range checking that checks the unit type and calls the appropriate checker (friend/harm/misc). +-- @param range the range to check for. +-- @return **checker** function. +function lib:GetSmartMinChecker(range) + return createSmartChecker( + getMinChecker(self.friendRC, range), + getMinChecker(self.harmRC, range), + getMinChecker(self.miscRC, range)) +end + +--- Return a checker suitable for in-range checking that checks the unit type and calls the appropriate checker (friend/harm/misc). +-- @param range the range to check for. +-- @return **checker** function. +function lib:GetSmartMaxChecker(range) + return createSmartChecker( + getMaxChecker(self.friendRC, range), + getMaxChecker(self.harmRC, range), + getMaxChecker(self.miscRC, range)) +end + +--- Return a checker for the given range that checks the unit type and calls the appropriate checker (friend/harm/misc). +-- @param range the range to check for. +-- @param fallback optional fallback function that gets called as fallback(unit) if a checker is not available for the given type (friend/harm/misc) at the requested range. The default fallback function return nil. +-- @return **checker** function. +function lib:GetSmartChecker(range, fallback) + return createSmartChecker( + getChecker(self.friendRC, range) or fallback, + getChecker(self.harmRC, range) or fallback, + getChecker(self.miscRC, range) or fallback) +end + +--- Get a range estimate as **minRange**, **maxRange**. +-- @param unit the target unit to check range to. +-- @param checkVisible if set to true, then a UnitIsVisible check is made, and **nil** is returned if the unit is not visible +-- @param noItems if set to true, no items and only spells are being used for the range check +-- @param maxCacheAge the timespan a cached range value is considered valid (default 0.1 seconds, maximum 1 second) +-- @return **minRange**, **maxRange** pair if a range estimate could be determined, **nil** otherwise. **maxRange** is **nil** if **unit** is further away than the highest possible range we can check. +-- Includes checks for unit validity and friendly/enemy status. +-- @usage +-- local rc = LibStub("LibRangeCheck-3.0") +-- local minRange, maxRange = rc:GetRange('target') +-- local minRangeIfVisible, maxRangeIfVisible = rc:GetRange('target', true) +function lib:GetRange(unit, checkVisible, noItems, maxCacheAge) + if not UnitExists(unit) then + return nil + end + + if checkVisible and not UnitIsVisible(unit) then + return nil + end + + return getCachedRange(unit, noItems, maxCacheAge) +end + +-- keep this for compatibility +lib.getRange = lib.GetRange + +-- >> Public API + +function lib:OnEvent(event, ...) + if type(self[event]) == 'function' then + self[event](self, event, ...) + end +end + +function lib:LEARNED_SPELL_IN_TAB() + self:scheduleInit() +end + +function lib:CHARACTER_POINTS_CHANGED() + self:scheduleInit() +end + +function lib:PLAYER_TALENT_UPDATE() + self:scheduleInit() +end + +function lib:SPELLS_CHANGED() + self:scheduleInit() +end + +function lib:UNIT_INVENTORY_CHANGED(event, unit) + if self.initialized and unit == "player" and self.handSlotItem ~= GetInventoryItemLink("player", HandSlotId) then + self:scheduleInit() + end +end + +function lib:UNIT_AURA(event, unit) + if self.initialized and unit == "player" then + self:scheduleAuraCheck() + end +end + +function lib:GET_ITEM_INFO_RECEIVED(event, item, success) + -- print("### GET_ITEM_INFO_RECEIVED: " .. tostring(item) .. ", " .. tostring(success)) + if item == pendingItemRequest then + pendingItemRequest = nil + if not success then + self.failedItemRequests[item] = true + end + lastUpdate = UpdateDelay + end +end + +function lib:processItemRequests(itemRequests) + while true do + local range, items = next(itemRequests) + if not range then return end + while true do + local i, item = next(items) + if not i then + itemRequests[range] = nil + break + elseif self.failedItemRequests[item] then + -- print("### processItemRequests: failed: " .. tostring(item)) + tremove(items, i) + elseif item == pendingItemRequest and GetTime() < itemRequestTimeoutAt then + return true; -- still waiting for server response + elseif GetItemInfo(item) then + -- print("### processItemRequests: found: " .. tostring(item)) + if itemRequestTimeoutAt then + -- print("### processItemRequests: new: " .. tostring(item)) + foundNewItems = true + itemRequestTimeoutAt = nil + pendingItemRequest = nil + end + if not cacheAllItems then + itemRequests[range] = nil + break + end + tremove(items, i) + elseif not itemRequestTimeoutAt then + -- print("### processItemRequests: waiting: " .. tostring(item)) + itemRequestTimeoutAt = GetTime() + ItemRequestTimeout + pendingItemRequest = item + if not self.frame:IsEventRegistered("GET_ITEM_INFO_RECEIVED") then + self.frame:RegisterEvent("GET_ITEM_INFO_RECEIVED") + end + return true + elseif GetTime() >= itemRequestTimeoutAt then + -- print("### processItemRequests: timeout: " .. tostring(item)) + if cacheAllItems then + print(MAJOR_VERSION .. ": timeout for item: " .. tostring(item)) + end + self.failedItemRequests[item] = true + itemRequestTimeoutAt = nil + pendingItemRequest = nil + tremove(items, i) + else + return true -- still waiting for server response + end + end + end +end + +function lib:initialOnUpdate() + self:init() + if friendItemRequests then + if self:processItemRequests(friendItemRequests) then return end + friendItemRequests = nil + end + if harmItemRequests then + if self:processItemRequests(harmItemRequests) then return end + harmItemRequests = nil + end + if foundNewItems then + self:init(true) + foundNewItems = nil + end + if cacheAllItems then + print(MAJOR_VERSION .. ": finished cache") + cacheAllItems = nil + end + self.frame:Hide() + self.frame:UnregisterEvent("GET_ITEM_INFO_RECEIVED") +end + +function lib:scheduleInit() + self.initialized = nil + lastUpdate = 0 + self.frame:Show() +end + +function lib:scheduleAuraCheck() + lastUpdate = UpdateDelay + self.frame:Show() +end + +--@do-not-package@ +-- << DEBUG STUFF + +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do tinsert(a, n) end + sort(a, f) + local i = 0 + local iter = function () + i = i + 1 + if a[i] == nil then + return nil + else + return a[i], t[a[i]] + end + end + return iter +end + +function lib:cacheAllItems() + if (not self.initialized) or harmItemRequests then + print(MAJOR_VERSION .. ": init hasn't finished yet") + return + end + print(MAJOR_VERSION .. ": starting item cache") + initItemRequests(true) + self.frame:Show() +end + +function lib:startMeasurement(unit, resultTable) + if (not self.initialized) or harmItemRequests then + print(MAJOR_VERSION .. ": init hasn't finished yet") + return + end + if self.measurements then + print(MAJOR_VERSION .. ": measurements already running") + return + end + print(MAJOR_VERSION .. ": starting measurements") + local _, playerClass = UnitClass("player") + local spellList + local itemList + if UnitCanAttack("player", unit) then + spellList = HarmSpells[playerClass] + itemList = HarmItems + elseif UnitCanAssist("player", unit) then + spellList = FriendSpells[playerClass] + itemList = FriendItems + end + self.spellsToMeasure = {} + if spellList then + for i = 1, #spellList do + local sid = spellList[i] + local name = GetSpellInfo(sid) + local spellIdx = findSpellIdx(name) + if spellIdx then + self.spellsToMeasure[name] = spellIdx + end + end + end + self.itemsToMeasure = {} + if itemList then + for range, items in pairs(itemList) do + for i = 1, #items do + local item = items[i] + local name = GetItemInfo(item) + if name then + self.itemsToMeasure[name] = item + end + end + end + end + self.measurements = resultTable + self.measurementUnit = unit + self.measurementStart = GetTime() + self.lastMeasurements = {} + self:updateMeasurements() + self.frame:SetScript("OnUpdate", function(frame, elapsed) self:updateMeasurements() end) + self.frame:Show() +end + +function lib:stopMeasurement() + print(MAJOR_VERSION .. ": stopping measurements") + self.frame:Hide() + self.frame:SetScript("OnUpdate", function(frame, elapsed) + lastUpdate = lastUpdate + elapsed + if lastUpdate < UpdateDelay then + return + end + lastUpdate = 0 + self:initialOnUpdate() + end) + self.measurements = nil +end + +function lib:checkItems(itemList, verbose, color) + if not itemList then return end + color = color or 'ffffffff' + for range, items in pairsByKeys(itemList) do + for i = 1, #items do + local item = items[i] + local name = GetItemInfo(item) + if not name then + print(MAJOR_VERSION .. ": |c" .. color .. tostring(item) .. "|r: " .. tostring(range) .. "yd: |cffeda500not in cache|r") + else + local res = IsItemInRange(item, "target") + if res == nil or verbose then + if res == nil then res = "|cffed0000nil|r" end + print(MAJOR_VERSION .. ": |c" .. color .. tostring(item) .. ": " .. tostring(name) .. "|r: " .. tostring(range) .. "yd: " .. tostring(res)) + end + end + end + end +end + +function lib:checkSpells(spellList, verbose, color) + if not spellList then return end + color = color or 'ffffffff' + for i = 1, #spellList do + local sid = spellList[i] + local name, _, _, _, minRange, range = GetSpellInfo(sid) + if (not name) or (name == "") or (not range) then + print(MAJOR_VERSION .. ": |c" .. color .. tostring(sid) .. "|r: " .. tostring(range) .. "yd: |cffeda500invalid spell id|r") + else + local spellIdx = self:findSpellIndex(sid) + if not spellIdx then + print(MAJOR_VERSION .. ": |c" .. color .. tostring(sid) .. ": " .. tostring(name) .. "|r: " .. tostring(minRange) .. "-" .. tostring(range) .. "yd: |cffeda500not in spellbook|r") + else + local res = IsSpellInRange(spellIdx, BOOKTYPE_SPELL, "target") + if res == nil or verbose then + if res == nil then res = "|cffed0000nil|r" end + print(MAJOR_VERSION .. ": |c" .. color .. tostring(sid) .. ": " .. tostring(name) .. "|r: " .. tostring(minRange) .. "-" .. tostring(range) .. "yd: " .. tostring(res)) + end + end + end + end +end + +function lib:checkAllItems() + print(MAJOR_VERSION .. ": Checking FriendItems...") + self:checkItems(FriendItems, true, FriendColor) + print(MAJOR_VERSION .. ": Checking HarmItems...") + self:checkItems(HarmItems, true, HarmColor) +end + +function lib:checkAllSpells() + local _, playerClass = UnitClass("player") + print(MAJOR_VERSION .. ": Checking FriendSpells: " .. playerClass) + self:checkSpells(FriendSpells[playerClass], true, FriendColor) + print(MAJOR_VERSION .. ": Checking HarmSpells..." .. playerClass) + self:checkSpells(HarmSpells[playerClass], true, HarmColor) +end + +local function dumpCheckerList(checkerList) + for _, rc in ipairs(checkerList) do + if rc.minRange then + print(rc.minRange .. "-" .. rc.range .. ": " .. rc.info) + else + print(rc.range .. ": " .. rc.info) + end + end +end + +function lib:checkAllCheckers() + if not UnitExists("target") then + print(MAJOR_VERSION .. ": Invalid unit, cannot check") + return + end + local _, playerClass = UnitClass("player") + if UnitCanAttack("player", "target") then + print(MAJOR_VERSION .. ": Harm checker list: " .. playerClass) + dumpCheckerList(self.harmRC) + print(MAJOR_VERSION .. ": Checking HarmCheckers: " .. playerClass) + self:checkItems(HarmItems) + self:checkSpells(HarmSpells[playerClass]) + elseif UnitCanAssist("player", "target") then + print(MAJOR_VERSION .. ": Friend checker list: " .. playerClass) + dumpCheckerList(self.friendRC) + print(MAJOR_VERSION .. ": Checking FriendCheckers: ") + self:checkItems(FriendItems) + self:checkSpells(FriendSpells[playerClass]) + else + print(MAJOR_VERSION .. ": Misc checker list: " .. playerClass) + dumpCheckerList(self.miscRC) + print(MAJOR_VERSION .. ": Misc unit, cannot check") + return + end + print(MAJOR_VERSION .. ": done.") +end + +local function logMeasurementChange(t, t0, key, last, curr) + local d = 0 + local scale = 1240 + if t0 then + local dx = scale * (t.x - t0.x) + local dy = scale * (t.y - t0.y) + d = _G.sqrt(dx * dx + dy * dy) + end + print(MAJOR_VERSION .. ": t=" .. ("%.4f"):format(t.stamp) .. ": d=" .. ("%.4f"):format(d) .. ": " .. tostring(key) .. ": " .. tostring(last) .. " -> " .. tostring(curr)) +end + +local GetPlayerMapPosition = GetPlayerMapPosition or function(unit) + local map = C_Map.GetBestMapForUnit(unit) + local pos = C_Map.GetPlayerMapPosition(map, unit) + return pos:GetXY() +end +function lib:updateMeasurements() + local now = GetTime() - self.measurementStart + local x, y = GetPlayerMapPosition("player") + local t0 = self.measurements[0] + local t = self.measurements[now] + local unit = self.measurementUnit + for name, id in pairs(self.spellsToMeasure) do + local key = 'spell: ' .. name + local last = self.lastMeasurements[key] + local curr = (IsSpellInRange(id, BOOKTYPE_SPELL, unit) == 1) and true or false + if last == nil or last ~= curr then + if not t then + t = {} + t.x, t.y, t.stamp, t.states = x, y, now, {} + self.measurements[now] = t + end + logMeasurementChange(t, t0, key, last, curr) + t.states[key]= curr + self.lastMeasurements[key] = curr + end + end + for name, item in pairs(self.itemsToMeasure) do + local key = 'item: ' .. name; + local last = self.lastMeasurements[key] + local curr = IsItemInRange(item, unit) and true or false + if last == nil or last ~= curr then + if not t then + t = {} + t.x, t.y, t.stamp, t.states = x, y, now, {} + self.measurements[now] = t + end + logMeasurementChange(t, t0, key, last, curr) + t.states[key]= curr + self.lastMeasurements[key] = curr + end + end + for i, v in pairs(DefaultInteractList) do + local key = 'interact: ' .. i + local last = self.lastMeasurements[key] + local curr = CheckInteractDistance(unit, i) and true or false + if last == nil or last ~= curr then + if not t then + t = {} + t.x, t.y, t.stamp, t.states = x, y, now, {} + self.measurements[now] = t + end + logMeasurementChange(t, t0, key, last, curr) + t.states[key] = curr + self.lastMeasurements[key] = curr + end + end +end + +local debugprofilestop = debugprofilestop +function lib:speedTest(numBatches, numIterationsPerBatch) + if not UnitExists("target") then + print(MAJOR_VERSION .. ": Invalid unit, cannot check") + return + end + + numBatches = numBatches or 10000 + numIterationsPerBatch = numIterationsPerBatch or 1 + + local min, max, total = 999999, 0, 0 + for b = 1, numBatches do + resetRangeCache() + local start = debugprofilestop() + for i = 1, numIterationsPerBatch do + self:getRange("target") + end + local duration = debugprofilestop() - start + + if duration < min then min = duration end + if duration > max then max = duration end + total = total + duration + end + + local minRange, maxRange = self:getRange("target"); + + print(string.format("SpeedTest: numBatches = %d, numIterationsPerBatch = %d", numBatches, numIterationsPerBatch)) + print(string.format(" Range: min = %d, max = %d", minRange, maxRange)) + print(string.format(" Time per batch: min = %f, max = %f, total = %f, avg = %f", min, max, total, total/numBatches)) +end + +-- >> DEBUG STUFF +--@end-do-not-package@ + +-- << load-time initialization + +function lib:activate() + if not self.frame then + local frame = CreateFrame("Frame") + self.frame = frame + + frame:RegisterEvent("LEARNED_SPELL_IN_TAB") + frame:RegisterEvent("CHARACTER_POINTS_CHANGED") + frame:RegisterEvent("SPELLS_CHANGED") + + if isRetail or isWrath then + frame:RegisterEvent("PLAYER_TALENT_UPDATE") + end + + local _, playerClass = UnitClass("player") + if playerClass == "MAGE" or playerClass == "SHAMAN" then + -- Mage and Shaman gladiator gloves modify spell ranges + frame:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player") + end + end + + if not self.cacheResetTimer then + self.cacheResetTimer = C_Timer.NewTicker(5, function() + invalidateRangeCache(5) + end) + end + + initItemRequests() + + self.frame:SetScript("OnEvent", function(_, ...) self:OnEvent(...) end) + self.frame:SetScript("OnUpdate", function(_, elapsed) + lastUpdate = lastUpdate + elapsed + if lastUpdate < UpdateDelay then + return + end + lastUpdate = 0 + self:initialOnUpdate() + end) + + self:scheduleInit() +end + +--- BEGIN CallbackHandler stuff + +do + --- Register a callback to get called when checkers are updated + -- @class function + -- @name lib.RegisterCallback + -- @usage + -- rc.RegisterCallback(self, rc.CHECKERS_CHANGED, "myCallback") + -- -- or + -- rc.RegisterCallback(self, "CHECKERS_CHANGED", someCallbackFunction) + -- @see CallbackHandler-1.0 documentation for more details + lib.RegisterCallback = lib.RegisterCallback or function(...) + local CBH = LibStub("CallbackHandler-1.0") + lib.RegisterCallback = nil -- extra safety, we shouldn't get this far if CBH is not found, but better an error later than an infinite recursion now + lib.callbacks = CBH:New(lib) + -- ok, CBH hopefully injected or new shiny RegisterCallback + return lib.RegisterCallback(...) + end +end + +--- END CallbackHandler stuff + +lib:activate() diff --git a/LibRangeCheck-3.0_Vanilla.toc b/LibRangeCheck-3.0_Vanilla.toc new file mode 100644 index 0000000..f107c39 --- /dev/null +++ b/LibRangeCheck-3.0_Vanilla.toc @@ -0,0 +1,13 @@ +## Interface: 11403 +## Title: Lib: RangeCheck-3.0 +## Notes: A library to determine estimated range. +## Author: The WoWUIDev Community +## Version: @project-version@ +## LoadOnDemand: 1 +## X-Category: Library +## X-Credits: mitch0 +## X-License: MIT + +LibStub-1.0\LibStub.lua +CallbackHandler-1.0\CallbackHandler-1.0.xml +LibRangeCheck-2.0\LibRangeCheck-2.0.lua diff --git a/LibRangeCheck-3.0_Wrath.toc b/LibRangeCheck-3.0_Wrath.toc new file mode 100644 index 0000000..03ac0dc --- /dev/null +++ b/LibRangeCheck-3.0_Wrath.toc @@ -0,0 +1,13 @@ +## Interface: 30402 +## Title: Lib: RangeCheck-3.0 +## Notes: A library to determine estimated range. +## Author: The WoWUIDev Community +## Version: @project-version@ +## LoadOnDemand: 1 +## X-Category: Library +## X-Credits: mitch0 +## X-License: MIT + +LibStub-1.0\LibStub.lua +CallbackHandler-1.0\CallbackHandler-1.0.xml +LibRangeCheck-2.0\LibRangeCheck-2.0.lua diff --git a/README.md b/README.md index ca1089e..e5ea644 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # LibRangeCheck-3.0 + A library to determine estimated range in World of Warcraft