-- 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[" 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:UpdateAllDailyKillMarks() self:Debug("Updating daily kill marks.") self:UpdateDisplayList() self:Debug("Updating display list.") 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() message_delay_frame.num_of_retries = 0 message_delay_frame:SetScript("OnUpdate", function(self) if GetServerTime() - self.start_time > 0 then if #{GetChannelList()} == 0 and message_delay_frame.num_of_retries < 3 then if #{EnumerateServerChannels()} > 0 then pcall(RareTracker.Debug, RareTracker, "Retry", 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()