Mercurial > wow > wowdb-profiler
view Main.lua @ 332:5ef583564381 WoD
Merge
author | James D. Callahan III <jcallahan@curse.com> |
---|---|
date | Tue, 26 Aug 2014 10:45:49 -0500 |
parents | 998f3264b482 267e7100407c |
children | 601d3e517460 |
line wrap: on
line source
-- LUA API ------------------------------------------------------------ local _G = getfenv(0) local pairs = _G.pairs local tostring = _G.tostring local tonumber = _G.tonumber local bit = _G.bit local math = _G.math local table = _G.table local select = _G.select local unpack = _G.unpack -- ADDON NAMESPACE ---------------------------------------------------- local ADDON_NAME, private = ... local LibStub = _G.LibStub local WDP = LibStub("AceAddon-3.0"):NewAddon(ADDON_NAME, "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0") local deformat = LibStub("LibDeformat-3.0") local LPJ = LibStub("LibPetJournal-2.0") local MapData = LibStub("LibMapData-1.0") local DatamineTT = _G.CreateFrame("GameTooltip", "WDPDatamineTT", _G.UIParent, "GameTooltipTemplate") DatamineTT:SetOwner(_G.WorldFrame, "ANCHOR_NONE") -- CONSTANTS ---------------------------------------------------------- local AF = private.ACTION_TYPE_FLAGS local CLIENT_LOCALE = _G.GetLocale() local DB_VERSION = 18 local DEBUGGING = false local EVENT_DEBUG = false local OBJECT_ID_ANVIL = 192628 local OBJECT_ID_FISHING_BOBBER = 35591 local OBJECT_ID_FORGE = 1685 local PLAYER_CLASS = _G.select(2, _G.UnitClass("player")) local PLAYER_FACTION = _G.UnitFactionGroup("player") local PLAYER_GUID local PLAYER_NAME = _G.UnitName("player") local PLAYER_RACE = _G.select(2, _G.UnitRace("player")) local SPELL_ID_CHI_WAVE = 132464 local SPELL_ID_DISGUISE = 121308 local ALLOWED_LOCALES = { enUS = true, enGB = true, } local DATABASE_DEFAULTS = { char = {}, global = { config = { minimap_icon = { hide = true, }, }, items = {}, npcs = {}, objects = {}, quests = {}, spells = {}, zones = {}, } } local EVENT_MAPPING = { AUCTION_HOUSE_SHOW = true, BANKFRAME_OPENED = true, BATTLEFIELDS_SHOW = true, BLACK_MARKET_ITEM_UPDATE = true, CHAT_MSG_LOOT = true, CHAT_MSG_MONSTER_SAY = "RecordQuote", CHAT_MSG_MONSTER_WHISPER = "RecordQuote", CHAT_MSG_MONSTER_YELL = "RecordQuote", CHAT_MSG_SYSTEM = true, COMBAT_LOG_EVENT_UNFILTERED = true, COMBAT_TEXT_UPDATE = true, CURSOR_UPDATE = true, FORGE_MASTER_OPENED = true, GOSSIP_SHOW = true, GROUP_ROSTER_UPDATE = true, GUILDBANKFRAME_OPENED = true, ITEM_TEXT_BEGIN = true, ITEM_UPGRADE_MASTER_OPENED = true, LOOT_CLOSED = true, LOOT_READY = true, MAIL_SHOW = true, MERCHANT_CLOSED = true, MERCHANT_SHOW = "UpdateMerchantItems", MERCHANT_UPDATE = "UpdateMerchantItems", PET_BAR_UPDATE = true, PET_JOURNAL_LIST_UPDATE = true, PLAYER_REGEN_DISABLED = true, PLAYER_REGEN_ENABLED = true, PLAYER_TARGET_CHANGED = true, QUEST_COMPLETE = true, QUEST_DETAIL = true, QUEST_LOG_UPDATE = true, QUEST_PROGRESS = true, SHOW_LOOT_TOAST = true, SPELL_CONFIRMATION_PROMPT = true, TAXIMAP_OPENED = true, TRADE_SKILL_SHOW = true, TRAINER_CLOSED = true, TRAINER_SHOW = true, TRANSMOGRIFY_OPEN = true, UNIT_PET = true, UNIT_QUEST_LOG_CHANGED = true, UNIT_SPELLCAST_FAILED = "HandleSpellFailure", UNIT_SPELLCAST_FAILED_QUIET = "HandleSpellFailure", UNIT_SPELLCAST_INTERRUPTED = "HandleSpellFailure", UNIT_SPELLCAST_SENT = true, UNIT_SPELLCAST_SUCCEEDED = true, VOID_STORAGE_OPEN = true, ZONE_CHANGED = "HandleZoneChange", ZONE_CHANGED_INDOORS = "HandleZoneChange", ZONE_CHANGED_NEW_AREA = "HandleZoneChange", } -- VARIABLES ---------------------------------------------------------- local anvil_spell_ids = {} local currently_drunk local char_db local global_db local group_member_guids = {} local group_owner_guids_to_pet_guids = {} local group_pet_guids = {} local in_instance local item_process_timer_handle local faction_standings = {} local forge_spell_ids = {} local languages_known = {} local boss_loot_toasting = {} local loot_toast_container_timer_handle local loot_toast_data local loot_toast_data_timer_handle local name_to_id_map = {} local killed_boss_id_timer_handle local killed_npc_id local target_location_timer_handle local current_target_id local current_area_id local current_loot -- Data for our current action. Including possible values as a reference. local current_action = { identifier = nil, loot_label = nil, loot_list = nil, loot_sources = nil, map_level = nil, spell_label = nil, target_type = nil, x = nil, y = nil, zone_data = nil, } -- HELPERS ------------------------------------------------------------ local function Debug(message, ...) if not DEBUGGING or not message or not ... then return end local args = { ... } for index = 1, #args do if args[index] == nil then args[index] = "nil" end end _G.print(message:format(unpack(args))) end local TradeSkillExecutePer do local header_list = {} function TradeSkillExecutePer(iter_func) if not _G.TradeSkillFrame or not _G.TradeSkillFrame:IsVisible() then return end -- Clear the search box focus so the scan will have correct results. local search_box = _G.TradeSkillFrameSearchBox search_box:SetText("") _G.TradeSkillSearch_OnTextChanged(search_box) search_box:ClearFocus() search_box:GetScript("OnEditFocusLost")(search_box) table.wipe(header_list) -- Save the current state of the TradeSkillFrame so it can be restored after we muck with it. local have_materials = _G.TradeSkillFrame.filterTbl.hasMaterials local have_skillup = _G.TradeSkillFrame.filterTbl.hasSkillUp if have_materials then _G.TradeSkillFrame.filterTbl.hasMaterials = false _G.TradeSkillOnlyShowMakeable(false) end if have_skillup then _G.TradeSkillFrame.filterTbl.hasSkillUp = false _G.TradeSkillOnlyShowSkillUps(false) end _G.SetTradeSkillInvSlotFilter(0, true, true) _G.TradeSkillUpdateFilterBar() _G.TradeSkillFrame_Update() -- Expand all headers so we can see all the recipes there are for tradeskill_index = 1, _G.GetNumTradeSkills() do local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index) if tradeskill_type == "header" or tradeskill_type == "subheader" then if not is_expanded then header_list[name] = true _G.ExpandTradeSkillSubClass(tradeskill_index) end elseif iter_func(name, tradeskill_index) then break end end -- Restore the state of the things we changed. for tradeskill_index = 1, _G.GetNumTradeSkills() do local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index) if header_list[name] then _G.CollapseTradeSkillSubClass(tradeskill_index) end end _G.TradeSkillFrame.filterTbl.hasMaterials = have_materials _G.TradeSkillOnlyShowMakeable(have_materials) _G.TradeSkillFrame.filterTbl.hasSkillUp = have_skillup _G.TradeSkillOnlyShowSkillUps(have_skillup) _G.TradeSkillUpdateFilterBar() _G.TradeSkillFrame_Update() end end -- do-block local ActualCopperCost do local BARTERING_SPELL_ID = 83964 local STANDING_DISCOUNTS = { HATED = 0, HOSTILE = 0, UNFRIENDLY = 0, NEUTRAL = 0, FRIENDLY = 0.05, HONORED = 0.1, REVERED = 0.15, EXALTED = 0.2, } function ActualCopperCost(copper_cost, rep_standing) if not copper_cost or copper_cost == 0 then return 0 end local modifier = 1 if _G.IsSpellKnown(BARTERING_SPELL_ID) then modifier = modifier - 0.1 end if rep_standing then if PLAYER_RACE == "Goblin" then modifier = modifier - STANDING_DISCOUNTS["EXALTED"] elseif STANDING_DISCOUNTS[rep_standing] then modifier = modifier - STANDING_DISCOUNTS[rep_standing] end end return math.floor(copper_cost / modifier) end end -- do-block -- Constant for duplicate boss data; a dirty hack to get around world bosses that cannot be identified individually and cannot be linked on wowdb because they are not in a raid local DUPLICATE_WORLD_BOSS_IDS = { [71952] = { 71953, 71954, 71955, }, } -- Called on a timer local function ClearKilledNPC() killed_npc_id = nil end local function ClearKilledBossID() if killed_boss_id_timer_handle then WDP:CancelTimer(killed_boss_id_timer_handle) killed_boss_id_timer_handle = nil end table.wipe(boss_loot_toasting) private.raid_finder_boss_id = nil private.world_boss_id = nil end local function ClearLootToastContainerID() if loot_toast_container_timer_handle then WDP:CancelTimer(loot_toast_container_timer_handle) loot_toast_container_timer_handle = nil end private.container_loot_toasting = false private.loot_toast_container_id = nil end local function ClearLootToastData() -- cancel existing timer if found if loot_toast_data_timer_handle then WDP:CancelTimer(loot_toast_data_timer_handle) loot_toast_data_timer_handle = nil end if loot_toast_data then table.wipe(loot_toast_data) end end local function InstanceDifficultyToken() local _, instance_type, instance_difficulty, _, _, _, is_dynamic = _G.GetInstanceInfo() if not instance_type or instance_type == "" then instance_type = "NONE" end return ("%s:%d:%s"):format(instance_type:upper(), instance_difficulty, tostring(is_dynamic)) end local function DBEntry(data_type, unit_id) if not data_type or not unit_id then return end local category = global_db[data_type] if not category then category = {} global_db[data_type] = category end local unit = category[unit_id] if not unit then unit = {} category[unit_id] = unit end return unit end private.DBEntry = DBEntry local NPCEntry do local npc_prototype = {} local npc_meta = { __index = npc_prototype } function NPCEntry(identifier) local npc = DBEntry("npcs", identifier) return npc and _G.setmetatable(npc, npc_meta) or nil end function npc_prototype:EncounterData(difficulty_token) self.encounter_data = self.encounter_data or {} self.encounter_data[difficulty_token] = self.encounter_data[difficulty_token] or {} self.encounter_data[difficulty_token].stats = self.encounter_data[difficulty_token].stats or {} return self.encounter_data[difficulty_token] end end local function CurrentLocationData() if _G.GetCurrentMapAreaID() ~= current_area_id then return _G.GetRealZoneText(), current_area_id, 0, 0, 0, InstanceDifficultyToken() end local map_level = _G.GetCurrentMapDungeonLevel() or 0 local x, y = _G.GetPlayerMapPosition("player") x = x or 0 y = y or 0 if x == 0 and y == 0 then for level_index = 1, _G.GetNumDungeonMapLevels() do _G.SetDungeonMapLevel(level_index) x, y = _G.GetPlayerMapPosition("player") if x and y and (x > 0 or y > 0) then _G.SetDungeonMapLevel(map_level) map_level = level_index break end end end if _G.DungeonUsesTerrainMap() then map_level = map_level - 1 end local x = _G.floor(x * 1000) local y = _G.floor(y * 1000) if x % 2 ~= 0 then x = x + 1 end if y % 2 ~= 0 then y = y + 1 end return _G.GetRealZoneText(), current_area_id, x, y, map_level, InstanceDifficultyToken() end local function CurrencyLinkToTexture(currency_link) if not currency_link then return end local _, _, texture_path = _G.GetCurrencyInfo(tonumber(currency_link:match("currency:(%d+)"))) return texture_path:match("[^\\]+$"):lower() end local function ItemLinkToID(item_link) if not item_link then return end return tonumber(item_link:match("item:(%d+)")) end private.ItemLinkToID = ItemLinkToID local function UnitTypeIsNPC(unit_type) return unit_type == private.UNIT_TYPES.NPC or unit_type == private.UNIT_TYPES.VEHICLE end local ParseGUID do local UNIT_TYPES = private.UNIT_TYPES local NPC_ID_MAPPING = { [62164] = 63191, -- Garalon } function MatchUnitTypes(unit_type_name) if not unit_type_name then return UNIT_TYPES.UNKNOWN end for def, text in next, UNIT_TYPES do if unit_type_name == text then return UNIT_TYPES[def] end end return UNIT_TYPES.UNKNOWN end function ParseGUID(guid) if not guid then return end -- We might want to use some of this new information later, but leaving the returns alone for now local unit_type_name, unk_id1, server_id, instance_id, unk_id2, unit_idnum, spawn_id = (":"):split(guid) unit_type = MatchUnitTypes(unit_type_name) if unit_type ~= UNIT_TYPES.PLAYER and unit_type ~= UNIT_TYPES.PET and unit_type ~= UNIT_TYPES.ITEM then local id_mapping = NPC_ID_MAPPING[unit_idnum] if id_mapping and UnitTypeIsNPC(unit_type) then unit_idnum = id_mapping end return unit_type, unit_idnum end return unit_type end private.ParseGUID = ParseGUID end -- do-block local UpdateDBEntryLocation do -- Fishing node coordinate code based on code in GatherMate2 with permission from Kagaro. local function FishingCoordinates(x, y, yard_width, yard_height) local facing = _G.GetPlayerFacing() if not facing then return x, y end local rad = facing + math.pi return x + math.sin(rad) * 15 / yard_width, y + math.cos(rad) * 15 / yard_height end function UpdateDBEntryLocation(entry_type, identifier) if not identifier then return end local zone_name, area_id, x, y, map_level, difficulty_token = CurrentLocationData() if not (zone_name and area_id and x and y and map_level) then Debug("UpdateDBEntryLocation: Missing current location data - %s, %d, %d, %d, %d.", zone_name, area_id, x, y, map_level) return end local entry = DBEntry(entry_type, identifier) entry[difficulty_token] = entry[difficulty_token] or {} entry[difficulty_token].locations = entry[difficulty_token].locations or {} local zone_token = ("%s:%d"):format(zone_name, area_id) local zone_data = entry[difficulty_token].locations[zone_token] if not zone_data then zone_data = {} entry[difficulty_token].locations[zone_token] = zone_data end -- Special case for Fishing. if current_action.spell_label == "FISHING" then local yard_width, yard_height = MapData:MapArea(area_id, map_level) if yard_width > 0 and yard_height > 0 then x, y = FishingCoordinates(x, y, yard_width, yard_height) current_action.x = x current_action.y = y end end local location_token = ("%d:%d:%d"):format(map_level, x, y) zone_data[location_token] = zone_data[location_token] or true return zone_data end end -- do-block local function HandleItemUse(item_link, bag_index, slot_index) if not item_link then return end local item_id = ItemLinkToID(item_link) if not bag_index or not slot_index then for new_bag_index = 0, _G.NUM_BAG_FRAMES do for new_slot_index = 1, _G.GetContainerNumSlots(new_bag_index) do if item_id == ItemLinkToID(_G.GetContainerItemLink(new_bag_index, new_slot_index)) then bag_index = new_bag_index slot_index = new_slot_index break end end end end if not bag_index or not slot_index then return end local _, _, _, _, _, is_lootable = _G.GetContainerItemInfo(bag_index, slot_index) if not is_lootable then return end DatamineTT:ClearLines() DatamineTT:SetBagItem(bag_index, slot_index) for line_index = 1, DatamineTT:NumLines() do local current_line = _G["WDPDatamineTTTextLeft" .. line_index] if not current_line then Debug("HandleItemUse: Item with ID %d and link %s had an invalid tooltip.", item_id, item_link, _G.ITEM_OPENABLE) return end if current_line:GetText() == _G.ITEM_OPENABLE then table.wipe(current_action) current_loot = nil current_action.target_type = AF.ITEM current_action.identifier = item_id current_action.loot_label = "contains" return end end Debug("HandleItemUse: Item with ID %d and link %s did not have a tooltip that contained the string %s.", item_id, item_link, _G.ITEM_OPENABLE) end local UnitFactionStanding local UpdateFactionData do local MAX_FACTION_INDEX = 1000 local STANDING_NAMES = { "HATED", "HOSTILE", "UNFRIENDLY", "NEUTRAL", "FRIENDLY", "HONORED", "REVERED", "EXALTED", } function UnitFactionStanding(unit) local unit_name = _G.UnitName(unit) UpdateFactionData() DatamineTT:ClearLines() DatamineTT:SetUnit(unit) for line_index = 1, DatamineTT:NumLines() do local faction_name = _G["WDPDatamineTTTextLeft" .. line_index]:GetText():trim() if faction_name and faction_name ~= unit_name and faction_standings[faction_name] then return faction_name, faction_standings[faction_name] end end end function UpdateFactionData() for faction_index = 1, MAX_FACTION_INDEX do local faction_name, _, current_standing, _, _, _, _, _, is_header = _G.GetFactionInfo(faction_index) if faction_name then faction_standings[faction_name] = STANDING_NAMES[current_standing] elseif not faction_name then break end end end end -- do-block local GenericLootUpdate do local function LootTable(entry, loot_type, top_field) if top_field then entry[top_field] = entry[top_field] or {} entry[top_field][loot_type] = entry[top_field][loot_type] or {} return entry[top_field][loot_type] end entry[loot_type] = entry[loot_type] or {} return entry[loot_type] end function GenericLootUpdate(data_type, top_field) local loot_type = current_loot.label local loot_count = ("%s_count"):format(loot_type) local source_list = {} if current_loot.sources then for source_guid, loot_data in pairs(current_loot.sources) do local source_id if current_loot.target_type == AF.ITEM then -- Items return the player as the source, so we need to use the item's ID (disenchant, milling, etc) source_id = current_loot.identifier else local _, unit_ID = ParseGUID(source_guid) if unit_ID then if current_loot.target_type == AF.OBJECT then source_id = ("%s:%s"):format(current_loot.spell_label, unit_ID) else source_id = unit_ID end end end local entry = DBEntry(data_type, source_id) if entry then local loot_table = LootTable(entry, loot_type, top_field) if not source_list[source_id] then if top_field then entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1 elseif not private.container_loot_toasting then entry[loot_count] = (entry[loot_count] or 0) + 1 end source_list[source_id] = true end UpdateDBEntryLocation(data_type, source_id) if current_loot.target_type == AF.ZONE then for item_id, quantity in pairs(loot_data) do table.insert(loot_table, ("%d:%d"):format(item_id, quantity)) end else for loot_token, quantity in pairs(loot_data) do local label, currency_texture = (":"):split(loot_token) if label == "currency" and currency_texture then table.insert(loot_table, ("currency:%d:%s"):format(quantity, currency_texture)) elseif loot_token == "money" then table.insert(loot_table, ("money:%d"):format(quantity)) else table.insert(loot_table, ("%d:%d"):format(loot_token, quantity)) end end end end end end -- This is used for Gas Extractions. if #current_loot.list <= 0 then return end local entry -- At this point we only have a name if it's an object. if current_loot.target_type == AF.OBJECT then entry = DBEntry(data_type, ("%s:%s"):format(current_loot.spell_label, current_loot.object_name)) else entry = DBEntry(data_type, current_loot.identifier) end if not entry then return end local loot_table = LootTable(entry, loot_type, top_field) if current_loot.identifier then if not source_list[current_loot.identifier] then if top_field then entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1 else entry[loot_count] = (entry[loot_count] or 0) + 1 end source_list[current_loot.identifier] = true end end for index = 1, #current_loot.list do table.insert(loot_table, current_loot.list[index]) end end end -- do-block local ReplaceKeywords do local KEYWORD_SUBSTITUTIONS = { class = PLAYER_CLASS, name = PLAYER_NAME, race = PLAYER_RACE, } function ReplaceKeywords(text) if not text or text == "" then return "" end for category, lookup in pairs(KEYWORD_SUBSTITUTIONS) do local category_format = ("<%s>"):format(category) text = text:gsub(lookup, category_format):gsub(lookup:lower(), category_format) end return text end end -- do-block -- Contains a dirty hack due to Blizzard's strange handling of Micro Dungeons; GetMapInfo() will not return correct information -- unless the WorldMapFrame is shown. do -- MapFileName = MapAreaID local MICRO_DUNGEON_IDS = { ShrineofTwoMoons = 903, ShrineofSevenStars = 905, } local function SetCurrentAreaID() if private.in_combat then private.set_area_id = true return end local map_area_id = _G.GetCurrentMapAreaID() if map_area_id == current_area_id then return end local world_map = _G.WorldMapFrame local map_visible = world_map:IsVisible() local sfx_value = tonumber(_G.GetCVar("Sound_EnableSFX")) if not map_visible then _G.SetCVar("Sound_EnableSFX", 0) world_map:Show() end local _, _, _, _, micro_dungeon_map_name = _G.GetMapInfo() local micro_dungeon_id = MICRO_DUNGEON_IDS[micro_dungeon_map_name] _G.SetMapToCurrentZone() if micro_dungeon_id then current_area_id = micro_dungeon_id else current_area_id = _G.GetCurrentMapAreaID() end if map_visible then _G.SetMapByID(map_area_id) else world_map:Hide() _G.SetCVar("Sound_EnableSFX", sfx_value) end end function WDP:HandleZoneChange(event_name) in_instance = _G.IsInInstance() SetCurrentAreaID() end end local function InitializeCurrentLoot() current_loot = { list = {}, sources = {}, identifier = current_action.identifier, label = current_action.loot_label or "drops", map_level = current_action.map_level, object_name = current_action.object_name, spell_label = current_action.spell_label, target_type = current_action.target_type, x = current_action.x, y = current_action.y, zone_data = current_action.zone_data, } table.wipe(current_action) end -- METHODS ------------------------------------------------------------ function WDP:OnInitialize() local db = LibStub("AceDB-3.0"):New("WoWDBProfilerData", DATABASE_DEFAULTS, "Default") private.db = db global_db = db.global char_db = db.char local raw_db = _G.WoWDBProfilerData local build_num = tonumber(private.build_num) if (raw_db.version and raw_db.version < DB_VERSION) or (raw_db.build_num and raw_db.build_num < build_num) then for entry in pairs(DATABASE_DEFAULTS.global) do global_db[entry] = {} end end raw_db.build_num = build_num raw_db.region = private.region raw_db.version = DB_VERSION private.InitializeCommentSystem() self:RegisterChatCommand("comment", private.ProcessCommentCommand) end function WDP:EventDispatcher(...) local event_name = ... if DEBUGGING then if event_name == "COMBAT_LOG_EVENT_UNFILTERED" then Debug(event_name) else Debug(...) end end local func = EVENT_MAPPING[event_name] if _G.type(func) == "boolean" then self[event_name](self, ...) elseif _G.type(func) == "function" then self[func](self, ...) end end function WDP:OnEnable() PLAYER_GUID = _G.UnitGUID("player") for event_name, mapping in pairs(EVENT_MAPPING) do if EVENT_DEBUG then self:RegisterEvent(event_name, "EventDispatcher") else self:RegisterEvent(event_name, (_G.type(mapping) ~= "boolean") and mapping or nil) end end for index = 1, _G.GetNumLanguages() do languages_known[_G.GetLanguageByIndex(index)] = true end item_process_timer_handle = self:ScheduleRepeatingTimer("ProcessItems", 30) target_location_timer_handle = self:ScheduleRepeatingTimer("UpdateTargetLocation", 0.5) _G.hooksecurefunc("UseContainerItem", function(bag_index, slot_index, target_unit) if target_unit then return end HandleItemUse(_G.GetContainerItemLink(bag_index, slot_index), bag_index, slot_index) end) _G.hooksecurefunc("UseItemByName", function(identifier, target_unit) if target_unit then return end local _, item_link = _G.GetItemInfo(identifier) HandleItemUse(item_link) end) self:HandleZoneChange("OnEnable") self:GROUP_ROSTER_UPDATE() end local ScrapeItemUpgradeStats do local intermediary = {} function ScrapeItemUpgradeStats(item_id, upgrade_id, reforge_id) if not ALLOWED_LOCALES[CLIENT_LOCALE] then return end local create_entry table.wipe(intermediary) for line_index = 1, DatamineTT:NumLines() do local left_text = _G["WDPDatamineTTTextLeft" .. line_index]:GetText():trim() if not left_text or left_text == "" or left_text:find("Socket") or left_text:find("Set:") then break end local amount, stat = left_text:match("+(.-) (.*)") if amount and stat then create_entry = true intermediary[stat:lower():gsub(" ", "_"):gsub("|r", "")] = tonumber((amount:gsub(",", ""))) end end if not create_entry then return end local item = DBEntry("items", item_id) item.upgrade_id = upgrade_id item.upgrades = item.upgrades or {} item.upgrades[upgrade_id] = item.upgrades[upgrade_id] or {} for stat, amount in pairs(intermediary) do item.upgrades[upgrade_id][stat] = amount end end end -- do-block local function RecordItemData(item_id, item_link, durability) local _, _, item_string = item_link:find("^|%x+|H(.+)|h%[.+%]") local item if item_string then local _, _, _, _, _, _, _, suffix_id, unique_id, _, upgrade_id, instance_difficulty_id, num_bonus_ids = (":"):split(item_string) local _, _, _, _, _, _, _, _, _, _, _, _, _, bonus_ids = (":"):split(item_string) upgrade_id = tonumber(upgrade_id) instance_difficulty_id = tonumber(instance_difficulty_id) num_bonus_ids = tonumber(num_bonus_ids) suffix_id = tonumber(suffix_id) if (not num_bonus_ids) or (num_bonus_ids == 0) then if (suffix_id and suffix_id ~= 0) or (instance_difficulty_id and instance_difficulty_id ~= 0) then item = DBEntry("items", item_id) item.unique_id = bit.band(unique_id, 0xFFFF) if suffix_id and suffix_id ~= 0 then item.suffix_id = suffix_id end if instance_difficulty_id and instance_difficulty_id ~= 0 then item.instance_difficulty_id = instance_difficulty_id end end elseif num_bonus_ids > 0 then item = DBEntry("items", item_id) item.unique_id = bit.band(unique_id, 0xFFFF) item.instance_difficulty_id = instance_difficulty_id if not item.bonus_ids then item.bonus_ids = {} end for bonus_index = 1, num_bonus_ids do item.bonus_ids[tonumber(bonus_ids[bonus_index])] = true end else Debug("RecordItemData: Item_system is supposed to be 0 or positive, instead it was %s.", item_system) end if upgrade_id and upgrade_id ~= 0 then DatamineTT:SetHyperlink(item_link) ScrapeItemUpgradeStats(item_id, upgrade_id) end end if durability and durability > 0 then item = item or DBEntry("items", item_id) item.durability = durability end end function WDP:ProcessItems() for slot_index = _G.INVSLOT_FIRST_EQUIPPED, _G.INVSLOT_LAST_EQUIPPED do local item_id = _G.GetInventoryItemID("player", slot_index) if item_id and item_id > 0 then local _, max_durability = _G.GetInventoryItemDurability(slot_index) RecordItemData(item_id, _G.GetInventoryItemLink("player", slot_index), max_durability) end end for bag_index = 0, _G.NUM_BAG_SLOTS do for slot_index = 1, _G.GetContainerNumSlots(bag_index) do local item_id = _G.GetContainerItemID(bag_index, slot_index) if item_id and item_id > 0 then local _, max_durability = _G.GetContainerItemDurability(bag_index, slot_index) RecordItemData(item_id, _G.GetContainerItemLink(bag_index, slot_index), max_durability) end end end end local TargetedNPC do local GENDER_NAMES = { "UNKNOWN", "MALE", "FEMALE", } local REACTION_NAMES = { "HATED", "HOSTILE", "UNFRIENDLY", "NEUTRAL", "FRIENDLY", "HONORED", "REVERED", "EXALTED", } local POWER_TYPE_NAMES = { ["0"] = "MANA", ["1"] = "RAGE", ["2"] = "FOCUS", ["3"] = "ENERGY", ["6"] = "RUNIC_POWER", } function TargetedNPC() if not _G.UnitExists("target") or _G.UnitPlayerControlled("target") or currently_drunk then current_target_id = nil return end local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("target")) if not unit_idnum or not UnitTypeIsNPC(unit_type) then return end current_target_id = unit_idnum local npc = NPCEntry(unit_idnum) local _, class_token = _G.UnitClass("target") npc.class = class_token npc.faction = UnitFactionStanding("target") npc.genders = npc.genders or {} npc.genders[GENDER_NAMES[_G.UnitSex("target")] or "UNDEFINED"] = true npc.is_pvp = _G.UnitIsPVP("target") and true or nil npc.reaction = ("%s:%s:%s"):format(_G.UnitLevel("player"), _G.UnitFactionGroup("player"), REACTION_NAMES[_G.UnitReaction("player", "target")]) local encounter_data = npc:EncounterData(InstanceDifficultyToken()).stats local npc_level = ("level_%d"):format(_G.UnitLevel("target")) local level_data = encounter_data[npc_level] if not level_data then level_data = {} encounter_data[npc_level] = level_data end level_data.max_health = level_data.max_health or _G.UnitHealthMax("target") if not level_data.power then local max_power = _G.UnitManaMax("target") if max_power > 0 then local power_type = _G.UnitPowerType("target") level_data.power = ("%s:%d"):format(POWER_TYPE_NAMES[tostring(power_type)] or power_type, max_power) end end name_to_id_map[_G.UnitName("target")] = unit_idnum return npc, unit_idnum end end -- do-block do local COORD_MAX = 5 function WDP:UpdateTargetLocation() if currently_drunk or not _G.UnitExists("target") or _G.UnitPlayerControlled("target") or (_G.UnitIsTapped("target") and not _G.UnitIsDead("target")) then return end for index = 1, 4 do if not _G.CheckInteractDistance("target", index) then return end end local npc = TargetedNPC() if not npc then return end local zone_name, area_id, x, y, map_level, difficulty_token = CurrentLocationData() if not (zone_name and area_id and x and y and map_level) then Debug("UpdateTargetLocation: Missing current location data - %s, %d, %d, %d, %d.", zone_name, area_id, x, y, map_level) return end local npc_data = npc:EncounterData(difficulty_token).stats[("level_%d"):format(_G.UnitLevel("target"))] local zone_token = ("%s:%d"):format(zone_name, area_id) npc_data.locations = npc_data.locations or {} -- TODO: Fix this. It is broken. Possibly something to do with the timed updates. local zone_data = npc_data.locations[zone_token] if not zone_data then zone_data = {} npc_data.locations[zone_token] = zone_data end for location_token in pairs(zone_data) do local loc_level, loc_x, loc_y = (":"):split(location_token) loc_level = tonumber(loc_level) if map_level == loc_level and math.abs(x - loc_x) <= COORD_MAX and math.abs(y - loc_y) <= COORD_MAX then return end end zone_data[("%d:%d:%d"):format(map_level, x, y)] = true end end -- do-block -- EVENT HANDLERS ----------------------------------------------------- function WDP:BLACK_MARKET_ITEM_UPDATE(event_name) if not ALLOWED_LOCALES[CLIENT_LOCALE] then return end local num_items = _G.C_BlackMarket.GetNumItems() or 0 for index = 1, num_items do local name, texture, quantity, item_type, is_usable, level, level_type, seller_name, min_bid, min_increment, current_bid, has_high_bid, num_bids, time_left, item_link, market_id = _G.C_BlackMarket.GetItemInfoByIndex(index); if item_link then DBEntry("items", ItemLinkToID(item_link)).black_market = seller_name or "UNKNOWN" end end end local function UpdateUnitPet(unit_guid, unit_id) local current_pet_guid = group_owner_guids_to_pet_guids[unit_guid] if current_pet_guid then group_owner_guids_to_pet_guids[unit_guid] = nil group_pet_guids[current_pet_guid] = nil end local pet_guid = _G.UnitGUID(unit_id .. "pet") if pet_guid then group_owner_guids_to_pet_guids[unit_guid] = pet_guid group_pet_guids[pet_guid] = true end end function WDP:GROUP_ROSTER_UPDATE(event_name) local is_raid = _G.IsInRaid() local unit_type = is_raid and "raid" or "party" local group_size = is_raid and _G.GetNumGroupMembers() or _G.GetNumSubgroupMembers() table.wipe(group_member_guids) for index = 1, group_size do local unit_id = unit_type .. index local unit_guid = _G.UnitGUID(unit_id) group_member_guids[unit_guid] = true UpdateUnitPet(unit_guid, unit_id) end group_member_guids[PLAYER_GUID] = true end function WDP:UNIT_PET(event_name, unit_id) UpdateUnitPet(_G.UnitGUID(unit_id), unit_id) end function WDP:SHOW_LOOT_TOAST(event_name, loot_type, item_link, quantity) if not loot_type or (loot_type ~= "item" and loot_type ~= "money" and loot_type ~= "currency") then Debug("%s: loot_type is %s. Item link is %s, and quantity is %d.", event_name, loot_type, item_link, quantity) return end local container_id = private.loot_toast_container_id local npc_id = private.raid_finder_boss_id or private.world_boss_id if npc_id then -- slightly messy hack to workaround duplicate world bosses local upper_limit = 0 if DUPLICATE_WORLD_BOSS_IDS[npc_id] then upper_limit = #DUPLICATE_WORLD_BOSS_IDS[npc_id] end for i = 0, upper_limit do local temp_npc_id = npc_id if i > 0 then temp_npc_id = DUPLICATE_WORLD_BOSS_IDS[npc_id][i] end local npc = NPCEntry(temp_npc_id) if npc then local loot_label = "drops" local encounter_data = npc:EncounterData(InstanceDifficultyToken()) encounter_data[loot_label] = encounter_data[loot_label] or {} encounter_data.loot_counts = encounter_data.loot_counts or {} if loot_type == "item" then local item_id = ItemLinkToID(item_link) if item_id then Debug("%s: %s X %d (%d)", event_name, item_link, quantity, item_id) table.insert(encounter_data[loot_label], ("%d:%d"):format(item_id, quantity)) else Debug("%s: ItemID is nil, from item link %s", event_name, item_link) return end elseif loot_type == "money" then Debug("%s: money X %d", event_name, quantity) table.insert(encounter_data[loot_label], ("money:%d"):format(quantity)) elseif loot_type == "currency" then local currency_texture = CurrencyLinkToTexture(item_link) if currency_texture and currency_texture ~= "" then Debug("%s: %s X %d", event_name, currency_texture, quantity) -- workaround for Patch 5.4.0 bug with Flexible raid Siege of Orgrimmar bosses and Valor Points if quantity > 1000 and currency_texture == "pvecurrency-valor" then quantity = math.floor(quantity / 100) end table.insert(encounter_data[loot_label], ("currency:%d:%s"):format(quantity, currency_texture)) else Debug("%s: Currency texture is nil, from currency link %s", event_name, item_link) return end end if not boss_loot_toasting[temp_npc_id] then encounter_data.loot_counts[loot_label] = (encounter_data.loot_counts[loot_label] or 0) + 1 boss_loot_toasting[temp_npc_id] = true -- Do not count further loots until timer expires or another boss is killed end end end elseif container_id then InitializeCurrentLoot() -- Fake the loot characteristics to match that of an actual container item current_loot.identifier = container_id current_loot.label = "contains" current_loot.target_type = AF.ITEM current_loot.sources[container_id] = current_loot.sources[container_id] or {} if loot_type == "item" then local item_id = ItemLinkToID(item_link) if item_id then Debug("%s: %s X %d (%d)", event_name, item_link, quantity, item_id) current_loot.sources[container_id][item_id] = current_loot.sources[container_id][item_id] or 0 + quantity else Debug("%s: ItemID is nil, from item link %s", event_name, item_link) current_loot = nil return end elseif loot_type == "money" then Debug("%s: money X %d", event_name, quantity) current_loot.sources[container_id]["money"] = current_loot.sources[container_id]["money"] or 0 + quantity elseif loot_type == "currency" then local currency_texture = CurrencyLinkToTexture(item_link) if currency_texture and currency_texture ~= "" then Debug("%s: %s X %d", event_name, currency_texture, quantity) local currency_token = ("currency:%s"):format(currency_texture) current_loot.sources[container_id][currency_token] = current_loot.sources[container_id][currency_token] or 0 + quantity else Debug("%s: Currency texture is nil, from currency link %s", event_name, item_link) current_loot = nil return end end GenericLootUpdate("items") current_loot = nil private.container_loot_toasting = true -- Do not count further loots until timer expires or another container is opened else Debug("%s: NPC and Container are nil, storing loot toast data for 5 seconds.", event_name) loot_toast_data = loot_toast_data or {} loot_toast_data[#loot_toast_data + 1] = { loot_type, item_link, quantity } loot_toast_data_timer_handle = WDP:ScheduleTimer(ClearLootToastData, 5) end end do local CHAT_MSG_LOOT_UPDATE_FUNCS = { [AF.NPC] = function(item_id, quantity) Debug("CHAT_MSG_LOOT: %d (%d)", item_id, quantity) end, [AF.ZONE] = function(item_id, quantity) InitializeCurrentLoot() current_loot.list[1] = ("%d:%d"):format(item_id, quantity) GenericLootUpdate("zones") current_loot = nil end, } function WDP:CHAT_MSG_LOOT(event_name, message) local category if current_action.spell_label ~= "EXTRACT_GAS" then category = AF.ZONE elseif private.raid_finder_boss_id then category = AF.NPC end local update_func = CHAT_MSG_LOOT_UPDATE_FUNCS[category] if not category or not update_func then return end local item_link, quantity = deformat(message, _G.LOOT_ITEM_PUSHED_SELF_MULTIPLE) if not item_link then quantity, item_link = 1, deformat(message, _G.LOOT_ITEM_PUSHED_SELF) end local item_id = ItemLinkToID(item_link) if not item_id then return end update_func(item_id, quantity) end end function WDP:RecordQuote(event_name, message, source_name, language_name) if not ALLOWED_LOCALES[CLIENT_LOCALE] or not source_name or not name_to_id_map[source_name] or (language_name ~= "" and not languages_known[language_name]) then return end local npc = NPCEntry(name_to_id_map[source_name]) npc.quotes = npc.quotes or {} npc.quotes[event_name] = npc.quotes[event_name] or {} npc.quotes[event_name][ReplaceKeywords(message)] = true end do local SOBER_MATCH = _G.DRUNK_MESSAGE_ITEM_SELF1:gsub("%%s", ".+") local DRUNK_COMPARES = { _G.DRUNK_MESSAGE_SELF2, _G.DRUNK_MESSAGE_SELF3, _G.DRUNK_MESSAGE_SELF4, } local DRUNK_MATCHES = { (_G.DRUNK_MESSAGE_SELF2:gsub("%%s", ".+")), (_G.DRUNK_MESSAGE_SELF3:gsub("%%s", ".+")), (_G.DRUNK_MESSAGE_SELF4:gsub("%%s", ".+")), } local RECIPE_MATCH = _G.ERR_LEARN_RECIPE_S:gsub("%%s", "(.*)") local function RecordDiscovery(tradeskill_name, tradeskill_index) if tradeskill_name == private.discovered_recipe_name then DBEntry("spells", tonumber(_G.GetTradeSkillRecipeLink(tradeskill_index):match("^|c%x%x%x%x%x%x%x%x|H%w+:(%d+)"))).discovery = ("%d:%d"):format(private.previous_spell_id, private.profession_level) private.discovered_recipe_name = nil private.profession_level = nil private.previous_spell_id = nil return true end end local function IterativeRecordDiscovery() TradeSkillExecutePer(RecordDiscovery) end function WDP:CHAT_MSG_SYSTEM(event_name, message) if not private.trainer_shown then local recipe_name = message:match(RECIPE_MATCH) if recipe_name and private.previous_spell_id then local profession_name, prof_level = _G.GetTradeSkillLine() if profession_name == _G.UNKNOWN then return end private.discovered_recipe_name = recipe_name private.profession_level = prof_level self:ScheduleTimer(IterativeRecordDiscovery, 0.2) end end if currently_drunk then if message == _G.DRUNK_MESSAGE_SELF1 or message:match(SOBER_MATCH) then currently_drunk = nil end return end for index = 1, #DRUNK_MATCHES do if message == DRUNK_COMPARES[index] or message:match(DRUNK_MATCHES[index]) then currently_drunk = true break end end end end do local FLAGS_NPC = bit.bor(_G.COMBATLOG_OBJECT_TYPE_GUARDIAN, _G.COMBATLOG_OBJECT_CONTROL_NPC) local FLAGS_NPC_CONTROL = bit.bor(_G.COMBATLOG_OBJECT_AFFILIATION_OUTSIDER, _G.COMBATLOG_OBJECT_CONTROL_NPC) local function RecordNPCSpell(sub_event, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, spell_id, spell_name) if not spell_id or spell_id == SPELL_ID_CHI_WAVE or spell_id == SPELL_ID_DISGUISE then return end local source_type, source_id = ParseGUID(source_guid) if not source_id or not UnitTypeIsNPC(source_type) then return end if bit.band(FLAGS_NPC_CONTROL, source_flags) == FLAGS_NPC_CONTROL and bit.band(FLAGS_NPC, source_flags) ~= 0 then local encounter_data = NPCEntry(source_id):EncounterData(InstanceDifficultyToken()) encounter_data.spells = encounter_data.spells or {} encounter_data.spells[spell_id] = (encounter_data.spells[spell_id] or 0) + 1 end end local HEAL_BATTLE_PETS_SPELL_ID = 125801 local previous_combat_event = {} local COMBAT_LOG_FUNCS = { SPELL_AURA_APPLIED = RecordNPCSpell, SPELL_CAST_START = RecordNPCSpell, SPELL_CAST_SUCCESS = function(sub_event, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, spell_id, spell_name) if spell_id == HEAL_BATTLE_PETS_SPELL_ID then local unit_type, unit_idnum = ParseGUID(source_guid) if unit_idnum and UnitTypeIsNPC(unit_type) then NPCEntry(unit_idnum).stable_master = true end end RecordNPCSpell(sub_event, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, spell_id, spell_name) end, UNIT_DIED = function(sub_event, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, spell_id, spell_name) local unit_type, unit_idnum = ParseGUID(dest_guid) if not unit_idnum or not UnitTypeIsNPC(unit_type) then Debug("%s: %s is not an NPC, or has no ID.", sub_event, dest_name or _G.UNKNOWN) ClearKilledNPC() private.harvesting = nil return end if source_guid == "" then source_guid = nil end local killer_guid = source_guid or previous_combat_event.source_guid local killer_name = source_name or previous_combat_event.source_name if not previous_combat_event.party_damage then --Debug("%s: %s was killed by %s (not group member or pet).", sub_event, dest_name or _G.UNKNOWN, killer_name or _G.UNKNOWN) -- broken in Patch 5.4 table.wipe(previous_combat_event) ClearKilledNPC() else --Debug("%s: %s was killed by %s.", sub_event, dest_name or _G.UNKNOWN, killer_name or _G.UNKNOWN) -- broken in Patch 5.4 end killed_npc_id = unit_idnum WDP:ScheduleTimer(ClearKilledNPC, 0.1) end, } local NON_DAMAGE_EVENTS = { SPELL_AURA_APPLIED = true, SPELL_AURA_REMOVED = true, SPELL_AURA_REMOVED_DOSE = true, SPELL_CAST_FAILED = true, SWING_MISSED = true, } local DAMAGE_EVENTS = { RANGE_DAMAGE = true, SPELL_BUILDING_DAMAGE = true, SPELL_DAMAGE = true, SPELL_PERIODIC_DAMAGE = true, SWING_DAMAGE = true, } function WDP:COMBAT_LOG_EVENT_UNFILTERED(event_name, time_stamp, sub_event, hide_caster, source_guid, source_name, source_flags, source_raid_flags, dest_guid, dest_name, dest_flags, dest_raid_flags, ...) local combat_log_func = COMBAT_LOG_FUNCS[sub_event] if not combat_log_func then if DAMAGE_EVENTS[sub_event] then table.wipe(previous_combat_event) previous_combat_event.source_name = source_name if source_guid ~= dest_guid and (in_instance or group_member_guids[source_guid] or group_pet_guids[source_guid]) then previous_combat_event.party_damage = true end end return end combat_log_func(sub_event, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, ...) if NON_DAMAGE_EVENTS[sub_event] then table.wipe(previous_combat_event) end end local DIPLOMACY_SPELL_ID = 20599 local MR_POP_RANK1_SPELL_ID = 78634 local MR_POP_RANK2_SPELL_ID = 78635 local REP_BUFFS = { [_G.GetSpellInfo(30754)] = "CENARION_FAVOR", [_G.GetSpellInfo(24705)] = "GRIM_VISAGE", [_G.GetSpellInfo(32098)] = "HONOR_HOLD_FAVOR", [_G.GetSpellInfo(39913)] = "NAZGRELS_FERVOR", [_G.GetSpellInfo(39953)] = "SONG_OF_BATTLE", [_G.GetSpellInfo(61849)] = "SPIRIT_OF_SHARING", [_G.GetSpellInfo(32096)] = "THRALLMARS_FAVOR", [_G.GetSpellInfo(39911)] = "TROLLBANES_COMMAND", [_G.GetSpellInfo(95987)] = "UNBURDENED", [_G.GetSpellInfo(100951)] = "WOW_ANNIVERSARY", } local FACTION_NAMES = { CENARION_CIRCLE = _G.GetFactionInfoByID(609), HONOR_HOLD = _G.GetFactionInfoByID(946), THE_SHATAR = _G.GetFactionInfoByID(935), THRALLMAR = _G.GetFactionInfoByID(947), } local MODIFIERS = { CENARION_FAVOR = { faction = FACTION_NAMES.CENARION_CIRCLE, modifier = 0.25, }, GRIM_VISAGE = { modifier = 0.1, }, HONOR_HOLD_FAVOR = { faction = FACTION_NAMES.HONOR_HOLD, modifier = 0.25, }, NAZGRELS_FERVOR = { faction = FACTION_NAMES.THRALLMAR, modifier = 0.1, }, SONG_OF_BATTLE = { faction = FACTION_NAMES.THE_SHATAR, modifier = 0.1, }, SPIRIT_OF_SHARING = { modifier = 0.1, }, THRALLMARS_FAVOR = { faction = FACTION_NAMES.THRALLMAR, modifier = 0.25, }, TROLLBANES_COMMAND = { faction = FACTION_NAMES.HONOR_HOLD, modifier = 0.1, }, UNBURDENED = { modifier = 0.1, }, WOW_ANNIVERSARY = { modifier = 0.08, } } function WDP:COMBAT_TEXT_UPDATE(event_name, message_type, faction_name, amount) if message_type ~= "FACTION" or not killed_npc_id then return end UpdateFactionData() if not faction_name or not faction_standings[faction_name] then return end local npc = NPCEntry(killed_npc_id) ClearKilledNPC() if not npc then private.harvesting = nil return end npc.harvested = private.harvesting private.harvesting = nil local modifier = 1 if _G.IsSpellKnown(DIPLOMACY_SPELL_ID) then modifier = modifier + 0.1 end if _G.IsSpellKnown(MR_POP_RANK2_SPELL_ID) then modifier = modifier + 0.1 elseif _G.IsSpellKnown(MR_POP_RANK1_SPELL_ID) then modifier = modifier + 0.05 end for buff_name, buff_label in pairs(REP_BUFFS) do if _G.UnitBuff("player", buff_name) then local modded_faction = MODIFIERS[buff_label].faction if not modded_faction or faction_name == modded_faction then modifier = modifier + MODIFIERS[buff_label].modifier end end end npc.reputations = npc.reputations or {} npc.reputations[("%s:%s"):format(faction_name, faction_standings[faction_name])] = math.floor(amount / modifier) end end -- do-block function WDP:CURSOR_UPDATE(event_name) if current_action.fishing_target or _G.Minimap:IsMouseOver() or current_action.spell_label ~= "FISHING" then return end local text = _G["GameTooltipTextLeft1"]:GetText() if not text or text == "Fishing Bobber" then text = "NONE" else current_action.fishing_target = true end current_action.identifier = ("%s:%s"):format(current_action.spell_label, text) end function WDP:ITEM_TEXT_BEGIN(event_name) local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) if not unit_idnum or unit_type ~= private.UNIT_TYPES.OBJECT or _G.UnitName("npc") ~= _G.ItemTextGetItem() then return end UpdateDBEntryLocation("objects", unit_idnum) end do local LOOT_READY_VERIFY_FUNCS = { -- Item containers can be AOE-looted in Patch 5.4.2 if the user clicks fast enough, but this verification still works as long as they both have loot. [AF.ITEM] = function() local locked_item_id for bag_index = 0, _G.NUM_BAG_FRAMES do for slot_index = 1, _G.GetContainerNumSlots(bag_index) do local _, _, is_locked, _, _, is_lootable = _G.GetContainerItemInfo(bag_index, slot_index) if is_locked and is_lootable then locked_item_id = ItemLinkToID(_G.GetContainerItemLink(bag_index, slot_index)) break end end if locked_item_id then break end end if not locked_item_id or (current_action.identifier and current_action.identifier ~= locked_item_id) then return false end current_action.identifier = locked_item_id return true end, [AF.NPC] = true, [AF.OBJECT] = true, [AF.ZONE] = function() current_action.zone_data = UpdateDBEntryLocation("zones", current_action.identifier) return _G.IsFishingLoot() end, } local LOOT_READY_UPDATE_FUNCS = { [AF.ITEM] = function() GenericLootUpdate("items") end, [AF.NPC] = function() local difficulty_token = InstanceDifficultyToken() local loot_label = current_loot.label local source_list = {} for source_guid, loot_data in pairs(current_loot.sources) do local _, source_id = ParseGUID(source_guid) local npc = NPCEntry(source_id) if npc then local encounter_data = npc:EncounterData(difficulty_token) encounter_data[loot_label] = encounter_data[loot_label] or {} if not source_list[source_guid] then encounter_data.loot_counts = encounter_data.loot_counts or {} encounter_data.loot_counts[loot_label] = (encounter_data.loot_counts[loot_label] or 0) + 1 source_list[source_guid] = true end for loot_token, quantity in pairs(loot_data) do local loot_type, currency_texture = (":"):split(loot_token) if loot_type == "currency" and currency_texture then table.insert(encounter_data[loot_label], ("currency:%d:%s"):format(quantity, currency_texture)) elseif loot_token == "money" then table.insert(encounter_data[loot_label], ("money:%d"):format(quantity)) else table.insert(encounter_data[loot_label], ("%d:%d"):format(loot_token, quantity)) end end end end end, [AF.OBJECT] = function() GenericLootUpdate("objects", InstanceDifficultyToken()) end, [AF.ZONE] = function() if not (current_loot.map_level and current_loot.x and current_loot.y and current_loot.zone_data) then return end local location_token = ("%d:%d:%d"):format(current_loot.map_level, current_loot.x, current_loot.y) -- This will start life as a boolean true. if _G.type(current_loot.zone_data[location_token]) ~= "table" then current_loot.zone_data[location_token] = { drops = {} } end local loot_count = ("%s_count"):format(current_loot.label) current_loot.zone_data[location_token][loot_count] = (current_loot.zone_data[location_token][loot_count] or 0) + 1 if current_loot.sources then for source_guid, loot_data in pairs(current_loot.sources) do for item_id, quantity in pairs(loot_data) do table.insert(current_loot.zone_data[location_token].drops, ("%d:%d"):format(item_id, quantity)) end end end if #current_loot.list <= 0 then return end for index = 1, #current_loot.list do table.insert(current_loot.zone_data[location_token].drops, current_loot.loot_list[index]) end end, } -- Prevent opening the same loot window multiple times from recording data multiple times. local loot_guid_registry = {} function WDP:LOOT_CLOSED(event_name) current_loot = nil table.wipe(current_action) end local function ExtrapolatedCurrentActionFromLootData(event_name) local extrapolated_guid_registry = {} local num_guids = 0 table.wipe(current_action) for loot_slot = 1, _G.GetNumLootItems() do local loot_info = { _G.GetLootSourceInfo(loot_slot) } for loot_index = 1, #loot_info, 2 do local source_guid = loot_info[loot_index] if not extrapolated_guid_registry[source_guid] then local unit_type, unit_idnum = ParseGUID(source_guid) if unit_type and unit_idnum then extrapolated_guid_registry[source_guid] = { unit_type, unit_idnum } num_guids = num_guids + 1 end end end end local log_source = event_name .. "- ExtrapolatedCurrentActionFromLootData" if num_guids == 0 then Debug("%s: No GUIDs found in loot. Blank loot window?", log_source) return false end if private.previous_spell_id and private.EXTRAPOLATION_BANNED_SPELL_IDS[private.previous_spell_id] then Debug("%s: Problematic spell id %s found. Loot extrapolation for this set of loot would have run an increased risk of introducing bad data into the system.", log_source, private.previous_spell_id) return false end local num_npcs = 0 local num_objects = 0 local num_itemcontainers = 0 for source_guid, guid_data in pairs(extrapolated_guid_registry) do local unit_type = guid_data[1] local loot_label = (unit_type == private.UNIT_TYPES.OBJECT) and "opening" or (UnitTypeIsNPC(unit_type) and "drops") or ((unit_type == private.UNIT_TYPES.PLAYER) and "contains") if loot_label then local unit_idnum = guid_data[2] if loot_guid_registry[loot_label] and loot_guid_registry[loot_label][source_guid] then Debug("%s: Previously scanned loot for unit with GUID %s and identifier %s.", log_source, source_guid, unit_idnum) elseif unit_type == private.UNIT_TYPES.OBJECT and unit_idnum ~= OBJECT_ID_FISHING_BOBBER then current_action.loot_label = loot_label current_action.spell_label = "OPENING" current_action.target_type = AF.OBJECT current_action.identifier = unit_idnum num_objects = num_objects + 1 elseif UnitTypeIsNPC(unit_type) then current_action.loot_label = loot_label current_action.target_type = AF.NPC current_action.identifier = unit_idnum num_npcs = num_npcs + 1 elseif unit_type == private.UNIT_TYPES.PLAYER then -- Item container GUIDs are currently of the 'PLAYER' type; this may be unintended and could change in the future. current_action.loot_label = loot_label current_action.target_type = AF.ITEM -- current_action.identifier assigned during loot verification. num_itemcontainers = num_itemcontainers + 1 end else -- Bail here; not only do we not know what this unit is, but we don't want to attribute loot to something that doesn't actually drop it. Debug("%s: Unit with GUID %s has unsupported type for looting.", log_source, source_guid) return false end end if not current_action.target_type then Debug("%s: Failure to obtain target_type.", log_source) return false end -- We can't figure out what dropped the loot. This shouldn't ever happen, but hey - Blizzard does some awesome stuff on occasion. if (num_npcs > 0 and num_objects + num_itemcontainers > 0) or (num_objects > 0 and num_npcs + num_itemcontainers > 0) or (num_itemcontainers > 0 and num_npcs + num_objects > 0) then Debug("%s: Mixed target types are not supported. NPCs - %d, Objects - %d, Item Containers - %d.", log_source, num_npcs, num_objects, num_itemcontainers) return false end Debug("%s: Successfully extrapolated information for current_action.", log_source) return true end function WDP:LOOT_READY(event_name) if current_loot then current_loot = nil Debug("%s: Previous loot did not process in time for this event. Attempting to extrapolate current_action from loot data.", event_name) if not ExtrapolatedCurrentActionFromLootData(event_name) then Debug("%s: Unable to extrapolate current_action. Aborting attempts to handle loot for now.", event_name) return end end if not current_action.target_type then Debug("%s: No target type found. Attempting to extrapolate current_action from loot data.", event_name) if not ExtrapolatedCurrentActionFromLootData(event_name) then Debug("%s: Unable to extrapolate current_action. Aborting attempts to handle loot for now.", event_name) return end end local verify_func = LOOT_READY_VERIFY_FUNCS[current_action.target_type] local update_func = LOOT_READY_UPDATE_FUNCS[current_action.target_type] if not verify_func or not update_func then Debug("%s: The current action's target type is unsupported or nil.", event_name) return end if _G.type(verify_func) == "function" and not verify_func() then Debug("%s: The current action type, %s, is supported but has failed loot verification.", event_name, private.ACTION_TYPE_NAMES[current_action.target_type]) return end local guids_used = {} InitializeCurrentLoot() loot_guid_registry[current_loot.label] = loot_guid_registry[current_loot.label] or {} for loot_slot = 1, _G.GetNumLootItems() do local icon_texture, item_text, slot_quantity, quality, locked = _G.GetLootSlotInfo(loot_slot) local slot_type = _G.GetLootSlotType(loot_slot) local loot_info = { _G.GetLootSourceInfo(loot_slot) } -- Odd index is GUID, even is count. for loot_index = 1, #loot_info, 2 do local source_guid = loot_info[loot_index] if not loot_guid_registry[current_loot.label][source_guid] then local loot_quantity = loot_info[loot_index + 1] -- There is a new bug in 5.4.0 that causes GetLootSlotInfo() to (rarely) return nil values for slot_quantity. if slot_quantity then -- We need slot_quantity to account for an old bug where loot_quantity is sometimes '1' for stacks of items, such as cloth. if slot_quantity > loot_quantity then loot_quantity = slot_quantity end local source_type, source_id = ParseGUID(source_guid) local source_key = ("%s:%d"):format(source_type, source_id) if slot_type == _G.LOOT_SLOT_ITEM then local item_id = ItemLinkToID(_G.GetLootSlotLink(loot_slot)) Debug("GUID: %s - Type:ID: %s - ItemID: %d - Amount: %d (%d)", loot_info[loot_index], source_key, item_id, loot_info[loot_index + 1], slot_quantity) current_loot.sources[source_guid] = current_loot.sources[source_guid] or {} current_loot.sources[source_guid][item_id] = current_loot.sources[source_guid][item_id] or 0 + loot_quantity guids_used[source_guid] = true elseif slot_type == _G.LOOT_SLOT_MONEY then Debug("GUID: %s - Type:ID: %s - Money - Amount: %d (%d)", loot_info[loot_index], source_key, loot_info[loot_index + 1], slot_quantity) if current_loot.target_type == AF.ZONE then table.insert(current_loot.list, ("money:%d"):format(loot_quantity)) else current_loot.sources[source_guid] = current_loot.sources[source_guid] or {} current_loot.sources[source_guid]["money"] = current_loot.sources[source_guid]["money"] or 0 + loot_quantity guids_used[source_guid] = true end elseif slot_type == _G.LOOT_SLOT_CURRENCY then -- Same bug with GetLootSlotInfo() will screw up currency when it happens, so we won't process this slot's loot. if icon_texture then Debug("GUID: %s - Type:ID: %s - Currency: %s - Amount: %d (%d)", loot_info[loot_index], source_key, icon_texture, loot_info[loot_index + 1], slot_quantity) if current_loot.target_type == AF.ZONE then table.insert(current_loot.list, ("currency:%d:%s"):format(loot_quantity, icon_texture:match("[^\\]+$"):lower())) else local currency_token = ("currency:%s"):format(icon_texture:match("[^\\]+$"):lower()) current_loot.sources[source_guid] = current_loot.sources[source_guid] or {} current_loot.sources[source_guid][currency_token] = current_loot.sources[source_guid][currency_token] or 0 + loot_quantity guids_used[source_guid] = true end else Debug("%s: Slot quantity is nil for loot slot %d of the entity with GUID %s and Type:ID: %s.", event_name, loot_slot, loot_info[loot_index], source_key) end end else -- If this is nil, then the item's quantity could be wrong if loot_quantity is wrong, so we won't process this slot's loot. Debug("%s: Slot quantity is nil for loot slot %d of the entity with GUID %s and Type:ID: %s.", event_name, loot_slot, loot_info[loot_index], source_key) end end end end for guid in pairs(guids_used) do loot_guid_registry[current_loot.label][guid] = true end update_func() end end -- do-block function WDP:MAIL_SHOW(event_name) local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) if not unit_idnum or unit_type ~= private.UNIT_TYPES.OBJECT then return end UpdateDBEntryLocation("objects", unit_idnum) end do local POINT_MATCH_PATTERNS = { ("^%s$"):format(_G.ITEM_REQ_ARENA_RATING:gsub("%%d", "(%%d+)")), -- May no longer be necessary ("^%s$"):format(_G.ITEM_REQ_ARENA_RATING_3V3:gsub("%%d", "(%%d+)")), -- May no longer be necessary ("^%s$"):format(_G.ITEM_REQ_ARENA_RATING_5V5:gsub("%%d", "(%%d+)")), -- May no longer be necessary ("^%s$"):format(_G.ITEM_REQ_ARENA_RATING_BG:gsub("%%d", "(%%d+)")), ("^%s$"):format(_G.ITEM_REQ_ARENA_RATING_3V3_BG:gsub("%%d", "(%%d+)")), } local ITEM_REQ_REPUTATION_MATCH = "Requires (.-) %- (.*)" local ITEM_REQ_QUEST_MATCH1 = "Requires: .*" local ITEM_REQ_QUEST_MATCH2 = "Must have completed: .*" local current_merchant local merchant_standing function WDP:MERCHANT_CLOSED(event_name) current_merchant = nil merchant_standing = nil end function WDP:UpdateMerchantItems(event_name) if not current_merchant or event_name == "MERCHANT_SHOW" then local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) if not unit_idnum or not UnitTypeIsNPC(unit_type) then return end local _, faction_standing = UnitFactionStanding("npc") merchant_standing = faction_standing current_merchant = NPCEntry(unit_idnum) current_merchant.sells = current_merchant.sells or {} end local current_filters = _G.GetMerchantFilter() _G.SetMerchantFilter(_G.LE_LOOT_FILTER_ALL) _G.MerchantFrame_Update() local num_items = _G.GetMerchantNumItems() for item_index = 1, num_items do local _, _, copper_price, stack_size, num_available, _, extended_cost = _G.GetMerchantItemInfo(item_index) local item_id = ItemLinkToID(_G.GetMerchantItemLink(item_index)) DatamineTT:ClearLines() DatamineTT:SetMerchantItem(item_index) if not item_id then local item_name, item_link = DatamineTT:GetItem() item_id = ItemLinkToID(item_link) if item_id then Debug("%s: GetMerchantItemLink() still ocassionally fails, apparently. Failed item's ID - %s", event_name, item_id) else Debug("%s: GetMerchantItemLink() still ocassionally fails, apparently. Failed item's ID - nil", event_name) end end if item_id and item_id > 0 then local price_string = ActualCopperCost(copper_price, merchant_standing) local num_lines = DatamineTT:NumLines() for line_index = 1, num_lines do local current_line = _G["WDPDatamineTTTextLeft" .. line_index] if not current_line then break end local faction, reputation = current_line:GetText():match(ITEM_REQ_REPUTATION_MATCH) if faction or reputation then DBEntry("items", item_id).req_reputation = ("%s:%s"):format(faction:gsub("-", ""), reputation:upper()) break end end for line_index = 1, num_lines do local current_line = _G["WDPDatamineTTTextLeft" .. line_index] if not current_line then break end local line_text = current_line:GetText() local quest_name = line_text:match(ITEM_REQ_QUEST_MATCH1) or line_text:match(ITEM_REQ_QUEST_MATCH2) if quest_name then DBEntry("items", item_id).req_quest = ("%s"):format(quest_name:gsub("(.+): ", ""), quest_name) break end end if extended_cost then local battleground_rating = 0 local personal_rating = 0 local required_season_amount for line_index = 1, num_lines do local current_line = _G["WDPDatamineTTTextLeft" .. line_index] if not current_line then break end required_season_amount = current_line:GetText():match("Requires earning a total of (%d+)\n(.-) for the season.") for match_index = 1, #POINT_MATCH_PATTERNS do local match1, match2 = current_line:GetText():match(POINT_MATCH_PATTERNS[match_index]) personal_rating = personal_rating + (match1 or 0) battleground_rating = battleground_rating + (match2 or 0) if match1 or match2 then break end end end local currency_list = {} local item_count = _G.GetMerchantItemCostInfo(item_index) -- Keeping this around in case Blizzard makes the two points diverge at some point. -- price_string = ("%s:%s:%s:%s"):format(price_string, battleground_rating, personal_rating, required_season_amount or 0) price_string = ("%s:%s:%s"):format(price_string, personal_rating, required_season_amount or 0) for cost_index = 1, item_count do -- The third return (Blizz calls "currency_link") of GetMerchantItemCostItem only returns item links as of Patch 5.3.0. local icon_texture, amount_required, item_link = _G.GetMerchantItemCostItem(item_index, cost_index) local currency_identifier = item_link and ItemLinkToID(item_link) or nil if (not currency_identifier or currency_identifier < 1) and icon_texture then currency_identifier = icon_texture:match("[^\\]+$"):lower() end if currency_identifier then currency_list[#currency_list + 1] = ("(%s:%s)"):format(amount_required, currency_identifier) end end for currency_index = 1, #currency_list do price_string = ("%s:%s"):format(price_string, currency_list[currency_index]) end end current_merchant.sells[item_id] = ("%s:%s:[%s]"):format(num_available, stack_size, price_string) end end if _G.CanMerchantRepair() then current_merchant.can_repair = true end _G.SetMerchantFilter(current_filters) _G.MerchantFrame_Update() end end -- do-block function WDP:PET_BAR_UPDATE(event_name) if current_action.spell_label ~= "MIND_CONTROL" then return end local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("pet")) if not unit_idnum or not UnitTypeIsNPC(unit_type) then return end NPCEntry(unit_idnum).mind_control = true table.wipe(current_action) end function WDP:PET_JOURNAL_LIST_UPDATE(event_name) -- This function produces data currently unused by wowdb.com and it makes debugging errors in the .lua output nearly impossible due to the massive bloat. if DEBUGGING then return end local num_pets = LPJ:NumPets() for index, pet_id in LPJ:IteratePetIDs() do local _, _, is_owned, _, level, _, _, name, icon, pet_type, npc_id, _, _, is_wild = _G.C_PetJournal.GetPetInfoByIndex(index) if is_owned then local health, max_health, attack, speed, rarity = _G.C_PetJournal.GetPetStats(pet_id) if rarity then local rarity_name = _G["BATTLE_PET_BREED_QUALITY" .. rarity] local npc = NPCEntry(npc_id) npc.wild_pet = is_wild or nil npc.battle_pet_data = npc.battle_pet_data or {} npc.battle_pet_data[rarity_name] = npc.battle_pet_data[rarity_name] or {} npc.battle_pet_data[rarity_name]["level_" .. level] = npc.battle_pet_data[rarity_name]["level_" .. level] or {} local data = npc.battle_pet_data[rarity_name]["level_" .. level] data.max_health = max_health data.attack = attack data.speed = speed end end end end function WDP:PLAYER_REGEN_DISABLED(event_name) private.in_combat = true end function WDP:PLAYER_REGEN_ENABLED(event_name) private.in_combat = nil if private.set_area_id then self:HandleZoneChange(event_name) private.set_area_id = nil end end function WDP:PLAYER_TARGET_CHANGED(event_name) if not TargetedNPC() then return end current_action.target_type = AF.NPC self:UpdateTargetLocation() end do local function UpdateQuestJuncture(point) local unit_name = _G.UnitName("questnpc") if not unit_name then return end local unit_type, unit_id = ParseGUID(_G.UnitGUID("questnpc")) if unit_type == private.UNIT_TYPES.OBJECT then UpdateDBEntryLocation("objects", unit_id) end local quest = DBEntry("quests", _G.GetQuestID()) quest[point] = quest[point] or {} quest[point][("%s:%d"):format(private.UNIT_TYPE_NAMES[unit_type], unit_id)] = true return quest end function WDP:QUEST_COMPLETE(event_name) local quest = UpdateQuestJuncture("end") if ALLOWED_LOCALES[CLIENT_LOCALE] then quest.reward_text = ReplaceKeywords(_G.GetRewardText()) end -- Make sure the quest NPC isn't erroneously recorded as giving reputation for quests which award it. ClearKilledNPC() end function WDP:QUEST_DETAIL(event_name) local quest = UpdateQuestJuncture("begin") if not quest then return end quest.classes = quest.classes or {} quest.classes[PLAYER_CLASS] = true quest.races = quest.races or {} quest.races[(PLAYER_RACE == "Pandaren") and ("%s_%s"):format(PLAYER_RACE, PLAYER_FACTION or "Neutral") or PLAYER_RACE] = true end end -- do-block function WDP:QUEST_LOG_UPDATE(event_name) local selected_quest = _G.GetQuestLogSelection() -- Save current selection to be restored when we're done. local entry_index, processed_quests = 1, 0 local _, num_quests = _G.GetNumQuestLogEntries() while processed_quests <= num_quests do local _, _, _, is_header, _, _, _, quest_id = _G.GetQuestLogTitle(entry_index) if quest_id == 0 then processed_quests = processed_quests + 1 elseif not is_header then _G.SelectQuestLogEntry(entry_index); local quest = DBEntry("quests", quest_id) quest.timer = _G.GetQuestLogTimeLeft() quest.can_share = _G.GetQuestLogPushable() and true or nil processed_quests = processed_quests + 1 end entry_index = entry_index + 1 end _G.SelectQuestLogEntry(selected_quest) self:UnregisterEvent("QUEST_LOG_UPDATE") end function WDP:QUEST_PROGRESS(event_name) if not ALLOWED_LOCALES[CLIENT_LOCALE] then return end DBEntry("quests", _G.GetQuestID()).progress_text = ReplaceKeywords(_G.GetProgressText()) end function WDP:UNIT_QUEST_LOG_CHANGED(event_name, unit_id) if unit_id ~= "player" then return end self:RegisterEvent("QUEST_LOG_UPDATE") end do local TRADESKILL_TOOLS = { Anvil = anvil_spell_ids, Forge = forge_spell_ids, } local function RegisterTools(tradeskill_name, tradeskill_index) local spell_id = tonumber(_G.GetTradeSkillRecipeLink(tradeskill_index):match("^|c%x%x%x%x%x%x%x%x|H%w+:(%d+)")) local required_tool = _G.GetTradeSkillTools(tradeskill_index) if required_tool then for tool_name, registry in pairs(TRADESKILL_TOOLS) do if required_tool:find(tool_name) then registry[spell_id] = true end end end end function WDP:TRADE_SKILL_SHOW(event_name) local profession_name, prof_level = _G.GetTradeSkillLine() if profession_name == _G.UNKNOWN then return end TradeSkillExecutePer(RegisterTools) end end -- do-block function WDP:TRAINER_CLOSED(event_name) private.trainer_shown = nil end function WDP:TRAINER_SHOW(event_name) local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) local trainer = NPCEntry(unit_idnum) if not trainer then return end local _, trainer_standing = UnitFactionStanding("npc") trainer.teaches = trainer.teaches or {} private.trainer_shown = true -- Get the initial trainer filters local available = _G.GetTrainerServiceTypeFilter("available") local unavailable = _G.GetTrainerServiceTypeFilter("unavailable") local used = _G.GetTrainerServiceTypeFilter("used") -- Clear the trainer filters _G.SetTrainerServiceTypeFilter("available", true) _G.SetTrainerServiceTypeFilter("unavailable", true) _G.SetTrainerServiceTypeFilter("used", true) for index = 1, _G.GetNumTrainerServices(), 1 do local spell_name, rank_name, _, _, required_level = _G.GetTrainerServiceInfo(index) if spell_name then DatamineTT:ClearLines() DatamineTT:SetTrainerService(index) local _, _, spell_id = DatamineTT:GetSpell() if spell_id then local class_professions = trainer.teaches[PLAYER_CLASS] if not class_professions then trainer.teaches[PLAYER_CLASS] = {} class_professions = trainer.teaches[PLAYER_CLASS] end local profession, min_skill = _G.GetTrainerServiceSkillReq(index) profession = profession or "General" local profession_skills = class_professions[profession] if not profession_skills then class_professions[profession] = {} profession_skills = class_professions[profession] end profession_skills[spell_id] = ("%d:%d:%d"):format(required_level, min_skill, _G.GetTrainerServiceCost(index)) end end end -- Reset the filters to what they were before _G.SetTrainerServiceTypeFilter("available", available or false) _G.SetTrainerServiceTypeFilter("unavailable", unavailable or false) _G.SetTrainerServiceTypeFilter("used", used or false) end function WDP:UNIT_SPELLCAST_SENT(event_name, unit_id, spell_name, spell_rank, target_name, spell_line) if private.tracked_line or unit_id ~= "player" then return end local spell_label = private.SPELL_LABELS_BY_NAME[spell_name] if not spell_label then return end local item_name, item_link = _G.GameTooltip:GetItem() local unit_name, unit_id = _G.GameTooltip:GetUnit() if not unit_name and _G.UnitName("target") == target_name then unit_name = target_name unit_id = "target" end local spell_flags = private.SPELL_FLAGS_BY_LABEL[spell_label] local zone_name, area_id, x, y, map_level, instance_token = CurrentLocationData() if not (zone_name and area_id and x and y and map_level) then Debug("%s: Missing current location data - %s, %d, %d, %d, %d.", event_name, zone_name, area_id, x, y, map_level) return end table.wipe(current_action) current_action.instance_token = instance_token current_action.map_level = map_level current_action.x = x current_action.y = y current_action.zone_data = ("%s:%d"):format(zone_name, area_id) current_action.spell_label = spell_label if not private.NON_LOOT_SPELL_LABELS[spell_label] then current_action.loot_label = spell_label:lower() end if unit_name and unit_name == target_name and not item_name then if bit.band(spell_flags, AF.NPC) == AF.NPC then if not unit_id or unit_name ~= target_name then return end current_action.target_type = AF.NPC end elseif bit.band(spell_flags, AF.ITEM) == AF.ITEM then current_action.target_type = AF.ITEM if item_name and item_name == target_name then current_action.identifier = ItemLinkToID(item_link) elseif target_name and target_name ~= "" then local _, item_link = _G.GetItemInfo(target_name) current_action.identifier = ItemLinkToID(item_link) end elseif not item_name and not unit_name then if bit.band(spell_flags, AF.OBJECT) == AF.OBJECT then if target_name == "" then return end current_action.object_name = target_name current_action.target_type = AF.OBJECT elseif bit.band(spell_flags, AF.ZONE) == AF.ZONE then current_action.target_type = AF.ZONE end end private.tracked_line = spell_line end function WDP:SPELL_CONFIRMATION_PROMPT(event_name, spell_id, confirm_type, text, duration, currency_id_cost) if private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] then ClearKilledBossID() ClearLootToastContainerID() private.raid_finder_boss_id = private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] elseif private.WORLD_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] then ClearKilledBossID() ClearLootToastContainerID() private.world_boss_id = private.WORLD_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] else Debug("%s: Spell ID %d is not a known raid or world boss 'Bonus' spell.", event_name, spell_id) return end -- Assign existing loot data to boss if it exists if loot_toast_data then local npc_id = private.raid_finder_boss_id or private.world_boss_id -- Slightly messy hack to workaround duplicate world bosses local upper_limit = 0 if DUPLICATE_WORLD_BOSS_IDS[npc_id] then upper_limit = #DUPLICATE_WORLD_BOSS_IDS[npc_id] end for i = 0, upper_limit do local temp_npc_id = npc_id if i > 0 then temp_npc_id = DUPLICATE_WORLD_BOSS_IDS[npc_id][i] end local npc = NPCEntry(temp_npc_id) if npc then -- Create needed npc fields if required local loot_label = "drops" local encounter_data = npc:EncounterData(InstanceDifficultyToken()) encounter_data[loot_label] = encounter_data[loot_label] or {} encounter_data.loot_counts = encounter_data.loot_counts or {} for index = 1, #loot_toast_data do local data = loot_toast_data[index] local loot_type = data[1] local hyperlink = data[2] local quantity = data[3] if loot_type == "item" then local item_id = ItemLinkToID(hyperlink) Debug("%s: Assigned stored item loot data - %s - %d:%d", event_name, hyperlink, item_id, quantity) table.insert(encounter_data[loot_label], ("%d:%d"):format(item_id, quantity)) elseif loot_type == "money" then Debug("%s: Assigned stored money loot data - money:%d", event_name, quantity) table.insert(encounter_data[loot_label], ("money:%d"):format(quantity)) elseif loot_type == "currency" then local currency_texture = CurrencyLinkToTexture(hyperlink) Debug("%s: Assigned stored currency loot data - %s - currency:%d:%s", event_name, hyperlink, currency_texture, quantity) -- Workaround for Patch 5.4.0 bug with Flexible raid Siege of Orgrimmar bosses and Valor Points if quantity > 1000 and currency_texture == "pvecurrency-valor" then quantity = math.floor(quantity / 100) end table.insert(encounter_data[loot_label], ("currency:%d:%s"):format(quantity, currency_texture)) end end if not boss_loot_toasting[temp_npc_id] then encounter_data.loot_counts[loot_label] = (encounter_data.loot_counts[loot_label] or 0) + 1 boss_loot_toasting[temp_npc_id] = true -- Do not count further loots until timer expires or another boss is killed end else Debug("%s: NPC is nil, but we have stored loot data...", event_name) end end end ClearLootToastData() killed_boss_id_timer_handle = WDP:ScheduleTimer(ClearKilledBossID, 5) -- we need to assign a handle here to cancel it later end function WDP:UNIT_SPELLCAST_SUCCEEDED(event_name, unit_id, spell_name, spell_rank, spell_line, spell_id) if unit_id ~= "player" then return end private.tracked_line = nil private.previous_spell_id = spell_id if private.LOOT_SPELL_ID_TO_ITEM_ID_MAP[spell_id] then ClearKilledBossID() ClearLootToastContainerID() ClearLootToastData() private.loot_toast_container_id = private.LOOT_SPELL_ID_TO_ITEM_ID_MAP[spell_id] loot_toast_container_timer_handle = WDP:ScheduleTimer(ClearLootToastContainerID, 1) -- we need to assign a handle here to cancel it later end if anvil_spell_ids[spell_id] then UpdateDBEntryLocation("objects", OBJECT_ID_ANVIL) elseif forge_spell_ids[spell_id] then UpdateDBEntryLocation("objects", OBJECT_ID_FORGE) elseif spell_name:match("^Harvest.+") then killed_npc_id = current_target_id private.harvesting = true end end function WDP:HandleSpellFailure(event_name, unit_id, spell_name, spell_rank, spell_line, spell_id) if unit_id ~= "player" then return end if private.tracked_line == spell_line then private.tracked_line = nil end table.wipe(current_action) end do local function SetUnitField(field, required_type) local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) if not unit_idnum or (required_type and unit_type ~= required_type) then return end if UnitTypeIsNPC(unit_type) then NPCEntry(unit_idnum)[field] = true elseif unit_type == private.UNIT_TYPES.OBJECT then DBEntry("objects", unit_idnum)[field] = true UpdateDBEntryLocation("objects", unit_idnum) end end function WDP:AUCTION_HOUSE_SHOW(event_name) SetUnitField("auctioneer", private.UNIT_TYPES.NPC) end function WDP:BANKFRAME_OPENED(event_name) SetUnitField("banker", private.UNIT_TYPES.NPC) end function WDP:BATTLEFIELDS_SHOW(event_name) SetUnitField("battlemaster", private.UNIT_TYPES.NPC) end function WDP:FORGE_MASTER_OPENED(event_name) SetUnitField("arcane_reforger", private.UNIT_TYPES.NPC) end local GOSSIP_SHOW_FUNCS = { [private.UNIT_TYPES.NPC] = function(unit_idnum) local gossip_options = { _G.GetGossipOptions() } for index = 2, #gossip_options, 2 do if gossip_options[index] == "binder" then SetUnitField("innkeeper", private.UNIT_TYPES.NPC) return end end end, [private.UNIT_TYPES.OBJECT] = function(unit_idnum) UpdateDBEntryLocation("objects", unit_idnum) end, } function WDP:GOSSIP_SHOW(event_name) local unit_type, unit_idnum = ParseGUID(_G.UnitGUID("npc")) if not unit_idnum then return end if GOSSIP_SHOW_FUNCS[unit_type] then GOSSIP_SHOW_FUNCS[unit_type](unit_idnum) end end function WDP:GUILDBANKFRAME_OPENED(event_name) SetUnitField("guild_bank", private.UNIT_TYPES.OBJECT) end function WDP:ITEM_UPGRADE_MASTER_OPENED(event_name) SetUnitField("item_upgrade_master", private.UNIT_TYPES.NPC) end function WDP:TAXIMAP_OPENED(event_name) SetUnitField("flight_master", private.UNIT_TYPES.NPC) end function WDP:TRANSMOGRIFY_OPEN(event_name) SetUnitField("transmogrifier", private.UNIT_TYPES.NPC) end function WDP:VOID_STORAGE_OPEN(event_name) SetUnitField("void_storage", private.UNIT_TYPES.NPC) end end -- do-block