local _, addonTable = ... local EventHandlers = {} -- Upvalues local R = Rarity local CONSTANTS = addonTable.constants -- Locals local coinamounts = {} -- Externals local L = LibStub("AceLocale-3.0"):GetLocale("Rarity") local lbz = LibStub("LibBabble-Zone-3.0"):GetUnstrictLookupTable() local lbsz = LibStub("LibBabble-SubZone-3.0"):GetUnstrictLookupTable() local lbb = LibStub("LibBabble-Boss-3.0"):GetUnstrictLookupTable() -- Lua APIs local bit_band = _G.bit.band local strlower = _G.strlower local format = _G.format -- WOW APIs local GetCurrencyInfo = _G.C_CurrencyInfo.GetCurrencyInfo local CombatLogGetCurrentEventInfo = _G.CombatLogGetCurrentEventInfo local UnitGUID = UnitGUID local LoadAddOn = _G.C_AddOns.LoadAddOn local GetBestMapForUnit = _G.C_Map.GetBestMapForUnit local GetMapInfo = _G.C_Map.GetMapInfo local IsQuestFlaggedCompleted = C_QuestLog.IsQuestFlaggedCompleted local UnitCanAttack = _G.UnitCanAttack local UnitIsPlayer = _G.UnitIsPlayer local UnitIsDead = _G.UnitIsDead local GetNumLootItems = _G.GetNumLootItems local GetLootSlotInfo = _G.GetLootSlotInfo local GetLootSlotLink = _G.GetLootSlotLink local GetItemInfo_Blizzard = _G.C_Item.GetItemInfo local GetItemInfo = function(id) return R:GetItemInfo(id) end local GetRealZoneText = _G.GetRealZoneText local GetContainerNumSlots = _G.C_Container.GetContainerNumSlots local GetContainerItemID = _G.C_Container.GetContainerItemID local GetContainerItemInfo = _G.C_Container.GetContainerItemInfo local GetNumArchaeologyRaces = _G.GetNumArchaeologyRaces or function() return 0 end local GetArchaeologyRaceInfo = _G.GetArchaeologyRaceInfo local GetStatistic = _G.GetStatistic local GetLootSourceInfo = _G.GetLootSourceInfo local C_Timer = _G.C_Timer local IsSpellKnown = _G.IsSpellKnown local GetCurrentRenownLevel = C_MajorFactions and C_MajorFactions.GetCurrentRenownLevel -- Addon APIs local DebugCache = Rarity.Utils.DebugCache function EventHandlers:Register() self = Rarity local WOW_INTERFACE_VER = select(4, GetBuildInfo()) self:UnregisterAllEvents() self:RegisterBucketEvent("BAG_UPDATE", 0.5, "OnBagUpdate") self:RegisterEvent("LOOT_READY", "OnLootReady") self:RegisterEvent("CURRENCY_DISPLAY_UPDATE", "OnCurrencyUpdate") self:RegisterEvent("RESEARCH_ARTIFACT_COMPLETE", "OnResearchArtifactComplete") self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", "OnCombat") -- Used to detect boss kills that we didn't solo self:RegisterEvent("CURSOR_CHANGED", "OnCursorChanged") -- Fishing detection self:RegisterEvent("UNIT_SPELLCAST_SENT", "OnSpellcastSent") -- Fishing detection self:RegisterEvent("UNIT_SPELLCAST_STOP", "OnSpellcastStopped") -- Fishing detection self:RegisterEvent("UNIT_SPELLCAST_FAILED", "OnSpellcastFailed") -- Fishing detection self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED", "OnSpellcastFailed") -- Fishing detection self:RegisterEvent("LOOT_CLOSED", "OnLootFrameClosed") -- Fishing detection self:RegisterEvent("PLAYER_LOGOUT", "OnEvent") self:RegisterEvent("TRADE_SKILL_SHOW", "OnEvent") self:RegisterEvent("TRADE_SKILL_CLOSE", "OnEvent") self:RegisterEvent("UPDATE_MOUSEOVER_UNIT", "OnMouseOver") self:RegisterEvent("CRITERIA_COMPLETE", "OnCriteriaComplete") self:RegisterEvent("ENCOUNTER_END", "OnEncounterEnd") self:RegisterEvent("PLAYER_REGEN_ENABLED", "OnCombatEnded") self:RegisterEvent("PET_BATTLE_OPENING_START", "OnPetBattleStart") self:RegisterEvent("PET_BATTLE_CLOSE", "OnPetBattleEnd") self:RegisterEvent("ISLAND_COMPLETED", "OnIslandCompleted") self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", "OnSpellcastSucceeded") self:RegisterEvent("QUEST_TURNED_IN", "OnQuestTurnedIn") if LE_EXPANSION_LEVEL_CURRENT >= LE_EXPANSION_MISTS_OF_PANDARIA then self:RegisterEvent("SHOW_LOOT_TOAST", "OnShowLootToast") end self:RegisterBucketEvent("UPDATE_INSTANCE_INFO", 1, "OnEvent") self:RegisterBucketEvent("LFG_UPDATE_RANDOM_INFO", 1, "OnEvent") self:RegisterBucketEvent("CALENDAR_UPDATE_EVENT_LIST", 1, "OnEvent") self:RegisterBucketEvent("TOYS_UPDATED", 1, "OnEvent") self:RegisterBucketEvent("COMPANION_UPDATE", 1, "OnEvent") self:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_SHOW", "OnPlayerInteractionFrameShow") self:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE", "OnPlayerInteractionFrameHide") end -- TODO: Move elsewhere/refactor local function addAttemptForItem(itemName, categoryName) if not itemName or not categoryName then return end local Rarity = Rarity local group = Rarity.db.profile.groups[categoryName] if not group then return end local item = group[itemName] if not item then return end if item and type(item) == "table" and item.enabled ~= false and Rarity:IsAttemptAllowed(item) then -- Add one attempt for this item if item.attempts == nil then item.attempts = 1 else item.attempts = item.attempts + 1 end Rarity:OutputAttempts(item) end end local function table_contains(haystack, needle) if type(haystack) ~= "table" then return end for _, value in pairs(haystack) do if value == needle then return true end end end local function IsPlayerInHorrificVision() return (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_VISION_OF_STORMWIND) or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_VISION_OF_ORGRIMMAR) or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_REVISION_OF_STORMWIND) or (GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.HORRIFIC_REVISION_OF_ORGRIMMAR) end function R:OnSpellcastSucceeded(event, unitID, castGUID, spellID) if unitID ~= "player" then return end if not Rarity.relevantSpells[spellID] then return end R:Debug("OnSpellcastSucceeded triggered with relevant spell " .. spellID) if IsPlayerInHorrificVision() and spellID == 312881 then self:Debug("Finished searching mailbox in a Horrific Vision") addAttemptForItem("Mail Muncher", "mounts") end if IsPlayerInHorrificVision() and spellID == 1223438 then self:Debug("Finished searching trash pile in a Horrific Vision") addAttemptForItem("Nesting Swarmite", "mounts") end if spellID == 430315 then addAttemptForItem("Writhing Transmutagen", "pets") end -- Detects opening on Dirty Glinting Object which may contain Lucy's Lost Collar if spellID == 345071 and Rarity.lastNode and Rarity.lastNode == L["Dirty Glinting Object"] then Rarity:Debug("Detected Opening on " .. L["Dirty Glinting Object"] .. " (method = SPECIAL)") addAttemptForItem("Lucy's Lost Collar", "pets") end end -- TBD: Move to shared constants? local TYPE_IDENTIFIER_ITEM = "item" -- What others do they have? currency? gold? No idea. -- Upvalues --- WOW API local GetItemInfoInstant = _G.C_Item.GetItemInfoInstant function R:OnShowLootToast( event, typeIdentifier, itemLink, quantity, specID, sex, personalLootToast, toastMethod, lessAwesome, upgraded, corrupted ) if typeIdentifier ~= TYPE_IDENTIFIER_ITEM then return R:Debug(format("Ignoring loot toast of type %s (not an item)", typeIdentifier)) end -- From wowhead: "\124cff0070dd\124Hitem:187278::::::::60:::::\124h[Talon-Pierced Mawsworn Lockbox]\124h\124r" local itemID = GetItemInfoInstant(itemLink) -- TBD: Should we generalize this to a LOOT_TOAST detection method? Not sure if there are many other items people would care about -- Seems a bit too specialized, since we're detecting the loot toast for one item and then add attempts for another (hacky) local TALONPIERCED_MAWSWORN_LOCKBOX = 187278 -- TBD: Move to shared enum for ITEM_IDS? If we ever get to that point, that is... local lootToastItems = { [TALONPIERCED_MAWSWORN_LOCKBOX] = "Wilderling Saddle" } local linkedItemName = lootToastItems[itemID] if not linkedItemName then return R:Debug(format("Ignoring loot toast item %s (not relevant)", itemID)) end -- There's only one item, so hardcoding the mounts group isn't an issue (but if we do want to generalize this later, it'll be easy) addAttemptForItem(linkedItemName, "items") -- Should take care of the covenant restriction by itself (and not add them if it doesn't match) end ------------------------------------------------------------------------------------- -- Pet battles: we want to hide the progress bar(s) during them ------------------------------------------------------------------------------------- local wasBarVisibleBeforePetBattle = false function R:OnPetBattleStart(event) R:Debug("Pet battle started") wasBarVisibleBeforePetBattle = R.db.profile.bar.visible R.db.profile.bar.visible = false Rarity.GUI:UpdateBar() Rarity.GUI:UpdateText() end function R:OnPetBattleEnd(event) R:Debug("Pet battle ended") R.db.profile.bar.visible = wasBarVisibleBeforePetBattle Rarity.GUI:UpdateBar() Rarity.GUI:UpdateText() end ------------------------------------------------------------------------------------- -- Currency updates. Used for coin roll and archaeology solve detection. ------------------------------------------------------------------------------------- function R:OnCurrencyUpdate(event) self:Debug("Currency updated (" .. event .. ")") -- Check if any coins were used for k, v in pairs(self.coins) do local currency = GetCurrencyInfo(k) if currency == nil then return end local name, currencyAmount = currency.name, currency.quantity local diff = currencyAmount - (coinamounts[k] or 0) coinamounts[k] = currencyAmount if diff < 0 then self:Debug("Used coin: " .. name) R:CheckForCoinItem() self:ScheduleTimer(function() R:CheckForCoinItem() end, 2) self:ScheduleTimer(function() R:CheckForCoinItem() end, 5) self:ScheduleTimer(function() R:CheckForCoinItem() end, 10) self:ScheduleTimer(function() R:CheckForCoinItem() end, 15) self:ScheduleTimer(function() R:CheckForCoinItem() end, 20) self:ScheduleTimer(function() R:CheckForCoinItem() end, 25) end end end function R:CheckForCoinItem() if self.lastCoinItem and self.lastCoinItem.enableCoin then self:Debug("COIN USE DETECTED FOR AN ITEM") if self.lastCoinItem.attempts == nil then self.lastCoinItem.attempts = 1 else self.lastCoinItem.attempts = self.lastCoinItem.attempts + 1 end self:OutputAttempts(self.lastCoinItem) self.lastCoinItem = nil end end ------------------------------------------------------------------------------------- -- Raid encounter ended: -- Used for detecting raid bosses that don't actually die when the encounter ends and -- have no statistic tied to them (e.g., the Keepers of Ulduar) -- While it might work to change their method from NPC to BOSS, -- at this time I'm not sure if that wouldn't cause problems elsewhere... so I won't touch it ------------------------------------------------------------------------------------- local encounterLUT = { -- See https://warcraft.wiki.gg/wiki/DungeonEncounterID [1140] = { "Stormforged Rune" }, -- The Assembly of Iron [1133] = { "Blessed Seed" }, -- Freya [1135] = { "Ominous Pile of Snow" }, -- Hodir [1138] = { "Overcomplicated Controller" }, -- Mimiron [1143] = { "Wriggling Darkness" }, -- Yogg-Saron (mount uses the BOSS method and is tracked separately) [1500] = { "Celestial Gift" }, -- Elegon [1505] = { "Azure Cloud Serpent Egg" }, -- Tsulong [1506] = { "Spirit of the Spring" }, -- Lei Shi -- 8.3: Horrific Visions [2332] = { "Swirling Black Bottle", "Void-Link Frostwolf Collar" }, -- Thrall the Corrupted [2338] = { "Swirling Black Bottle", "Voidwoven Cat Collar" }, -- Alleria Windrunner [2370] = { "C'Thuffer" }, -- Rexxar [2377] = { "Void-Scarred Hare" }, -- Magister Umbric [2372] = { "Void-Touched Souvenir Totem", "Box With Faintly Glowing 'Air' Holes" }, -- Oblivion Elemental (Final objective for Zekhan's area) [2374] = { 'Box Labeled "Danger: Void Rat Inside"' }, -- Therum Deepforge (Final objective for Kelsey's area) -- 11.1.5 Horrific Visions (Revisited) [3081] = { "Swirling Black Bottle", "Voidwoven Cat Collar" }, -- Alleria Windrunner [3082] = { 'Box Labeled "Danger: Void Rat Inside"' }, -- Therum Deepforge (Final objective for Kelsey's area) [3084] = { "Eye of Chaos" }, -- Mathias Shaw (Old Town) [3085] = { "Void-Scarred Hare" }, -- Magister Umbric [3086] = { "Swirling Black Bottle", "Void-Link Frostwolf Collar" }, -- Thrall the Corrupted [3087] = { "Void Scarred Scorpid" }, -- Inquistor Gnshal [3088] = { "Void-Touched Souvenir Totem", "Box With Faintly Glowing 'Air' Holes" }, -- Oblivion Elemental (Final objective for Zekhan's area) [3089] = { "Void-Scarred Egg" }, -- Vezokk [3090] = { "C'Thuffer" }, -- Rexxar } function R:OnEncounterEnd(event, encounterID, encounterName, difficultyID, raidSize, endStatus) R:Debug( "ENCOUNTER_END with encounterID = " .. tonumber(encounterID or "0") .. ", name = " .. tostring(encounterName) .. ", endStatus = " .. tostring(endStatus) ) local items = encounterLUT[encounterID] if type(items) ~= "table" then -- Not a relevant encounter return end for _, item in ipairs(items) do if item and type(item) == "string" then -- This encounter has an entry in the LUT and needs special handling R:Debug("Found item of interest for this encounter: " .. tostring(item)) local v = self.db.profile.groups.pets[item] or self.db.profile.groups.items[item] or self.db.profile.groups.mounts[item] -- v = value = number of attempts for this item if endStatus == 1 then -- Encounter succeeded -> Check if number of attempts should be increased if v and type(v) == "table" and v.enabled ~= false and R:IsAttemptAllowed(v) then -- Add one attempt for this item if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end R:OutputAttempts(v) end end end end end ------------------------------------------------------------------------------------- -- When combat ends, we scan your statistics a few times. -- This helps catch some items that can't be tracked by normal means (i.e. Ragnaros), -- as well as acting as another backup to detect attempts if we missed one. -- WoW can take a few seconds to update statistics, thus the repeated scans. -- This is also where we detect if an achievement criteria has been met. ------------------------------------------------------------------------------------- do local timer1, timer2, timer3, timer4, timer5, timer6 function R:OnCombatEnded(event) -- if R:InTooltip() then Rarity:ShowTooltip() end self:CancelTimer(timer1, true) self:CancelTimer(timer2, true) self:CancelTimer(timer3, true) self:CancelTimer(timer4, true) self:CancelTimer(timer5, true) self:CancelTimer(timer6, true) self:ScanStatistics(event) timer1 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 1") end, 2) timer2 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 2") end, 5) timer3 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 3") end, 8) timer4 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 4") end, 10) timer5 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 5") end, 15) timer6 = self:ScheduleTimer(function() Rarity:ScanStatistics(event .. " 6") end, 20) end end ------------------------------------------------------------------------------------- -- Handle boss kills. You may not ever open a loot window on a boss, so we need to watch the combat log for its death. -- This event also handles some special cases. ------------------------------------------------------------------------------------- function R:OnCombat() self.Profiling:StartTimer("EventHandlers.OnCombat") -- Extract event payload (it's no longer being passed by the event iself as of 8.0.1) local timestamp, eventType, hideCaster, srcGuid, srcName, srcFlags, srcRaidFlags, dstGuid, dstName, dstFlags, dstRaidFlags, spellId, spellName, spellSchool, auraType = CombatLogGetCurrentEventInfo() if eventType == "UNIT_DIED" then -- A unit died near you local npcid = self:GetNPCIDFromGUID(dstGuid) if Rarity.bosses[npcid] then -- It's a boss we're interested in R:Debug("Detected UNIT_DIED for relevant NPC with ID = " .. tostring(npcid)) if bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) or bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_PARTY) or bit_band(srcFlags, COMBATLOG_OBJECT_AFFILIATION_RAID) then -- You, a party member, or a raid member killed it if not Rarity.guids[dstGuid] then if not UnitAffectingCombat("player") and not UnitIsDead("player") then Rarity:Debug("Ignoring this UNIT_DIED event because the player is alive, but not in combat") self.Profiling:EndTimer("EventHandlers.OnCombat") return end -- Increment attempts counter(s). One NPC might drop multiple things we want, so scan for them all. if Rarity.npcs_to_items[npcid] and type(Rarity.npcs_to_items[npcid]) == "table" then for k, v in pairs(Rarity.npcs_to_items[npcid]) do local isBossDrop = (v.method == CONSTANTS.DETECTION_METHODS.BOSS) local hasKillStatistics = type(v.statisticId) ~= "nil" if v.enabled ~= false and isBossDrop and not hasKillStatistics then if self:IsAttemptAllowed(v) then Rarity.guids[dstGuid] = true if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end end end end end end self.Profiling:EndTimer("EventHandlers.OnCombat") end local worldEventQuests = { [52196] = "Slightly Damp Pile of Fur", -- Dunegorger Kraulok [70867] = "Everlasting Horn of Lavaswimming", -- Scalebane Keep (scenario completion) -- Not actually from a world quest/event [85830] = "Parrot Cage (Void-Scarred Parrot)", -- More accurately detected via object GUID } function R:OnQuestTurnedIn(event, questID, experience, money) self:Debug( "OnQuestTurnedIn triggered with ID = " .. questID .. ", experience = " .. experience .. ", money = " .. money ) local relevantItem = worldEventQuests[questID] if not relevantItem then return end self:Debug(format("Relevant quest turnin detected for item %s (questID = %d)", questID, relevantItem)) local v = self.db.profile.groups.items[relevantItem] or self.db.profile.groups.pets[relevantItem] or self.db.profile.groups.mounts[relevantItem] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end ------------------------------------------------------------------------------------- -- ISLAND_COMPLETED handling: Used for detecting Island Expeditions in Battle for Azeroth -- The collectibles are awarded randomly once the scenario has ended and appear in the "Pose" screen, right after this event is fired (indicated by a subsequent LFG_COMPLETION_REWARD event) ------------------------------------------------------------------------------------- local islandMapIDs = { -- These IDs can be found in DBFilesClient\Map.db2 [1813] = "Un'gol Ruins", [1897] = "Molten Cay", [1883] = "Whispering Reef", [1882] = "Verdant Wilds", [1892] = "Rotting Mire", [1893] = "Dread Chain", [1898] = "Skittering Hollow", [1814] = "Havenswood", [1879] = "Jorundall", [1907] = "Snowblossom", [2124] = "Crestfall", } local islandExpeditionCollectibles = { -- List of collectibles (so we don't have to search the item DB for them) -- Pets ---- 8.0 "Scuttle", "Captain Nibs", "Barnaby", "Poro", "Octopode Fry", "Inky", "Sparkleshell Sandcrawler", "Kindleweb Spiderling", "Mischievous Zephyr", "Littlehoof", "Snapper", "Sunscale Hatchling", "Bloodstone Tunneler", "Snort", "Muskflank Calfling", "Juvenile Brineshell", "Kunchong Hatchling", "Coldlight Surfrunner", "Voru'kar Leecher", "Tinder Pup", "Sandshell Chitterer", "Deathsting Scorpid", "Thistlebrush Bud", "Giggling Flame", "Laughing Stonekin", "Playful Frostkin", "False Knucklebump", "Craghoof Kid", ---- 8.1 "Baby Stonehide", "Leatherwing Screecher", "Rotting Ghoul", "Thunderscale Whelpling", "Scritches", "Tonguelasher", "Lord Woofington", "Firesting Buzzer", "Needleback Pup", "Shadefeather Hatchling", ---- 8.2 "Adventurous Hopling Pack", "Ghostly Whelpling", -- Toys ---- 8.0 "Oomgut Ritual Drum", "Whiskerwax Candle", "Enchanted Soup Stone", "Magic Monkey Banana", "Bad Mojo Banana", -- Mounts ---- 8.0 "Surf Jelly", "Squawks", "Qinsho's Eternal Hound", "Craghorn Chasm-Leaper", "Twilight Avenger", ---- 8.1 "Risen Mare", "Island Thunderscale", "Bloodgorged Hunter", "Stonehide Elderhorn", } function R:OnIslandCompleted(event, mapID, winner) R:Debug( "Detected completion for Island Expedition: " .. (islandMapIDs[mapID] or "Unknown Map") .. " (mapID = " .. tostring(mapID) .. ")" ) if islandMapIDs[mapID] then -- Is a relevant map -> Add attempts for all collectibles -- (for now, I'm assuming they just drop from everything at the same rate. -- This may have to be revised once more data is available...) -- Update: Proper tracking seems night on impossible so this'll have to do for the time being -- See https://github.com/SacredDuckwhale/Rarity/issues/61 for more details R:Debug("Found this Island Expedition to be relevant -> Adding attempts for all known collectibles...") for index, name in pairs(islandExpeditionCollectibles) do -- Add an attempt for each item local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] or self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end end local timewalkingCriteriaLUT = { [24801] = "Ozumat", -- Legacy (seems to no longer work? Perhaps the criterion ID was changed...) [34414] = "Ozumat", -- Timewalking difficulty only? (need to test) [24784] = "Trial of the King", -- [126952] = "Trial of the King", -- Object: Legacy of the Clan Leaders [19244] = "Master Snowdrift", -- [123096] = "Master Snowdrift", -- Object: Snowdrift's Possessions [34410] = "Taran Zhu", -- [123095] = "Taran Zhu", -- Object: Taran Zhu's Personal Stash } local timeRiftCriteriaLUT = { [60685] = "Gill'dan (Azmerloth)", [60688] = "Freya (Ulderoth)", [60689] = "The Lich King (Azmourne)", [60690] = "Illidan Stormrage (Azewrath)", [60691] = "Fury of N'zoth (Azq'roth)", [60692] = "Varian Wrynn (The Warlands)", [60693] = "Overlord Mechagon (A.Z.E.R.O.T.H.)", } local timeRiftMounts = { "Felstorm Dragon", "Gold-Toed Albatross", "Perfected Juggernaut", "Reins of the Scourgebound Vanquisher", "Sulfur Hound's Leash", } local timeRiftPets = { "Briarhorn Hatchling", "Doomrubble", "Gill'dan", "Jeepers", "Killbot 9000", "N'Ruby", "Obsidian Warwhelp", } function R:OnCriteriaComplete(event, id) local timewalkingEncounterName = timewalkingCriteriaLUT[id] local timeRiftEncounterName = timeRiftCriteriaLUT[id] R:Debug("Detected achievement criteria completion: " .. tostring(id)) if timewalkingEncounterName then R:Debug("Completed criteria for Timewalking encounter: " .. tostring(timewalkingEncounterName)) local v = self.db.profile.groups.mounts["Reins of the Infinite Timereaver"] if v and type(v) == "table" and v.enabled ~= false and R:IsAttemptAllowed(v) then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end R:OutputAttempts(v) end end if timeRiftEncounterName then R:Debug("Completed criteria for Time Rift encounter: " .. timeRiftEncounterName) self:OnTimeRiftCompleted() end end function R:OnTimeRiftCompleted() if self.Caching:IsAlliance() then local itemName = "Reins of the Ravenous Black Gryphon" addAttemptForItem(itemName, "mounts") else local itemName = "Horn of the White War Wolf" addAttemptForItem(itemName, "mounts") end for _, itemName in ipairs(timeRiftMounts) do addAttemptForItem(itemName, "mounts") end for _, itemName in ipairs(timeRiftPets) do addAttemptForItem(itemName, "pets") end end ------------------------------------------------------------------------------------- -- Mouseover detection, currently used for Mysterious Camel Figurine as a special case ------------------------------------------------------------------------------------- function R:OnMouseOver(event) self.Profiling:StartTimer("EventHandlers.OnMouseOver") local guid = UnitGUID("mouseover") local npcid = self:GetNPCIDFromGUID(guid) Rarity:Debug("OnMouseOver") if not npcid then self.Profiling:EndTimer("EventHandlers.OnMouseOver") return end Rarity:Debug("UnitGUID: " .. tostring(npcid)) if npcid == 50409 or npcid == 50410 then if not Rarity.guids[guid] then Rarity.guids[guid] = true local v = self.db.profile.groups.mounts["Reins of the Grey Riding Camel"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end self.Profiling:EndTimer("EventHandlers.OnMouseOver") end function R:OnProfileChanged(event, database, newProfileKey) self:Debug("Profile changed. Reinitializing.") Rarity.Session:Cancel() local sessionTimer = Rarity.Session:GetTimer() if sessionTimer then self:CancelTimer(sessionTimer, true) end Rarity.Session:SetTimer(nil) self.db:RegisterDefaults(self.defaults) self:UpdateInterestingThings() self:OnCurrencyUpdate(event) self:ScanAllArch(event) Rarity.Collections:ScanExistingItems(event) self:ScanBags() Rarity.Tracking:FindTrackedItem() Rarity.GUI:UpdateText() self.db.profile.lastRevision = R.MINOR_VERSION end function R:OnChatCommand(input) if strlower(input) == "debug" then if self.db.profile.debugMode then self.db.profile.debugMode = false self:Print(L["Debug mode OFF"]) else self.db.profile.debugMode = true self:Print(L["Debug mode ON"]) end elseif strlower(input) == "dump" then self.ScrollingDebugMessageFrame:Toggle() elseif strlower(input) == "validate" then -- Verify the ItemDB self.Validation:ValidateItemDB() elseif strlower(input) == "mapinfo" then local mapID = C_Map.GetBestMapForUnit("player") local mapInfo = C_Map.GetMapInfo(mapID) local mapName = mapInfo and mapInfo.name or "Unknown" self:Print("Current map: " .. mapID .. " ~ " .. mapName) elseif strlower(input) == "purge" then -- TODO: This should be done automatically, no? self.Database:PurgeObsoleteEntries() elseif strlower(input) == "progress" then self.GUI:ToggleProgressBar() elseif strlower(input) == "test" then self.Testing:RunIntegrationTests() elseif strlower(input) == "profiling" then if self.db.profile.enableProfiling then self.db.profile.enableProfiling = false self:Print(L["Profiling OFF"]) else self.db.profile.enableProfiling = true self:Print(L["Profiling ON"]) end elseif strlower(input) == "tinspect" then -- TODO Document it? Rarity.Profiling:InspectAccumulatedTimes() else Rarity:TryShowOptionsUI() end end function R:OnItemFound(itemId, item) if item.found and not item.repeatable then return end local playerClass = select(2, UnitClass("player")) if item.disableForClass and item.disableForClass[playerClass] then Rarity:Debug(format("Ignoring OnItemFound trigger for item %s (disabled for class %s)", item.name, playerClass)) return end self:Debug("FOUND ITEM %d!", itemId) if item.attempts == nil then item.attempts = 1 end if item.lastAttempts == nil then item.lastAttempts = 0 end -- Hacky: If the item is unique and has 0 attempts, don't do this (if you really find a unique item on your first attempt, sorry) if item.unique and item.attempts - item.lastAttempts <= 1 then return end self:ShowFoundAlert(itemId, item.attempts - item.lastAttempts, item, item) if Rarity.Session:IsActive() then Rarity.Session:End() end item.realAttempts = item.attempts - item.lastAttempts item.lastAttempts = item.attempts item.enabled = false item.found = true item.totalFinds = (item.totalFinds or 0) + 1 if not item.finds then item.finds = {} end local count = 0 for k, v in pairs(item.finds) do count = count + 1 end table.insert(item.finds, { num = count + 1, totalAttempts = item.attempts, totalTime = item.time, attempts = item.realAttempts, time = (item.time or 0) - (item.lastTime or 0), }) item.lastTime = item.time Rarity.Tracking:Update(item) self:UpdateInterestingThings() if item.repeatable then self:ScheduleTimer(function() -- If this is a repeatable item, turn it back on in a few seconds. -- OnItemFound() gets called repeatedly when we get an item, so we need to lock it out for a few seconds. item.enabled = nil item.found = nil self:UpdateInterestingThings() Rarity.GUI:UpdateText() end, 5) end end local FISHING_DELAY = 22 function R:OnSpellcastSent(event, unit, target, castGUID, spellID) if unit ~= "player" then return end self.Profiling:StartTimer("EventHandlers.OnSpellcastSent") Rarity.foundTarget = false -- ga = "No" -- WTF is this? if Rarity.relevantSpells[spellID] then -- An entry exists for this spell in the LUT -> It's one that needs to be tracked Rarity:Debug( "Detected relevant spell: " .. tostring(spellID) .. " ~ " .. tostring(Rarity.relevantSpells[spellID]) ) Rarity.currentSpell = spellID Rarity.previousSpell = spellID if Rarity.relevantSpells[spellID] == "Fishing" or Rarity.relevantSpells[spellID] == "Opening" then self:Debug("Fishing or opening something") if Rarity.relevantSpells[spellID] == "Opening" then self:Debug("Opening detected") Rarity.isOpening = true else Rarity.isOpening = false end Rarity.isFishing = true if Rarity.fishingTimer then self:CancelTimer(Rarity.fishingTimer, true) end Rarity.fishingTimer = self:ScheduleTimer(Rarity.OnFishingEnded, FISHING_DELAY) self:GetWorldTarget() end else Rarity.previousSpell, Rarity.currentSpell = nil, nil end self.Profiling:EndTimer("EventHandlers.OnSpellcastSent") end function R:OnFishingEnded() R:Debug("You didn't loot anything from that fishing. Giving up.") Rarity.fishingTimer = nil Rarity.isFishing = false Rarity.isPool = false Rarity.isOpening = false end function R:OnLootFrameClosed(event) Rarity.previousSpell, Rarity.currentSpell = nil, nil Rarity.foundTarget = false self:ScheduleTimer(function() Rarity.lastNode = nil end, 1) end local tooltipLeftText1 = _G["GameTooltipTextLeft1"] local function stripColorCode(input) local output = input or "" output = gsub(output, "|c%x%x%x%x%x%x%x%x", "") output = gsub(output, "|r", "") return output end function R:OnCursorChanged(event) if Rarity.foundTarget then return end if MinimapCluster:IsMouseOver() then return end self.Profiling:StartTimer("EventHandlers.OnCursorChanged") local t = stripColorCode(tooltipLeftText1:GetText()) if self.miningnodes[t] or self.fishnodes[t] or self.opennodes[t] then Rarity.lastNode = t Rarity:Debug("OnCursorChanged found lastNode = " .. tostring(t)) end if Rarity.relevantSpells[Rarity.previousSpell] then self:GetWorldTarget() end self.Profiling:EndTimer("EventHandlers.OnCursorChanged") end -- Doesn't really belong here, but no idea where to put it right now. Later... function R:GetWorldTarget() if Rarity.foundTarget or not Rarity.relevantSpells[Rarity.currentSpell] then return end if MinimapCluster:IsMouseOver() then return end self.Profiling:StartTimer("EventHandlers.GetWorldTarget") local t = tooltipLeftText1:GetText() Rarity:Debug("Getting world target " .. tostring(t)) if t and Rarity.previousSpell and t ~= Rarity.previousSpell and R.fishnodes[t] then self:Debug("------YOU HAVE STARTED FISHING A NODE ------") Rarity.isFishing = true Rarity.isPool = true if Rarity.fishingTimer then self:CancelTimer(Rarity.fishingTimer, true) end Rarity.fishingTimer = self:ScheduleTimer(Rarity.OnFishingEnded, FISHING_DELAY) Rarity.foundTarget = true end self.Profiling:EndTimer("EventHandlers.GetWorldTarget") end function R:OnSpellcastStopped(event, unit) if unit ~= "player" then return end self.Profiling:StartTimer("EventHandlers.OnSpellcastStopped") if Rarity.relevantSpells[Rarity.previousSpell] then self:GetWorldTarget() end Rarity.previousSpell, Rarity.currentSpell = Rarity.currentSpell, Rarity.currentSpell self.Profiling:EndTimer("EventHandlers.OnSpellcastStopped") end function R:OnSpellcastFailed(event, unit) if unit ~= "player" then return end Rarity.previousSpell, Rarity.currentSpell = nil, nil end ------------------------------------------------------------------------------------- -- Something in your bags changed. -- -- This is used for a couple things. First, for boss drops that require a group, you may not have obtained the item even if it dropped from the boss. -- Therefore, we only say you obtained it when it appears in your inventory. Secondly, this is useful as a second line of defense in case -- you somehow obtain an item without us noticing it. This event fires a lot, so we need to be fast. -- -- We also store how many of every item you have on you at the moment. If we notice an item decreasing in quantity, and it's something we care -- about, you just used an item or opened a container. -- -- This event is bucketed because it tends to fire tons of times in a row rapidly, leading to innaccurate results. ------------------------------------------------------------------------------------- local table_wipe = table.wipe -- Nonstandard, but always exists in WOW's Lua environment function R:BackUpInventoryItemAmounts() table_wipe(Rarity.tempbagitems) for itemID, inventoryAmount in pairs(Rarity.bagitems) do Rarity.tempbagitems[itemID] = inventoryAmount end end function R:ProcessContainerItems() for k, v in pairs(Rarity.tempbagitems) do if (Rarity.bagitems[k] or 0) < (Rarity.tempbagitems[k] or 0) then -- An inventory item went down in count or disappeared if Rarity.used[k] then -- It's an item we care about -- Scan through the whole item database now to find all items that could want this for _k, _v in pairs(self.db.profile.groups) do if type(_v) == "table" then for kk, vv in pairs(_v) do if type(vv) == "table" then if vv.enabled ~= false then if vv.method == CONSTANTS.DETECTION_METHODS.USE and vv.items ~= nil and type(vv.items) == "table" then local isHordePlayer = R.Caching:IsHorde() local canPlayerObtainFactionSpecificItem = not ( vv.requiresHorde and not isHordePlayer ) or (vv.requiresAlliance and isHordePlayer) if canPlayerObtainFactionSpecificItem then for kkk, vvv in pairs(vv.items) do if vvv == k then local i = vv if i.attempts == nil then i.attempts = 1 else i.attempts = i.attempts + 1 end self:OutputAttempts(i) end end end end end end end end end -- End scan through all items end end end end function R:ProcessInventoryItems() self.Profiling:StartTimer("EventHandlers.ProcessInventoryItems") for itemID, _ in pairs(Rarity.bagitems) do self:ProcessCollectionItem(itemID) self:ProcessOtherItem(itemID) end self.Profiling:EndTimer("EventHandlers.ProcessInventoryItems") end function R:ProcessCollectionItem(itemID) if not itemID then return end local item = Rarity.items[itemID] if not item then return end -- Handle collection items if not self:IsCollectionItem(item) then return end local inventoryItemCount = R:GetInventoryItemCount(itemID) -- Our items hashtable only saves one item for this collected item, so we have to scan to find them all now. -- Earlier, we pre-built a list of just the items that are COLLECTION items to save some time here. for collectionItemID, collectionItem in pairs(Rarity.collection_items) do -- This item is a collection of several items; add them all up and check for attempts if self:HasMultipleCollectionItems(collectionItem) then self:ProcessCollectionItemAggregate(collectionItem) else self:ProcessCollectionItemSingle(collectionItem, itemID) end end end function R:HasMultipleCollectionItems(item) return type(item.collectedItemId) == "table" end -- TODO: DRY function R:IsCollectionItem(item) return item.method == CONSTANTS.DETECTION_METHODS.COLLECTION end function R:ProcessOtherItem(itemID) if not itemID then return end local item = Rarity.items[itemID] if not item then return end local amountIncreasedSinceLastScan = (Rarity.bagitems[itemID] or 0) > (Rarity.tempbagitems[itemID] or 0) if amountIncreasedSinceLastScan then -- An inventory item went up in count if item and item.enabled ~= false and not self:IsCollectionItem(item) then self:OnItemFound(itemID, item) end end end function R:GetInventoryItemCount(itemID) local inventoryItemCount = (Rarity.bagitems[itemID] or 0) return inventoryItemCount end -- Still incomprehensible, but I'll leave it for now function R:ProcessCollectionItemSingle(collectionItem, itemID) local inventoryItemCount = self:GetInventoryItemCount(itemID) local item = Rarity.items[itemID] if collectionItem.enabled and ( collectionItem.collectedItemId == item.collectedItemId or table_contains(item.collectedItemId, collectionItem.collectedItemId) ) then local originalCount = (collectionItem.attempts or 0) local goal = (collectionItem.chance or 100) collectionItem.lastAttempts = 0 if collectionItem.attempts ~= inventoryItemCount then collectionItem.attempts = inventoryItemCount end if originalCount < inventoryItemCount and originalCount < goal and inventoryItemCount >= goal then self:OnItemFound(collectionItem.itemId, collectionItem) elseif originalCount < inventoryItemCount then self:OutputAttempts(collectionItem) end end end -- Still incomprehensible, but I'll leave it for now function R:ProcessCollectionItemAggregate(collectionItem) if collectionItem.enabled ~= false then local total = 0 local originalCount = (collectionItem.attempts or 0) local goal = (collectionItem.chance or 100) for kkk, vvv in pairs(collectionItem.collectedItemId) do vvv = tonumber(vvv) -- It's stored as string, but we expect numbers... if (Rarity.bagitems[vvv] or 0) > 0 then total = total + Rarity.bagitems[vvv] end end if total > originalCount then self:Debug("Total is > original count, overriding current attempts") collectionItem.attempts = total if originalCount < goal and total >= goal then self:Debug("Triggering OnItemFound since we just reached the goal") self:OnItemFound(collectionItem.itemId, collectionItem) elseif total > originalCount then self:Debug("Triggering OutputAttempts since we gained one item, but didn't reach the goal") self:OutputAttempts(collectionItem) end end end end -- It's an abomination, but without tests I'm not refactoring this any further function R:OnBagUpdate() -- Save a copy of your bags before this event R:BackUpInventoryItemAmounts() -- Get a list of the items you have now, alerting if we find anything we're looking for self:ScanBags() -- I assume that if there's even the feintest possibility of items being removed, we don't want to risk it? local shouldSkipBagUpdate = ( Rarity.isBankOpen or Rarity.isGuildBankOpen or Rarity.isAuctionHouseOpen or Rarity.isTradeWindowOpen or Rarity.isTradeskillOpen or Rarity.isMailboxOpen ) if shouldSkipBagUpdate then return end -- Check for a decrease in quantity of any items we're watching for R:ProcessContainerItems() -- Check for an increase in quantity of any items we're watching for R:ProcessInventoryItems() end function R:OnResearchArtifactComplete(event, _) self:ScanArchFragments(event) end function R:OnEvent(event, ...) self.Profiling:StartTimer("EventHandlers.OnEvent") if event == "TRADE_SKILL_SHOW" then Rarity.isTradeskillOpen = true elseif event == "TRADE_SKILL_CLOSE" then Rarity.isTradeskillOpen = false elseif event == "UPDATE_INSTANCE_INFO" then -- Instance lock info updated self:ScanInstanceLocks(event) elseif event == "LFG_UPDATE_RANDOM_INFO" then self:ScanInstanceLocks(event) elseif event == "CALENDAR_UPDATE_EVENT_LIST" then -- Calendar updated self:ScanCalendar(event) elseif event == "TOYS_UPDATED" then -- Toy box updated Rarity.Collections:ScanExistingItems(event) elseif event == "COMPANION_UPDATE" then -- Pets updated Rarity.Collections:ScanExistingItems(event) elseif event == "PLAYER_LOGOUT" then -- Logging out; end any open session if Rarity.Session:IsActive() then Rarity.Session:End() end end self.Profiling:EndTimer("EventHandlers.OnEvent") end ------------------------------------------------------------------------------------- -- You opened a loot window on a corpse or fishing node ------------------------------------------------------------------------------------- function R:OnLootReady(event, ...) do self:Debug("LOOT_READY with target: " .. (UnitGUID("target") or "NO TARGET")) -- Two LOOT_READY events may trigger when the loot window opens, in which case this prevents double counting if self.Session:IsLocked() then self:Debug("Session is locked; ignoring this LOOT_READY event") return end self.Session:Lock() if Rarity.isBankOpen then Rarity:Debug("Ignoring this LOOT_READY event (bank is open)") return end if Rarity.isGuildBankOpen then Rarity:Debug("Ignoring this LOOT_READY event (guild bank is open)") return end if Rarity.isMailboxOpen then Rarity:Debug("Ignoring this LOOT_READY event (mailbox is open)") return end if Rarity.isAuctionHouseOpen then Rarity:Debug("Ignoring this LOOT_READY event (auction house is open)") return end if Rarity.isTradeWindowOpen then Rarity:Debug("Ignoring this LOOT_READY event (trade window is open)") return end local zone = GetRealZoneText() local subzone = GetSubZoneText() local zone_t = LibStub("LibBabble-Zone-3.0"):GetReverseLookupTable()[zone] local subzone_t = LibStub("LibBabble-SubZone-3.0"):GetReverseLookupTable()[subzone] if Rarity.isFishing and Rarity.isOpening then self:Debug("Opened something") end if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode then self:Debug("Opened a node: " .. Rarity.lastNode) end -- Handle opening Crane Nest if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Crane Nest"]) then Rarity:Debug("Detected Opening on " .. L["Crane Nest"] .. " (method = SPECIAL)") local v = self.db.profile.groups.pets["Azure Crane Chick"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end -- Handle opening Timeless Chest if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Timeless Chest"]) then Rarity:Debug("Detected Opening on " .. L["Timeless Chest"] .. " (method = SPECIAL)") local v = self.db.profile.groups.pets["Bonkers"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end -- Handle opening Snow Mound if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Snow Mound"]) and GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.FROSTFIRE_RIDGE then -- Make sure we're in Frostfire Ridge (there are Snow Mounds in other zones, particularly Ulduar in the Hodir room) Rarity:Debug("Detected Opening on " .. L["Snow Mound"] .. " (method = SPECIAL)") local v = self.db.profile.groups.pets["Grumpling"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end -- Handle opening Curious Wyrmtongue Cache if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Curious Wyrmtongue Cache"]) then local names = { "Scraps", "Pilfered Sweeper" } Rarity:Debug("Detected Opening on " .. L["Curious Wyrmtongue Cache"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Glimmering Chest if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Glimmering Chest"]) then local names = { "Sandclaw Nestseeker" } Rarity:Debug("Detected Opening on " .. L["Glimmering Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Penitence of Purity (Shadowlands Kyrian only chest) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Penitence of Purity"]) then local names = { "Phalynx of Humility" } Rarity:Debug("Detected Opening on " .. L["Penitence of Purity"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Silver Strongbox & Gilded Chest (Bastion, Shadowlands nodes for Acrobatic Steward (toy) and Gilded Wader (pet)) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Silver Strongbox"] or Rarity.lastNode == L["Gilded Chest"]) then local names = { "Acrobatic Steward", "Gilded Wader" } Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Broken Bell & Skyward Bell (Shadowlands, Bastion nodes for Soothing Vesper (toy) & Gilded Wader (pet)) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Broken Bell"] or Rarity.lastNode == L["Skyward Bell"]) then local names = { "Soothing Vesper", "Gilded Wader" } Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Cache of the Ascended (Shadowlands, Bastion mount cache) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Cache of the Ascended"]) then local names = { "Ascended Skymane" } Rarity:Debug("Detected Opening on " .. L["Cache of the Ascended"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Slime-Coated Crate (Shadowlands, Maldraxxus crate for Kevin's Party Supplies (toy) & Bubbling Pustule (pet)) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Slime-Coated Crate"]) then local names = { "Kevin's Party Supplies", "Bubbling Pustule" } Rarity:Debug("Detected Opening on " .. L["Slime-Coated Crate"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Sprouting Growth (Shadowlands, Maldraxxus crate for Skittering Venomspitter pet) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Sprouting Growth"]) then local names = { "Skittering Venomspitter" } Rarity:Debug("Detected Opening on " .. L["Sprouting Growth"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Stewart's Stewpendous Stew (Shadowlands, Bastion crate for Silvershell Snapper pet) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Stewart's Stewpendous Stew"]) then local names = { "Silvershell Snapper" } Rarity:Debug("Detected Opening on " .. L["Stewart's Stewpendous Stew"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Bleakwood Chest (Shadowlands, Revendreth chest for Trapped Stonefiend pet) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Bleakwood Chest"]) then local names = { "Trapped Stonefiend" } Rarity:Debug("Detected Opening on " .. L["Bleakwood Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Blackhound Cache (Shadowlands, Maldraxxus cache for Battlecry of Krexus - Necrolord toy) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Blackhound Cache"]) then local names = { "Battlecry of Krexus" } Rarity:Debug("Detected Opening on " .. L["Blackhound Cache"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Secret Treasure (Shadowlands, Revendreth chest for Soullocked Sinstone pet) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Secret Treasure"]) then local names = { "Soullocked Sinstone" } Rarity:Debug("Detected Opening on " .. L["Secret Treasure"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Forgotten Chest (Shadowlands, Revendreth chest for Stony's Infused Ruby pet and Silessa's Battle Harness mount) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Forgotten Chest"]) and GetBestMapForUnit("player") ~= CONSTANTS.UIMAPIDS.STORMSONG_VALLEY -- Chest with the same name in Stormsong Valley then local names = { "Stony's Infused Ruby", "Silessa's Battle Harness" } Rarity:Debug("Detected Opening on " .. L["Forgotten Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.pets[name] or self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Cache of Eyes (Shadowlands, Maldraxxus chest for Luminous Webspinner pet) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Cache of Eyes"]) then local names = { "Luminous Webspinner" } Rarity:Debug("Detected Opening on " .. L["Cache of Eyes"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening lots of various chests for Gilded Wader (pet). local nodesGildedWader = { [L["Gift of Thenios"]] = true, [L["Hidden Hoard"]] = true, [L["Memorial Offerings"]] = true, [L["Treasure of Courage"]] = true, } local isRelevantNode = false if Rarity.lastNode then isRelevantNode = nodesGildedWader[Rarity.lastNode] end if Rarity.isFishing and Rarity.isOpening and isRelevantNode then local names = { "Gilded Wader" } Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Zovaal's Vault (The Maw, Shadowlands treasure for Personal Ball and Chain & Jailer's Cage if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Zovaal's Vault"]) then local names = { "Personal Ball and Chain", "Jailer's Cage" } Rarity:Debug("Detected Opening on " .. L["Zovaal's Vault"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Pile of Coins if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Pile of Coins"]) then local names = { "Armored Vaultbot" } Rarity:Debug("Detected Opening on " .. L["Pile of Coins"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.pets[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Glimmering Treasure Chest if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Glimmering Treasure Chest"]) and select(8, GetInstanceInfo()) == 1626 then -- Player is in Withered Army scenario and looted the reward chest local bigChest = false for _, slot in pairs(GetLootInfo()) do if slot.item == L["Ancient Mana"] and slot.quantity == 100 then bigChest = true end end if bigChest == true then self:Debug("Detected " .. Rarity.lastNode .. ": Adding toy drop attempts") local names = { "Arcano-Shower", "Displacer Meditation Stone", "Kaldorei Light Globe", "Unstable Powder Box", "Wisp in a Bottle", "Ley Spider Eggs", } for _, name in pairs(names) do local v = self.db.profile.groups.items[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end local v = self.db.profile.groups.mounts["Torn Invitation"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Mawsworn Supply Chest"]) then local names = { "Spectral Mawrat's Tail" } Rarity:Debug("Detected Opening on " .. L["Mawsworn Supply Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] or self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Sandworn Chest"]) then local names = { "Makaris's Satchel of Mines" } Rarity:Debug("Detected Opening on " .. L["Sandworn Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening Expedition Scout's Pack (Verdant Skitterfly mount in Dragonflight) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Expedition Scout's Pack"]) then local names = { "Verdant Skitterfly" } Rarity:Debug("Detected Opening on " .. L["Expedition Scout's Pack"] .. " (method = SPECIAL)") -- This mount has a prerequisite to drop. Renown 25 with Dragonscale Expedition if GetCurrentRenownLevel(CONSTANTS.FACTION_IDS.DRAGONSCALE_EXPEDITION) >= 25 then for _, name in pairs(names) do local v = self.db.profile.groups.mounts[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end end if Rarity.isOpening and Rarity.lastNode and Rarity.lastNode == L["Awakened Cache"] then Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)") addAttemptForItem("Machine Defense Unit 1-11", "mounts") end -- Handle opening Opera Chest (Holoviewers) if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and (Rarity.lastNode == L["Opera Chest"]) then local names = { "Holoviewer: The Timeless One", "Holoviewer: The Lady of Dreams" } Rarity:Debug("Detected Opening on " .. L["Opera Chest"] .. " (method = SPECIAL)") for _, name in pairs(names) do local v = self.db.profile.groups.items[name] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end end -- Handle opening various chests in Zskera Vaults local vaultPets = { "Bunbo", "Berylmane", "Brightfeather", } local vaultToys = { "Obsidian Battle Horn", } local vaultChests = { [L["Chain-Bound Strongbox"]] = true, [L["Crystal Basket"]] = true, [L["Hardened Chest"]] = true, [L["Mindless Slime"]] = true, [L["Searing Chest"]] = true, [L["Hardened Strongbox"]] = true, [L["Cart of Crushed Stone"]] = true, [L["Hardshell Chest"]] = true, [L["Titan Coffer"]] = true, [L["Void-Bound Strongbox"]] = true, [L["Inert Goo"]] = true, [L["Mysterious Chest"]] = true, [L["Supply Trunk"]] = true, [L["Shattered Crystals"]] = true, [L["Chest of Ice"]] = true, [L["Forgotten Lockbox"]] = true, [L["Slimy Goo"]] = true, [L["Wind-Bound Strongbox"]] = true, [L["Spun Webs"]] = true, [L["Obsidian Grand Cache"]] = true, [L["Frozen Coffer"]] = true, } if Rarity.isFishing and Rarity.isOpening and Rarity.lastNode and vaultChests[Rarity.lastNode] then Rarity:Debug("Detected Opening on " .. Rarity.lastNode .. " (method = SPECIAL)") for _, itemName in ipairs(vaultToys) do addAttemptForItem(itemName, "items") end for _, itemName in ipairs(vaultPets) do addAttemptForItem(itemName, "pets") end end -- HANDLE FISHING if Rarity.isFishing and Rarity.isOpening == false then if Rarity.isPool then self:Debug("Successfully fished from a pool") else self:Debug("Successfully fished") end if Rarity.fishzones[tostring(GetBestMapForUnit("player"))] or Rarity.fishzones[zone] or Rarity.fishzones[subzone] or Rarity.fishzones[zone_t] or Rarity.fishzones[subzone_t] then -- We're interested in fishing in this zone; let's find the item(s) involved Rarity:Debug("We're interested in fishing in this zone; let's find the item(s) involved") for k, v in pairs(self.db.profile.groups) do if type(v) == "table" then for kk, vv in pairs(v) do if type(vv) == "table" then if vv.enabled ~= false then local found = false if vv.method == CONSTANTS.DETECTION_METHODS.FISHING and vv.zones ~= nil and type(vv.zones) == "table" then for kkk, vvv in pairs(vv.zones) do if vvv == tostring(GetBestMapForUnit("player")) or vvv == zone or vvv == lbz[zone] or vvv == subzone or vvv == lbsz[subzone] or vvv == zone_t or vvv == subzone_t or vvv == lbz[zone_t] or vvv == subzone or vvv == lbsz[subzone_t] then if (vv.requiresPool and Rarity.isPool) or not vv.requiresPool then Rarity:Debug( "Found interesting item for this zone: " .. tostring(vv.name) ) found = true end end end end if vv.excludedMaps and type(vv.excludedMaps) == "table" and vv.excludedMaps[GetBestMapForUnit("player")] then Rarity:Debug( "The current map is excluded for item: " .. tostring(vv.name) .. ". Attempts will not be counted" ) found = false end if found then if self:IsAttemptAllowed(vv) then if vv.attempts == nil then vv.attempts = 1 else vv.attempts = vv.attempts + 1 end self:OutputAttempts(vv) end end end end end end end end end if Rarity.fishingTimer then self:CancelTimer(Rarity.fishingTimer, true) end Rarity.fishingTimer = nil Rarity.isFishing = false Rarity.isPool = false -- Handle Disgusting Vat Fishing if -- There's no UNIT_SPELLCAST_SENT, so this will have to do Rarity.lastNode == L["Disgusting Vat"] then Rarity:OnDisgustingVatFished() end if Rarity.isOpening and Rarity.lastNode == L["Chest of Massive Gains"] then Rarity:OnChestOfMassiveGainsOpened() end -- Handle mining Elementium if Rarity.relevantSpells[Rarity.previousSpell] == "Mining" and (Rarity.lastNode == L["Elementium Vein"] or Rarity.lastNode == L["Rich Elementium Vein"]) then Rarity:Debug("Detected Mining on " .. Rarity.lastNode .. " (method = SPECIAL)") local v = self.db.profile.groups.pets["Elementium Geode"] if v and type(v) == "table" and v.enabled ~= false then if v.attempts == nil then v.attempts = 1 else v.attempts = v.attempts + 1 end self:OutputAttempts(v) end end -- Handle skinning on Argus (Fossorial Bile Larva) if ( Rarity.relevantSpells[Rarity.previousSpell] == "Skinning" or Rarity.relevantSpells[Rarity.previousSpell] == "Mother's Skinning Knife" ) -- Skinned something and ( GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.KROKUUN or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.MACAREE or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.ANTORAN_WASTES ) then -- Player is on Argus -> Can obtain the pet from skinning creatures Rarity:Debug( "Detected skinning on Argus - Can obtain " .. L["Fossorial Bile Larva"] .. " (method = SPECIAL)" ) local v = self.db.profile.groups.pets["Fossorial Bile Larva"] if v and type(v) == "table" and v.enabled ~= false then -- Add an attempt v.attempts = v.attempts ~= nil and v.attempts + 1 or 1 -- Defaults to 1 if this is the first attempt self:OutputAttempts(v) end end if Rarity.isOpening and Rarity.lastNode == L["Dreamseed Cache"] then Rarity:OnDreamseedCacheOpened() end -- Handle herb gathering on Argus (Fel Lasher) if Rarity.relevantSpells[Rarity.previousSpell] == "Herb Gathering" -- Gathered a herbalism node and ( GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.KROKUUN or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.MACAREE or GetBestMapForUnit("player") == CONSTANTS.UIMAPIDS.ANTORAN_WASTES ) then -- Player is on Argus -> Can obtain the pet from gathering herbalism nodes Rarity:Debug("Detected herb gathering on Argus - Can obtain " .. L["Fel Lasher"] .. " (method = SPECIAL)") local v = self.db.profile.groups.pets["Fel Lasher"] if v and type(v) == "table" and v.enabled ~= false then -- Add an attempt v.attempts = v.attempts ~= nil and v.attempts + 1 or 1 -- Defaults to 1 if this is the first attempt self:OutputAttempts(v) end end -- HANDLE NORMAL NPC LOOTING local numItems = GetNumLootItems() -- Legacy support for pre-5.0 single-target looting local guid = UnitGUID("target") local name = UnitName("target") if not name or not guid then return end -- No target when looting if not UnitCanAttack("player", "target") then return end -- You targeted something you can't attack if UnitIsPlayer("target") then return end -- You targetted a player -- You're looting something that's alive -- this is only done for pickpocketing local requiresPickpocket = false if not UnitIsDead("target") then requiresPickpocket = true end -- Disallow "minus" NPCs; nothing good drops from them if UnitClassification(guid) == "minus" then return end -- (This doesn't actually work currently; UnitClassification needs a unit, not a GUID) local numChecked = 0 self:Debug(numItems .. " slot(s) to loot") for slotID = 1, numItems, 1 do -- Loop through all loot slots (for AoE looting) local guidlist if GetLootSourceInfo then guidlist = { GetLootSourceInfo(slotID) } else guidlist = { guid } end local guidIndex for k, v in pairs(guidlist) do -- Loop through all NPC Rarity.guids being looted (will be 1 for single-target looting pre-5.0) guid = v if guid and type(guid) == "string" then self:Debug("Checking NPC guid (" .. (numChecked + 1) .. "): " .. guid) self:CheckNpcInterest( guid, zone, subzone, zone_t, subzone_t, Rarity.currentSpell, requiresPickpocket ) -- Decide if we should increment an attempt count for this NPC numChecked = numChecked + 1 -- else -- --self:Debug("Didn't check guid: "..guid or "nil") end -- Loop through all NPC GUIDs being looted (will be 1 for single-target looting pre-5.0) end -- Haven't seen this corpse yet end -- Loop through all loot slots (for AoE looting) -- If we failed to scan anything, scan the current target if numChecked <= 0 then self:CheckNpcInterest(UnitGUID("target"), zone, subzone, zone_t, subzone_t, Rarity.currentSpell) end -- Scan the loot to see if we found something we're looking for for slotID = 1, numItems, 1 do local _, _, qty = GetLootSlotInfo(slotID) if (qty or 0) > 0 then -- Coins have quantity of 0, so skip those local itemLink = GetLootSlotLink(slotID) if itemLink then local _, itemId = strsplit(":", itemLink) itemId = tonumber(itemId) if Rarity.items[itemId] ~= nil and Rarity.items[itemId].method ~= CONSTANTS.DETECTION_METHODS.COLLECTION then self:OnItemFound(itemId, Rarity.items[itemId]) end end end end if requiresPickpocket then -- Pick Pocket triggers the same loot events, but it shouldn't prevent kills from counting afterwards Rarity.guids[guid] = false end end end function Rarity:OnChestOfMassiveGainsOpened() Rarity:Debug("Detected Opening on Chest of Massive Gains") local hasOpenedChestToday = IsQuestFlaggedCompleted(75325) if hasOpenedChestToday then Rarity:Debug("Skipping this attempt (loot lockout for Chest of Massive Gains is active)") return end local wasRequiredAuraFoundOnPlayer = false AuraUtil.ForEachAura("player", "HELPFUL", nil, function(_, _, _, _, _, _, _, _, _, spellID) if spellID == CONSTANTS.AURAS.ROCKS_ON_THE_ROCKS then wasRequiredAuraFoundOnPlayer = true end end) if not wasRequiredAuraFoundOnPlayer then Rarity:Debug(format("Required aura %s NOT found on player", L["Rocks on the Rocks"])) return end addAttemptForItem("Brul", "pets") end function Rarity:OnDisgustingVatFished() local hasFishedEmmahThisWeek = IsQuestFlaggedCompleted(75488) if hasFishedEmmahThisWeek then self:Debug("Skipping this fishing attempt (loot lockout for Emmah is active)") return end self:Debug("Detected fishing on Disgusting Vat (method = SPECIAL)") addAttemptForItem("Emmah", "pets") end local dreamseedMounts = { "Reins of the Winter Night Dreamsaber", "Reins of the Snowfluff Dreamtalon", "Reins of the Evening Sun Dreamsaber", "Reins of the Blossoming Dreamstag", "Reins of the Springtide Dreamtalon", "Reins of the Morning Flourish Dreamsaber", "Reins of the Rekindled Dreamstag", } function Rarity:OnDreamseedCacheOpened() Rarity:Debug("Detected Opening on Dreamseed Cache") for _, mount in ipairs(dreamseedMounts) do addAttemptForItem(mount, "mounts") end end function R:OnPlayerInteractionFrameShow(event, playerInteractionTypeID) if playerInteractionTypeID == Enum.PlayerInteractionType.Banker then Rarity.isBankOpen = true elseif playerInteractionTypeID == Enum.PlayerInteractionType.GuildBanker then Rarity.isGuildBankOpen = true elseif playerInteractionTypeID == Enum.PlayerInteractionType.Auctioneer then Rarity.isAuctionHouseOpen = true elseif playerInteractionTypeID == Enum.PlayerInteractionType.TradePartner then Rarity.isTradeWindowOpen = true elseif playerInteractionTypeID == Enum.PlayerInteractionType.MailInfo then Rarity.isMailboxOpen = true end end function R:OnPlayerInteractionFrameHide(event, playerInteractionTypeID) if playerInteractionTypeID == Enum.PlayerInteractionType.Banker then Rarity.isBankOpen = false elseif playerInteractionTypeID == Enum.PlayerInteractionType.GuildBanker then Rarity.isGuildBankOpen = false elseif playerInteractionTypeID == Enum.PlayerInteractionType.Auctioneer then Rarity.isAuctionHouseOpen = false elseif playerInteractionTypeID == Enum.PlayerInteractionType.TradePartner then Rarity.isTradeWindowOpen = false elseif playerInteractionTypeID == Enum.PlayerInteractionType.MailInfo then Rarity.isMailboxOpen = false end end Rarity.EventHandlers = EventHandlers return EventHandlers