view Main.lua @ 22:7e4ce6371608

Take instance difficulty into account for locations. Record locations based on GUID within the NPC id so only one set of location data exists per unique NPC.
author James D. Callahan III <jcallahan@curse.com>
date Thu, 10 May 2012 12:00:50 -0500
parents a82c5d1134db
children 2ff0171bddae
line wrap: on
line source
-----------------------------------------------------------------------
-- Upvalued Lua API.
-----------------------------------------------------------------------
local _G = getfenv(0)

local pairs = _G.pairs
local tonumber = _G.tonumber

local bit = _G.bit
local math = _G.math
local table = _G.table


-----------------------------------------------------------------------
-- AddOn namespace.
-----------------------------------------------------------------------
local ADDON_NAME, private = ...

local LibStub = _G.LibStub
local WDP = LibStub("AceAddon-3.0"):NewAddon(ADDON_NAME, "AceEvent-3.0", "AceTimer-3.0")

local DatamineTT = _G.CreateFrame("GameTooltip", "WDPDatamineTT", _G.UIParent, "GameTooltipTemplate")
DatamineTT:SetOwner(_G.WorldFrame, "ANCHOR_NONE")


-----------------------------------------------------------------------
-- Local constants.
-----------------------------------------------------------------------
local DATABASE_DEFAULTS = {
    global = {
        items = {},
        npcs = {},
        objects = {},
        quests = {},
        zones = {},
    }
}


local EVENT_MAPPING = {
    COMBAT_TEXT_UPDATE = true,
    LOOT_CLOSED = true,
    LOOT_OPENED = true,
    MERCHANT_SHOW = "UpdateMerchantItems",
    MERCHANT_UPDATE = "UpdateMerchantItems",
    PLAYER_TARGET_CHANGED = true,
    QUEST_COMPLETE = true,
    QUEST_DETAIL = true,
    QUEST_LOG_UPDATE = 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,
}


local AF = private.ACTION_TYPE_FLAGS


-----------------------------------------------------------------------
-- Local variables.
-----------------------------------------------------------------------
local db
local durability_timer_handle
local target_location_timer_handle
local action_data = {}
local faction_names = {}


-----------------------------------------------------------------------
-- Helper Functions.
-----------------------------------------------------------------------
local function DBEntry(data_type, unit_id)
    if not data_type or not unit_id then
        return
    end
    local unit = db[data_type][unit_id]

    if not unit then
        db[data_type][unit_id] = {}
        unit = db[data_type][unit_id]
    end
    return unit
end


local function InstanceDifficultyToken()
    local _, instance_type, instance_difficulty, difficulty_name, _, _, is_dynamic = _G.GetInstanceInfo()
    if difficulty_name == "" then
        difficulty_name = "NONE"
    end
    return ("%s:%s:%s"):format(instance_type:upper(), difficulty_name:upper():gsub(" ", "_"), _G.tostring(is_dynamic))
end


local function CurrentLocationData()
    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
    return _G.GetRealZoneText(), ("%.2f"):format(x * 100), ("%.2f"):format(y * 100), map_level or 0, InstanceDifficultyToken()
end


local function ItemLinkToID(item_link)
    if not item_link then
        return
    end
    return tonumber(item_link:match("item:(%d+)"))
end


do
    local UNIT_TYPE_BITMASK = 0x007

    function WDP:ParseGUID(guid)
        if not guid then
            return
        end
        local types = private.UNIT_TYPES
        local unit_type = _G.bit.band(tonumber(guid:sub(1, 5)), UNIT_TYPE_BITMASK)

        if unit_type ~= types.PLAYER and unit_type ~= types.PET then
            return unit_type, tonumber(guid:sub(-12, -9), 16)
        end

        return unit_type
    end
end -- do-block


local function UpdateObjectLocation(identifier)
    if not identifier then
        return
    end
    local zone_name, x, y, map_level, instance_token = CurrentLocationData()
    local object = DBEntry("objects", identifier)
    object.locations = object.locations or {}

    if not object.locations[zone_name] then
        object.locations[zone_name] = {}
    end
    object.locations[zone_name][("%s:%s:%s:%s"):format(instance_token, map_level, x, y)] = true
end


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
            break
        end

        if current_line:GetText() == _G.ITEM_OPENABLE then
            table.wipe(action_data)
            action_data.type = AF.ITEM
            action_data.item_id = item_id
            action_data.loot_type = "contains"
            break
        end
    end
end


local function UpdateFactionNames()
    for faction_index = 1, 1000 do
        local faction_name, _, _, _, _, _, _, _, is_header = _G.GetFactionInfo(faction_index)

        if faction_name and not is_header then
            faction_names[faction_name] = true
        elseif not faction_name then
            break
        end
    end
end


-----------------------------------------------------------------------
-- Methods.
-----------------------------------------------------------------------
function WDP:OnInitialize()
    db = LibStub("AceDB-3.0"):New("WoWDBProfilerData", DATABASE_DEFAULTS, "Default").global

    local raw_db = _G["WoWDBProfilerData"]

    local build_num = tonumber(private.build_num)

    if raw_db.build_num and raw_db.build_num < build_num then
        for entry in pairs(DATABASE_DEFAULTS.global) do
            db[entry] = {}
        end
        raw_db.build_num = build_num
    elseif not raw_db.build_num then
        raw_db.build_num = build_num
    end
end


function WDP:OnEnable()
    for event_name, mapping in pairs(EVENT_MAPPING) do
        self:RegisterEvent(event_name, (_G.type(mapping) ~= "boolean") and mapping or nil)
    end
    durability_timer_handle = self:ScheduleRepeatingTimer("ProcessDurability", 30)
    target_location_timer_handle = self:ScheduleRepeatingTimer("UpdateTargetLocation", 0.2)

    _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)
end


local function RecordDurability(item_id, durability)
    if not durability or durability <= 0 then
        return
    end

    if not db.items[item_id] then
        db.items[item_id] = {}
    end
    db.items[item_id].durability = durability
end


function WDP:ProcessDurability()
    for slot_index = 0, _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)
            RecordDurability(item_id, 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)
                RecordDurability(item_id, max_durability)
            end
        end
    end
end


function WDP:UpdateTargetLocation()
    local is_dead = _G.UnitIsDead("target")

    if not _G.UnitExists("target") or _G.UnitPlayerControlled("target") or (_G.UnitIsTapped("target") and not is_dead) then
        return
    end

    for index = 1, 4 do
        if not _G.CheckInteractDistance("target", index) then
            return
        end
    end
    local target_guid = _G.UnitGUID("target")
    local unit_type, unit_idnum = self:ParseGUID(target_guid)

    if unit_type ~= private.UNIT_TYPES.NPC or not unit_idnum then
        return
    end
    local zone_name, x, y, map_level, instance_token = CurrentLocationData()
    local npc_data = DBEntry("npcs", unit_idnum).encounter_data[("level_%d"):format(_G.UnitLevel("target"))]
    npc_data.locations = npc_data.locations or {}

    if not npc_data.locations[zone_name] then
        npc_data.locations[zone_name] = {}
    end

    -- Only record corpse location if there is no entry for this GUID.
    if is_dead and npc_data.locations[zone_name][target_guid] then
        return
    end
    npc_data.locations[zone_name][target_guid] = ("%s:%s:%s:%s"):format(instance_token, map_level, x, y)
end


-----------------------------------------------------------------------
-- Event handlers.
-----------------------------------------------------------------------
function WDP:COMBAT_TEXT_UPDATE(event, message_type, faction_name, amount)
    local npc = DBEntry("npcs", action_data.id_num)

    if not npc then
        return
    end
    npc.encounter_data[action_data.npc_level].reputations = npc.encounter_data[action_data.npc_level].reputations or {}
    npc.encounter_data[action_data.npc_level].reputations[faction_name] = amount
end


function WDP:LOOT_CLOSED()
    --    table.wipe(action_data)
end


do
    local re_gold = _G.GOLD_AMOUNT:gsub("%%d", "(%%d+)")
    local re_silver = _G.SILVER_AMOUNT:gsub("%%d", "(%%d+)")
    local re_copper = _G.COPPER_AMOUNT:gsub("%%d", "(%%d+)")


    local function _moneyMatch(money, re)
        return money:match(re) or 0
    end


    local function _toCopper(money)
        if not money then
            return 0
        end

        return _moneyMatch(money, re_gold) * 10000 + _moneyMatch(money, re_silver) * 100 + _moneyMatch(money, re_copper)
    end


    local LOOT_VERIFY_FUNCS = {
        [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 = _G.GetContainerItemInfo(bag_index, slot_index)

                    if is_locked then
                        locked_item_id = ItemLinkToID(_G.GetContainerItemLink(bag_index, slot_index))
                    end
                end
            end

            if not locked_item_id or (action_data.item_id and action_data.item_id ~= locked_item_id) then
                return false
            end
            action_data.item_id = locked_item_id
            return true
        end,
        [AF.NPC] = function()
            if not _G.UnitExists("target") or _G.UnitIsFriend("player", "target") or _G.UnitIsPlayer("target") or _G.UnitPlayerControlled("target") then
                return false
            end
            local unit_type, id_num = WDP:ParseGUID(_G.UnitGUID("target"))
            action_data.id_num = id_num
            return true
        end,
        [AF.OBJECT] = true,
        [AF.ZONE] = function()
            return action_data.loot_type and _G.IsFishingLoot()
        end,
    }


    local function GenericLootUpdate(data_type)
        local entry = DBEntry(data_type, action_data.id_num)

        if not entry then
            return
        end
        local loot_type = action_data.loot_type or "drops"
        entry[loot_type] = entry[loot_type] or {}

        for index = 1, #action_data.loot_list do
            table.insert(entry[loot_type], action_data.loot_list[index])
        end
    end


    local LOOT_UPDATE_FUNCS = {
        [AF.ITEM] = function()
            local item = DBEntry("items", action_data.item_id)
            local loot_type = action_data.loot_type or "drops"
            item[loot_type] = item[loot_type] or {}

            for index = 1, #action_data.loot_list do
                table.insert(item[loot_type], action_data.loot_list[index])
            end
        end,
        [AF.NPC] = function()
            GenericLootUpdate("npcs")
        end,
        [AF.OBJECT] = function()
            GenericLootUpdate("objects")
        end,
        [AF.ZONE] = function()
            local loot_type = action_data.loot_type or "drops"
            local zone = DBEntry("zones", action_data.zone)
            zone[loot_type] = zone[loot_type] or {}

            local location_data = ("%s:%s:%s:%s"):format(action_data.instance_token, action_data.map_level, action_data.x, action_data.y)
            local loot_data = zone[loot_type][location_data]

            if not loot_data then
                zone[loot_type][location_data] = {}
                loot_data = zone[loot_type][location_data]
            end

            for index = 1, #action_data.loot_list do
                table.insert(loot_data, action_data.loot_list[index])
            end
        end,
    }


    function WDP:LOOT_OPENED()
        if action_data.looting then
            return
        end

        if not action_data.type then
            action_data.type = AF.NPC
        end
        local verify_func = LOOT_VERIFY_FUNCS[action_data.type]
        local update_func = LOOT_UPDATE_FUNCS[action_data.type]

        if not verify_func or not update_func then
            return
        end

        if _G.type(verify_func) == "function" and not verify_func() then
            return
        end
        -- TODO: Remove this check once the MoP client goes live
        local wow_version = private.wow_version
        local loot_registry = {}
        action_data.loot_list = {}
        action_data.looting = true

        if wow_version == "5.0.1" then
            for loot_slot = 1, _G.GetNumLootItems() do
                local icon_texture, item_text, quantity, quality, locked = _G.GetLootSlotInfo(loot_slot)

                local slot_type = _G.GetLootSlotType(loot_slot)

                if slot_type == _G.LOOT_SLOT_ITEM then
                    local item_id = ItemLinkToID(_G.GetLootSlotLink(loot_slot))
                    loot_registry[item_id] = (loot_registry[item_id]) or 0 + quantity
                elseif slot_type == _G.LOOT_SLOT_MONEY then
                    table.insert(action_data.loot_list, ("money:%d"):format(_toCopper(item_text)))
                elseif slot_type == _G.LOOT_SLOT_CURRENCY then
                    table.insert(action_data.loot_list, ("currency:%d:%s"):format(quantity, icon_texture:match("[^\\]+$"):lower()))
                end
            end
        else
            for loot_slot = 1, _G.GetNumLootItems() do
                local icon_texture, item_text, quantity, quality, locked = _G.GetLootSlotInfo(loot_slot)
                if _G.LootSlotIsItem(loot_slot) then
                    local item_id = ItemLinkToID(_G.GetLootSlotLink(loot_slot))
                    loot_registry[item_id] = (loot_registry[item_id]) or 0 + quantity
                elseif _G.LootSlotIsCoin(loot_slot) then
                    table.insert(action_data.loot_list, ("money:%d"):format(_toCopper(item_text)))
                elseif _G.LootSlotIsCurrency(loot_slot) then
                    table.insert(action_data.loot_list, ("currency:%d:%s"):format(quantity, icon_texture:match("[^\\]+$"):lower()))
                end
            end
        end

        for item_id, quantity in pairs(loot_registry) do
            table.insert(action_data.loot_list, ("%d:%d"):format(item_id, quantity))
        end
        update_func()
    end
end -- do-block


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+)")),
}


function WDP:UpdateMerchantItems()
    local unit_type, unit_idnum = self:ParseGUID(_G.UnitGUID("target"))

    if unit_type ~= private.UNIT_TYPES.NPC or not unit_idnum then
        return
    end
    local merchant = DBEntry("npcs", unit_idnum)
    merchant.sells = merchant.sells or {}

    for item_index = 1, _G.GetMerchantNumItems() do
        local _, _, copper_price, stack_size, num_available, _, extended_cost = _G.GetMerchantItemInfo(item_index)
        local item_id = ItemLinkToID(_G.GetMerchantItemLink(item_index))

        if item_id and item_id > 0 then
            local price_string = copper_price

            if extended_cost then
                local bg_points = 0
                local personal_points = 0

                DatamineTT:ClearLines()
                DatamineTT:SetMerchantItem(item_index)

                for line_index = 1, DatamineTT:NumLines() do
                    local current_line = _G["WDPDatamineTTTextLeft" .. line_index]

                    if not current_line then
                        break
                    end
                    local breakout

                    for match_index = 1, #POINT_MATCH_PATTERNS do
                        local match1, match2 = current_line:GetText():match(POINT_MATCH_PATTERNS[match_index])
                        personal_points = personal_points + (match1 or 0)
                        bg_points = bg_points + (match2 or 0)

                        if match1 or match2 then
                            breakout = true
                            break
                        end
                    end

                    if breakout then
                        break
                    end
                end
                local currency_list = {}

                price_string = ("%s:%s:%s"):format(price_string, bg_points, personal_points)

                for cost_index = 1, _G.GetMerchantItemCostInfo(item_index) do
                    local icon_texture, amount_required, currency_link = _G.GetMerchantItemCostItem(item_index, cost_index)
                    local currency_id = currency_link and ItemLinkToID(currency_link) or nil

                    if not currency_id or currency_id < 1 then
                        if not icon_texture then
                            return
                        end
                        currency_id = icon_texture:match("[^\\]+$"):lower()
                    end
                    currency_list[#currency_list + 1] = ("(%s:%s)"):format(amount_required, currency_id)
                end

                for currency_index = 1, #currency_list do
                    price_string = ("%s:%s"):format(price_string, currency_list[currency_index])
                end
            end
            merchant.sells[("%s:%s:[%s]"):format(item_id, stack_size, price_string)] = num_available
        end
    end

    if _G.CanMerchantRepair() then
        merchant.can_repair = true
    end
end


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 WDP:PLAYER_TARGET_CHANGED()
        if not _G.UnitExists("target") or _G.UnitPlayerControlled("target") then
            return
        end
        local unit_type, unit_idnum = self:ParseGUID(_G.UnitGUID("target"))

        if unit_type ~= private.UNIT_TYPES.NPC or not unit_idnum then
            return
        end
        local npc = DBEntry("npcs", unit_idnum)
        local _, class_token = _G.UnitClass("target")
        npc.class = class_token

        UpdateFactionNames()
        DatamineTT:ClearLines()
        DatamineTT:SetUnit("target")

        for line_index = 1, DatamineTT:NumLines() do
            local current_line = _G["WDPDatamineTTTextLeft" .. line_index]

            if not current_line then
                break
            end
            local line_text = current_line:GetText()

            if faction_names[line_text] then
                npc.faction = line_text
                break
            end
        end
        npc.gender = GENDER_NAMES[_G.UnitSex("target")] or "UNDEFINED"
        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")])
        npc.encounter_data = npc.encounter_data or {}

        local npc_level = ("level_%d"):format(_G.UnitLevel("target"))

        if not npc.encounter_data[npc_level] then
            npc.encounter_data[npc_level] = {
                max_health = _G.UnitHealthMax("target"),
            }

            local max_power = _G.UnitManaMax("target")

            if max_power > 0 then
                local power_type = _G.UnitPowerType("target")
                npc.encounter_data[npc_level].power = ("%s:%d"):format(POWER_TYPE_NAMES[_G.tostring(power_type)] or power_type, max_power)
            end
        end
        table.wipe(action_data)
        action_data.type = AF.NPC
        action_data.id_num = unit_idnum
        action_data.npc_level = npc_level
    end
end -- do-block

do
    local function UpdateQuestJuncture(point)
        local unit_name = _G.UnitName("questnpc")

        if not unit_name then
            return
        end
        local unit_type, unit_id = WDP:ParseGUID(_G.UnitGUID("questnpc"))

        if unit_type == private.UNIT_TYPES.OBJECT then
            UpdateObjectLocation(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 + 1], unit_id)] = true
    end


    function WDP:QUEST_COMPLETE()
        UpdateQuestJuncture("end")
    end


    function WDP:QUEST_DETAIL()
        UpdateQuestJuncture("begin")
    end
end -- do-block


function WDP:QUEST_LOG_UPDATE()
    self:UnregisterEvent("QUEST_LOG_UPDATE")
end


function WDP:UNIT_QUEST_LOG_CHANGED(event, unit_id)
    if unit_id ~= "player" then
        return
    end
    self:RegisterEvent("QUEST_LOG_UPDATE")
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
    table.wipe(action_data)

    local tt_item_name, tt_item_link = _G.GameTooltip:GetItem()
    local tt_unit_name, tt_unit_id = _G.GameTooltip:GetUnit()

    if not tt_unit_name and _G.UnitName("target") == target_name then
        tt_unit_name = target_name
        tt_unit_id = "target"
    end
    local spell_flags = private.SPELL_FLAGS_BY_LABEL[spell_label]

    if tt_unit_name and not tt_item_name then
        if bit.band(spell_flags, AF.NPC) == AF.NPC then
            if not tt_unit_id or tt_unit_name ~= target_name then
                return
            end
            action_data.type = AF.NPC
            action_data.loot_type = spell_label:lower()
        end
    elseif bit.band(spell_flags, AF.ITEM) == AF.ITEM then
        action_data.type = AF.ITEM
        action_data.loot_type = spell_label:lower()

        if tt_item_name and tt_item_name == target_name then
            action_data.item_id = ItemLinkToID(tt_item_link)
        elseif target_name and target_name ~= "" then
            local _, target_item_link = _G.GetItemInfo(target_name)
            action_data.item_id = ItemLinkToID(target_item_link)
        end
    elseif not tt_item_name and not tt_unit_name then
        local zone_name, x, y, map_level, instance_token = CurrentLocationData()

        action_data.instance_token = instance_token
        action_data.map_level = map_level
        action_data.name = target_name
        action_data.x = x
        action_data.y = y
        action_data.zone = zone_name

        if bit.band(spell_flags, AF.OBJECT) == AF.OBJECT then
            if target_name == "" then
                return
            end
            local identifier = ("%s:%s"):format(spell_label, target_name)
            UpdateObjectLocation(identifier)

            action_data.type = AF.OBJECT
            action_data.identifier = identifier
        elseif bit.band(spell_flags, AF.ZONE) == AF.ZONE then
            action_data.type = AF.ZONE
            action_data.loot_type = spell_label:lower()
        end
    end

    --    print(("%s: '%s', '%s', '%s', '%s', '%s'"):format(event_name, unit_id, spell_name, spell_rank, target_name, spell_line))
    private.tracked_line = spell_line
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

    --    if private.SPELL_LABELS_BY_NAME[spell_name] then
    --        print(("%s: '%s', '%s', '%s', '%s', '%s'"):format(event_name, unit_id, spell_name, spell_rank, spell_line, spell_id))
    --    end
    private.tracked_line = nil
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
end