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

584 lines
24 KiB

-- Redefine often used functions locally.
local UnitGUID = UnitGUID
local strsplit = strsplit
local UnitHealth = UnitHealth
local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo
local C_VignetteInfo = C_VignetteInfo
local GetServerTime = GetServerTime
local CreateFrame = CreateFrame
local GetChannelList = GetChannelList
local IsQuestFlaggedCompleted = C_QuestLog.IsQuestFlaggedCompleted
local PlaySoundFile = PlaySoundFile
local select = select
local date = date
local time = time
local EnumerateServerChannels = EnumerateServerChannels
-- Redefine often used variables locally.
local C_Map = C_Map
local COMBATLOG_OBJECT_TYPE_GUARDIAN = COMBATLOG_OBJECT_TYPE_GUARDIAN
local COMBATLOG_OBJECT_TYPE_PET = COMBATLOG_OBJECT_TYPE_PET
local COMBATLOG_OBJECT_TYPE_OBJECT = COMBATLOG_OBJECT_TYPE_OBJECT
local bit = bit
local UIParent = UIParent
-- ####################################################################
-- ## Localization Support ##
-- ####################################################################
-- Get an object we can use for the localization of the addon.
local L = LibStub("AceLocale-3.0"):GetLocale("RareTracker", true)
-- ####################################################################
-- ## Event Variables ##
-- ####################################################################
-- The last zone id that was encountered.
RareTracker.zone_id = nil
-- The current shard id.
RareTracker.shard_id = nil
-- A flag used to detect guardians or pets.
local companion_type_mask = bit.bor(
COMBATLOG_OBJECT_TYPE_GUARDIAN, COMBATLOG_OBJECT_TYPE_PET, COMBATLOG_OBJECT_TYPE_OBJECT
)
-- A flag that will notify whether the char frame has loaded successfully, to avoid overwriting the chat order.
local chat_frame_loaded = false
-- Track whether an entity is considered to be alive.
RareTracker.is_alive = {}
-- Track the current health of the entity.
RareTracker.current_health = {}
-- Track when the entity was last seen dead.
RareTracker.last_recorded_death = {}
-- Track the reported current coordinates of the rares.
RareTracker.current_coordinates = {}
-- Record all spawn uids that are detected, such that we don't report the same spawn multiple times.
RareTracker.reported_spawn_uids = {}
-- Record all entities that died, such that we don't overwrite existing death.
RareTracker.recorded_entity_death_ids = {}
-- Record all vignettes that are detected, such that we don't report the same spawn multiple times.
local reported_vignettes = {}
-- For some reason... the Sha of Anger is a... vehicle?
local valid_unit_types = {
["Creature"] = true,
["Vehicle"] = true
}
-- The version of the db storage scheme.
local storage_version = 1
-- ####################################################################
-- ## Event Handlers ##
-- ####################################################################
-- Called whenever the user changes to a new zone or area.
function RareTracker:OnZoneTransition()
-- The zone the player is in.
local zone_id = C_Map.GetBestMapForUnit("player")
-- Update the zone id and keep the last id.
local last_zone_id = self.zone_id
self.zone_id = self.zone_id_to_primary_id[zone_id]
-- Check if the zone id changed. If so, update the list of rares to display when appropriate.
if self.zone_id_to_primary_id[zone_id] ~= self.zone_id_to_primary_id[last_zone_id] then
self:ChangeZone()
end
-- Show/hide the interface when appropriate.
if self.zone_id_to_primary_id[zone_id] and not self.zone_id_to_primary_id[last_zone_id] then
self:OpenWindow()
self:RegisterTrackingEvents()
elseif not self.zone_id_to_primary_id[zone_id] and self.zone_id_to_primary_id[last_zone_id] then
self:CloseWindow()
self:UnregisterTrackingEvents()
end
end
-- Fetch the new list of rares and ensure that these rares are properly displayed.
function RareTracker:ChangeZone()
-- Reset the shard id
self:ChangeShard(nil)
-- Ensure that the correct data is shown in the window.
self:UpdateDisplayList()
self:UpdateAllDailyKillMarks()
self:Debug("Changing zone to", C_Map.GetBestMapForUnit("player"))
end
-- Transfer to a new shard, reset current data and join the appropriate channel.
function RareTracker:ChangeShard(zone_uid)
-- Leave the channel associated with the previous shard id and save the data.
self:LeavePreviousShardChannel()
self:SaveRecordedData()
-- As a precaution, leave all other channels not in the current zone.
self:LeaveShardChannelsInOtherZones()
-- Reset all tracked data.
self:ResetTrackedData()
-- Set the new shard id.
self.shard_id = zone_uid
-- Update the shard number in the display.
self:UpdateShardNumber()
if self.shard_id then
-- Change the shard id to the new shard and add the channel.
self:LoadRecordedData()
self:AnnounceArrival()
end
end
-- Check whether the user has changed shards and proceed accordingly.
-- Return true if the shard changed, false otherwise.
function RareTracker:CheckForShardChange(zone_uid)
if self.shard_id ~= zone_uid and zone_uid ~= nil then
print(L["<RT> Moving to shard "]..zone_uid..".")
self:ChangeShard(zone_uid)
return true
end
return false
end
-- Check whether the given npc id needs to be redirected under the current circumstances.
function RareTracker:CheckForRedirectedRareIds(npc_id)
local NPCIdRedirection = self.primary_id_to_data[self.zone_id].NPCIdRedirection
if NPCIdRedirection then
return NPCIdRedirection(npc_id)
end
return npc_id
end
-- This event is fired whenever the player's target is changed, including when the target is lost.
function RareTracker:PLAYER_TARGET_CHANGED()
self:OnHealthDetection("target", "[PLAYER_TARGET_CHANGED]")
end
-- Fired when the mouseover object needs to be updated.
function RareTracker:UPDATE_MOUSEOVER_UNIT()
self:OnHealthDetection("mouseover", "[UPDATE_MOUSEOVER_UNIT]")
end
-- Fired whenever a unit's health is affected.
function RareTracker:UNIT_HEALTH(_, unit)
if unit == "target" or unit:find("nameplate") then
self:OnHealthDetection(unit, "[UNIT_HEALTH]")
end
end
function RareTracker:OnHealthDetection(unit, debug_tag)
-- Get information about the target.
local guid = UnitGUID(unit)
if chat_frame_loaded and guid and not UnitPlayerControlled(unit) then
-- unittype, zero, server_id, instance_id, zone_uid, npc_id, spawn_uid
local unittype, _, _, _, zone_uid, npc_id, spawn_uid = strsplit("-", guid)
npc_id = tonumber(npc_id)
-- It might occur that the NPC id is nil. Do not proceed in such a case.
if not npc_id then return end
-- Certain entities retain their zone_uid even after moving shards. Ignore them.
if not self.db.global.banned_NPC_ids[npc_id] then
if self:CheckForShardChange(zone_uid) then
self:Debug(debug_tag, unit, guid)
end
end
--A special check for duplicate NPC ids in different environments (Mecharantula).
npc_id = self:CheckForRedirectedRareIds(npc_id)
if valid_unit_types[unittype] and self.primary_id_to_data[self.zone_id].entities[npc_id] then
-- Find the health of the entity.
local health = UnitHealth(unit)
if health > 0 then
-- Get the health of the entity
local percentage = self.GetTargetHealthPercentage(unit)
-- Mark the entity as alive and report to your peers.
self:ProcessEntityHealth(npc_id, spawn_uid, percentage, nil, nil, true)
else
-- Mark the entity has dead and report to your peers.
self:ProcessEntityDeath(npc_id, spawn_uid, true)
end
end
end
end
-- Fires for combat events such as a player casting a spell or an NPC taking damage.
function RareTracker:COMBAT_LOG_EVENT_UNFILTERED()
if chat_frame_loaded then
-- The event does not have a payload (8.0 change). Use CombatLogGetCurrentEventInfo() instead.
-- timestamp, subevent, zero, sourceGUID, sourceName, sourceFlags, sourceRaidFlags,
-- destGUID, destName, destFlags, destRaidFlags
local _, subevent, _, sourceGUID, _, _, _, destGUID, _, destFlags, _ = CombatLogGetCurrentEventInfo()
-- unittype, zero, server_id, instance_id, zone_uid, npc_id, spawn_uid
local unittype, _, _, _, zone_uid, npc_id, spawn_uid = strsplit("-", destGUID)
npc_id = tonumber(npc_id)
-- It might occur that the NPC id is nil. Do not proceed in such a case.
if not npc_id or not destFlags then return end
-- Blacklist the entity.
if not self.db.global.banned_NPC_ids[npc_id] and bit.band(destFlags, companion_type_mask) > 0 and not self.tracked_npc_ids[npc_id] then
self.db.global.banned_NPC_ids[npc_id] = true
end
-- We can always check for a shard change.
-- We only take fights between creatures, since they seem to be the only reliable option.
-- We exclude all pets and guardians, since they might have retained their old shard change.
if valid_unit_types[unittype] and not self.db.global.banned_NPC_ids[npc_id] and bit.band(destFlags, companion_type_mask) == 0 then
if self:CheckForShardChange(zone_uid) then
self:Debug("[COMBAT_LOG_EVENT_UNFILTERED]", sourceGUID, destGUID)
end
end
if valid_unit_types[unittype] and self.primary_id_to_data[self.zone_id].entities[npc_id] and bit.band(destFlags, companion_type_mask) == 0 then
if subevent == "UNIT_DIED" then
-- Mark the entity has dead and report to your peers.
self:ProcessEntityDeath(npc_id, spawn_uid, true)
elseif subevent ~= "PARTY_KILL" then
-- Report the entity as alive to your peers, if it is not marked as alive already.
if not self.is_alive[npc_id] then
-- The combat log range is quite long, so no coordinates can be provided.
self:ProcessEntityAlive(npc_id, spawn_uid, true)
end
end
end
end
end
-- Fired whenever a vignette appears or disappears in the minimap.
function RareTracker:VIGNETTE_MINIMAP_UPDATED(_, vignetteGUID, _)
if chat_frame_loaded then
local vignetteInfo = C_VignetteInfo.GetVignetteInfo(vignetteGUID)
local vignetteLocation = C_VignetteInfo.GetVignettePosition(vignetteGUID, C_Map.GetBestMapForUnit("player"))
if vignetteInfo and vignetteLocation then
-- Report the entity.
-- unittype, zero, server_id, instance_id, zone_uid, npc_id, spawn_uid
local unittype, _, _, _, zone_uid, npc_id, spawn_uid = strsplit("-", vignetteInfo.objectGUID)
npc_id = tonumber(npc_id)
-- It might occur that the NPC id is nil. Do not proceed in such a case.
if not npc_id then return end
--A special check for duplicate NPC ids in different environments (Mecharantula).
npc_id = self:CheckForRedirectedRareIds(npc_id)
if valid_unit_types[unittype] then
if not self.db.global.banned_NPC_ids[npc_id] then
if self:CheckForShardChange(zone_uid) then
self:Debug("[VIGNETTE_MINIMAP_UPDATED]", vignetteInfo.objectGUID)
end
end
--A special check for duplicate NPC ids in different environments (Mecharantula).
npc_id = self:CheckForRedirectedRareIds(npc_id)
if self.primary_id_to_data[self.zone_id].entities[npc_id] and not reported_vignettes[vignetteGUID] then
reported_vignettes[vignetteGUID] = {npc_id, spawn_uid}
local x, y = 100 * vignetteLocation.x, 100 * vignetteLocation.y
self:ProcessEntityVignette(npc_id, spawn_uid, x, y, true)
end
end
end
end
end
-- Fires when an NPC speaks.
function RareTracker:OnMonsterChatMessage(_, ...)
if chat_frame_loaded then
local data = self.primary_id_to_data[self.zone_id]
-- Attempt to match by name or text, using the function provided by the plugin.
local text, name = select(1, ...), select(2, ...)
local npc_id = data.FindMatchForName and data.FindMatchForName(self, name) or data.FindMatchForText and data.FindMatchForText(self, text)
if npc_id then
-- We found a match.
self.is_alive[npc_id] = GetServerTime()
self:PlaySoundNotification(npc_id, npc_id)
if not self.current_coordinates[npc_id] then
self.current_coordinates[npc_id] = self.primary_id_to_data[self.zone_id].entities[npc_id].coordinates
end
end
end
end
-- ####################################################################
-- ## Event Handler Helper Functions ##
-- ####################################################################
-- Register the events that are needed for the proper tracking of rares.
function RareTracker:RegisterTrackingEvents()
self:RegisterEvent("PLAYER_TARGET_CHANGED")
self:RegisterEvent("UNIT_HEALTH")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
self:RegisterEvent("VIGNETTE_MINIMAP_UPDATED")
self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
self:RegisterEvent("CHAT_MSG_MONSTER_YELL", "OnMonsterChatMessage")
self:RegisterEvent("CHAT_MSG_MONSTER_SAY", "OnMonsterChatMessage")
self:RegisterEvent("CHAT_MSG_MONSTER_EMOTE", "OnMonsterChatMessage")
end
-- Unregister all events that aren't necessary when outside of tracking zones.
function RareTracker:UnregisterTrackingEvents()
self:UnregisterEvent("PLAYER_TARGET_CHANGED")
self:UnregisterEvent("UNIT_HEALTH")
self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
self:UnregisterEvent("VIGNETTE_MINIMAP_UPDATED")
self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT")
self:UnregisterEvent("CHAT_MSG_MONSTER_YELL")
self:UnregisterEvent("CHAT_MSG_MONSTER_SAY")
self:UnregisterEvent("CHAT_MSG_MONSTER_EMOTE")
end
-- Reset all the currently tracked data.
function RareTracker:ResetTrackedData()
self.is_alive = {}
self.current_health = {}
self.last_recorded_death = {}
self.is_npc_data_provided_by_other_player = {}
self.current_coordinates = {}
self.recorded_entity_death_ids = {}
self.reported_spawn_uids = {}
reported_vignettes = {}
end
-- Save all the recorded data in the database.
function RareTracker:SaveRecordedData()
if self.shard_id then
-- Store the timer data for the shard in the saved variables.
self.db.global.previous_records[self.shard_id] = {}
self.db.global.previous_records[self.shard_id].time_stamp = GetServerTime()
self.db.global.previous_records[self.shard_id].time_table = self.last_recorded_death
self.db.global.previous_records[self.shard_id].version = storage_version
end
end
-- Attempt to load previous data from our cache.
function RareTracker:LoadRecordedData()
if self.db.global.previous_records[self.shard_id] then
if GetServerTime() - self.db.global.previous_records[self.shard_id].time_stamp < 900 then
self:Debug("Restoring data from previous session in shard "..self.shard_id)
self.last_recorded_death = self.db.global.previous_records[self.shard_id].time_table
for npc_id, kill_data in pairs(self.last_recorded_death) do
local _, spawn_uid = unpack(kill_data)
self.recorded_entity_death_ids[spawn_uid..npc_id] = true
end
else
self:Debug("Resetting stored data for "..self.shard_id)
self.db.global.previous_records[self.shard_id] = nil
end
end
end
-- Play a sound notification if applicable
function RareTracker:PlaySoundNotification(npc_id, spawn_uid)
if self.db.global.favorite_rares[npc_id] and not self.reported_spawn_uids[spawn_uid] and not self.reported_spawn_uids[npc_id] then
-- Play a sound file.
local completion_quest_id = self.primary_id_to_data[self.zone_id].entities[npc_id].quest_id
self.reported_spawn_uids[spawn_uid] = true
if not completion_quest_id or not IsQuestFlaggedCompleted(completion_quest_id) then
PlaySoundFile(self.db.global.favorite_alert.favorite_sound_alert, self.db.global.favorite_alert.favorite_alert_sound_channel)
end
end
end
-- ####################################################################
-- ## Process Rare Event Helper Functions ##
-- ####################################################################
-- Process that an entity has died.
function RareTracker:ProcessEntityDeath(npc_id, spawn_uid, make_announcement)
if not self.recorded_entity_death_ids[spawn_uid..npc_id] then
-- Mark the entity as dead.
self.last_recorded_death[npc_id] = {GetServerTime(), spawn_uid}
self.is_alive[npc_id] = nil
self.current_health[npc_id] = nil
self.current_coordinates[npc_id] = nil
self.recorded_entity_death_ids[spawn_uid..npc_id] = true
self.reported_spawn_uids[spawn_uid] = nil
self.reported_spawn_uids[npc_id] = nil
-- Update the status of the rare in the display.
self:UpdateStatus(npc_id)
-- We need to delay the update daily kill mark check, since the servers don't update it instantly.
local primary_id = self.zone_id
if primary_id then
self:DelayedExecution(3,
function()
self:UpdateDailyKillMark(npc_id, primary_id)
if self.db.global.window.hide_killed_entities then
self:UpdateEntityVisibility()
end
end
)
end
-- Send the death message.
if make_announcement then
self:AnnounceEntityDeath(npc_id, spawn_uid)
end
end
end
-- Process that an entity has been seen alive.
function RareTracker:ProcessEntityAlive(npc_id, spawn_uid, make_announcement)
if not self.recorded_entity_death_ids[spawn_uid..npc_id] then
-- Mark the entity as alive.
self.is_alive[npc_id] = GetServerTime()
-- Update the status of the rare in the display.
self:UpdateStatus(npc_id)
-- Make a sound announcement if appropriate.
self:PlaySoundNotification(npc_id, spawn_uid)
-- Send the alive message.
if make_announcement then
self:AnnounceEntityAlive(npc_id, spawn_uid)
end
end
end
-- Process that an entity has been seen alive.
function RareTracker:ProcessEntityVignette(npc_id, spawn_uid, x, y, make_announcement)
if not self.recorded_entity_death_ids[spawn_uid..npc_id] then
-- Mark the entity as alive.
self.is_alive[npc_id] = GetServerTime()
self.current_coordinates[npc_id] = {x, y}
-- Update the status of the rare in the display.
self:UpdateStatus(npc_id)
-- Make a sound announcement if appropriate.
self:PlaySoundNotification(npc_id, spawn_uid)
-- Send the vignette message.
if make_announcement then
self:AnnounceEntityVignette(npc_id, spawn_uid, x, y)
end
end
end
-- Process an enemy health update.
function RareTracker:ProcessEntityHealth(npc_id, spawn_uid, percentage, x, y, make_announcement)
if not self.recorded_entity_death_ids[spawn_uid..npc_id] then
-- Update the health of the entity.
self.last_recorded_death[npc_id] = nil
self.is_alive[npc_id] = GetServerTime()
self.current_health[npc_id] = percentage
-- Update the status of the rare in the display.
self:UpdateStatus(npc_id)
-- Make a sound announcement if appropriate.
self:PlaySoundNotification(npc_id, spawn_uid)
-- Set some coordinates if none are yet known.
if not self.current_coordinates[npc_id] then
if self.primary_id_to_data[self.zone_id].entities[npc_id].coordinates then
self.current_coordinates[npc_id] = self.primary_id_to_data[self.zone_id].entities[npc_id].coordinates
elseif x ~= nil and y ~= nil then
-- This code should only be reached when make_announcement is false.
self.current_coordinates[npc_id] = {x, y}
else
-- Get the current position of the player.
local pos = C_Map.GetPlayerMapPosition(C_Map.GetBestMapForUnit("player"), "player")
self.current_coordinates[npc_id] = {math.floor(pos.x * 10000 + 0.5) / 100, math.floor(pos.y * 10000 + 0.5) / 100}
end
end
-- Send the alive message.
if make_announcement then
x, y = unpack(self.current_coordinates[npc_id])
self:AnnounceEntityHealthWithCoordinates(npc_id, spawn_uid, percentage, x, y)
end
end
end
-- ####################################################################
-- ## Daily Reset Handling ##
-- ####################################################################
-- Certain updates need to be made every hour because of the lack of daily reset/new world quest events.
function RareTracker:AddDailyResetHandler()
-- There is no event for the daily reset, so do a precautionary check every hour.
local f = CreateFrame("Frame", "RT.daily_reset_handling_frame", UIParent)
-- Which timestamp was the last hour?
local time_table = date("*t", GetServerTime())
time_table.sec = 0
time_table.min = 0
-- Check when the next hourly reset is going to be, by adding 3600 to the previous hour timestamp.
-- Add a 60 second offset, since the kill mark update might be delayed.
f.target_time = time(time_table) + 3600 + 60
-- Add an OnUpdate checker.
f:SetScript("OnUpdate",
function(_f)
if GetServerTime() > _f.target_time then
_f.target_time = _f.target_time + 3600
if self.gui.entities_frame ~= nil then
self:UpdateDisplayList()
self:Debug("Updating display list.")
self:UpdateAllDailyKillMarks()
self:Debug("Updating daily kill marks.")
end
end
end
)
f:Show()
self.gui.daily_reset_handling_frame = f
end
-- ####################################################################
-- ## Channel Wait Frame ##
-- ####################################################################
-- One of the issues encountered is that the chat might be joined before the default channels.
-- In such a situation, the order of the channels changes, which is undesirable.
-- Thus, we block certain events until these chats have been loaded.
local message_delay_frame = CreateFrame("Frame", "RT.message_delay_frame", UIParent)
message_delay_frame.start_time = GetServerTime() + 2
message_delay_frame.num_of_retries = 0
message_delay_frame:SetScript("OnUpdate",
function(self)
if GetServerTime() - self.start_time > 2 then
if #{GetChannelList()} == 0 and message_delay_frame.num_of_retries < 5 then
if #{EnumerateServerChannels()} > 0 then
pcall(RareTracker.Debug, RareTracker, "Waiting to join channel. Attempt:", self.num_of_retries)
self.num_of_retries = self.num_of_retries + 1
end
self.start_time = GetServerTime()
else
pcall(RareTracker.Debug, RareTracker, "Chat frame is loaded.")
chat_frame_loaded = true
self:SetScript("OnUpdate", nil)
self:Hide()
end
end
end
)
message_delay_frame:Show()