You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1882 lines
65 KiB

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 = LoadAddOn
local GetBestMapForUnit = _G.C_Map.GetBestMapForUnit
local GetMapInfo = _G.C_Map.GetMapInfo
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.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
local GetArchaeologyRaceInfo = _G.GetArchaeologyRaceInfo
local GetStatistic = _G.GetStatistic
local GetLootSourceInfo = _G.GetLootSourceInfo
local C_Timer = _G.C_Timer
local IsSpellKnown = _G.IsSpellKnown
-- Addon APIs
local DebugCache = Rarity.Utils.DebugCache
function EventHandlers:Register()
self = Rarity
self:UnregisterAllEvents()
self:RegisterBucketEvent("BAG_UPDATE", 0.5, "OnBagUpdate")
self:RegisterEvent("LOOT_READY", "OnEvent")
self:RegisterEvent("CURRENCY_DISPLAY_UPDATE", "OnCurrencyUpdate")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", "OnCombat") -- Used to detect boss kills that we didn't solo
self:RegisterEvent("BANKFRAME_OPENED", "OnEvent")
self:RegisterEvent("BANKFRAME_CLOSED", "OnEvent")
self:RegisterEvent("GUILDBANKFRAME_OPENED", "OnEvent")
self:RegisterEvent("GUILDBANKFRAME_CLOSED", "OnEvent")
self:RegisterEvent("MAIL_CLOSED", "OnEvent")
self:RegisterEvent("MAIL_SHOW", "OnEvent")
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("RESEARCH_ARTIFACT_HISTORY_READY", "ScanAllArch")
self:RegisterEvent("PLAYER_LOGOUT", "OnEvent")
self:RegisterEvent("AUCTION_HOUSE_CLOSED", "OnEvent")
self:RegisterEvent("AUCTION_HOUSE_SHOW", "OnEvent")
self:RegisterEvent("TRADE_CLOSED", "OnEvent")
self:RegisterEvent("TRADE_SHOW", "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")
self:RegisterEvent("SHOW_LOOT_TOAST", "OnShowLootToast")
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")
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)
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
-- 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 = 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 archaeology projects were solved
self:ScanArchFragments(event)
-- Check if any coins were used
for k, v in pairs(self.coins) do
local currency = GetCurrencyInfo(k)
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 = {
[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)
}
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()
-- 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")
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
if v.enabled ~= false and v.method == CONSTANTS.DETECTION_METHODS.BOSS 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
end
-- Handle quest turnins: Only used to detect world quests used for outdoor world bosses. It's not ideal, but probably more reliable than the loot lockout quest (which may or may not already be completed when the UNIT_DIED event is fired)
local worldBossQuests = {
[52196] = "Slightly Damp Pile of Fur", -- Dunegorger Kraulok
}
function R:OnQuestTurnedIn(event, questID, experience, money)
self:Debug(
"OnQuestTurnedIn triggered with ID = " .. questID .. ", experience = " .. experience .. ", money = " .. money
)
local relevantItem = worldBossQuests[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",
---- Not yet implemented?
-- "Regenerating Banana Bunch", -- NYI as of 18/01/19
-- "Yaungol Oil Stove", -- NYI as of 18/01/19
-- "Jinyu Light Globe", -- NYI as of 18/01/19
-- 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
-------------------------------------------------------------------------------------
-- Criteria in a dungeon completed, currently used for Reins of the Infinite Timereaver detection as a special case
-------------------------------------------------------------------------------------
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
}
function R:OnCriteriaComplete(event, id)
local encounterName = timewalkingCriteriaLUT[id]
R:Debug("Detected achievement criteria completion: " .. tostring(id))
if encounterName then -- Is an encounter that can't otherwise be detected
-- (due to the mount dropping from an object, and not a lootable NPC)
R:Debug("Completed criteria for relevant encounter: " .. tostring(encounterName))
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
end
-------------------------------------------------------------------------------------
-- Mouseover detection, currently used for Mysterious Camel Figurine as a special case
-------------------------------------------------------------------------------------
function R:OnMouseOver(event)
local guid = UnitGUID("mouseover")
local npcid = self:GetNPCIDFromGUID(guid)
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
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
local numMessages = 50 -- Hardcoded is meh, but it should suffice for the time being
DebugCache:PrintMessages(numMessages)
elseif strlower(input) == "validate" then -- Verify the ItemDB
self.Validation:ValidateItemDB()
elseif strlower(input) == "purge" then -- TODO: This should be done automatically, no?
self.Database:PurgeObsoleteEntries()
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
LoadAddOn("Rarity_Options")
if R.optionsFrame then
-- Thanks, Blizzard (https://www.wowinterface.com/forums/showthread.php?t=54599)
InterfaceOptionsFrame_OpenToCategory(R.optionsFrame)
InterfaceOptionsFrame_OpenToCategory(R.optionsFrame)
else
self:Print(L["The Rarity Options module has been disabled. Log out and enable it from your add-ons menu."])
end
end
end
function R:OnItemFound(itemId, item)
if item.found and not item.repeatable then
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
Rarity.foundTarget = false
-- ga = "No" -- WTF is this?
Rarity:Debug(
"Detected UNIT_SPELLCAST_SENT for unit = player, spellID = "
.. tostring(spellID)
.. ", castGUID = "
.. tostring(castGUID)
.. ", target = "
.. tostring(target)
) -- TODO: Remove?
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
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()
R:Debug("Setting lastNode to nil")
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
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
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
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
end
function R:OnSpellcastStopped(event, unit)
if unit ~= "player" then
return
end
if Rarity.relevantSpells[Rarity.previousSpell] then
self:GetWorldTarget()
end
Rarity.previousSpell, Rarity.currentSpell = Rarity.currentSpell, Rarity.currentSpell
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
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 scan through all items
end
end
end
end
function R:ProcessInventoryItems()
for itemID, currentInventoryAmount in pairs(Rarity.bagitems) do
self:Debug(format("Processing inventory item %s (currentInventoryAmount: %d)", itemID, currentInventoryAmount))
-- It's still really bad, but a major rework is probably too risky
self:ProcessCollectionItem(itemID)
self:ProcessOtherItem(itemID)
end
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
self:Debug(format("Processed item %s is something we're tracking", itemID))
if not self:IsCollectionItem(item) then
return
end
self:Debug("Processed item is a COLLECTION item we're tracking")
local inventoryItemCount = R:GetInventoryItemCount(itemID)
self:Debug(format("Processing collection item with inventoryItemCount %d", inventoryItemCount))
-- 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
self:Debug(format("Checking for new attempts at COLLECTION item %s", collectionItem.name))
-- This item is a collection of several items; add them all up and check for attempts
if self:HasMultipleCollectionItems(collectionItem) then
self:Debug(format("Processing aggregate collection item %s", collectionItem.name))
self:ProcessCollectionItemAggregate(collectionItem)
else
self:Debug(format("Processing single collection item %s", collectionItem.name))
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)
self:Debug(format("Aggregate with total %d, originalCount %d, goal %d", total, originalCount, goal))
for kkk, vvv in pairs(collectionItem.collectedItemId) do
vvv = tonumber(vvv) -- It's stored as string, but we expect numbers...
self:Debug(format("Adding inventoryAmount for item %d (%s)", kkk, vvv))
if (Rarity.bagitems[vvv] or 0) > 0 then
total = total + Rarity.bagitems[vvv]
self:Debug(format("Found %d of these in bags, new total is %d", Rarity.bagitems[vvv], total))
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()
self:Debug("BAG_UPDATE")
-- 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
--[[
OBTAIN DETECTION ---------------------------------------------------------------------------------------------------------
-- Some easy, some fairly arcane methods to detect when we've obtained something we're looking for
]]
function R:OnEvent(event, ...)
-------------------------------------------------------------------------------------
-- You opened a loot window on a corpse or fishing node
-------------------------------------------------------------------------------------
if event == "LOOT_READY" then
-- Detect bank, guild bank, auction house, tradeskill, trade, and mail. This turns off item use detection.
self:Debug("LOOT_READY with target: " .. (UnitGUID("target") or "NO TARGET"))
-- In 8.0.1, two LOOT_READY events fire when the loot window opens. We'll just ignore subsequent events for a short time to prevent double counting
if Rarity.Session:IsLocked() then -- One attempt is already being counted and we don't want another one for this loot event -> Ignore this call
Rarity:Debug("Session is locked; ignoring this LOOT_READY event")
return
else
Rarity.Session:Lock(1)
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 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 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
-- 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
elseif event == "BANKFRAME_OPENED" then
Rarity.isBankOpen = true
elseif event == "GUILDBANKFRAME_OPENED" then
Rarity.isGuildBankOpen = true
elseif event == "AUCTION_HOUSE_SHOW" then
Rarity.isAuctionHouseOpen = true
elseif event == "TRADE_SHOW" then
Rarity.isTradeWindowOpen = true
elseif event == "TRADE_SKILL_SHOW" then
Rarity.isTradeskillOpen = true
elseif event == "MAIL_SHOW" then
Rarity.isMailboxOpen = true
elseif event == "BANKFRAME_CLOSED" then
Rarity.isBankOpen = false
elseif event == "GUILDBANKFRAME_CLOSED" then
Rarity.isGuildBankOpen = false
elseif event == "AUCTION_HOUSE_CLOSED" then
Rarity.isAuctionHouseOpen = false
elseif event == "TRADE_CLOSED" then
Rarity.isTradeWindowOpen = false
elseif event == "TRADE_SKILL_CLOSE" then
Rarity.isTradeskillOpen = false
elseif event == "MAIL_CLOSED" then
-- Instance lock info updated
Rarity.isMailboxOpen = false
elseif event == "UPDATE_INSTANCE_INFO" then
self:ScanInstanceLocks(event)
elseif event == "LFG_UPDATE_RANDOM_INFO" then
-- Calendar updated
self:ScanInstanceLocks(event)
elseif event == "CALENDAR_UPDATE_EVENT_LIST" then
-- Toy box updated
self:ScanCalendar(event)
elseif event == "TOYS_UPDATED" then
-- Pets updated
Rarity.Collections:ScanExistingItems(event)
elseif event == "COMPANION_UPDATE" then
-- Logging out; end any open session
Rarity.Collections:ScanExistingItems(event)
elseif event == "PLAYER_LOGOUT" then
if Rarity.Session:IsActive() then
Rarity.Session:End()
end
end
end
Rarity.EventHandlers = EventHandlers
return EventHandlers