local _,rematch = ... local L = rematch.localization local C = rematch.constants rematch.targetInfo = {} --[[ This gets information about npcIDs, mostly for notable targets, but some support for all npcIDs Only three functions work for all npcIDs: GetUnicNpcID(unit) -- returns a numeric npcID for the unit, either "target" or "mouseover" usually GetNpcName(npcID) -- returns the name of the npcID (see comment on function; it returns C.CACHE_RETRIEVING if the name is not returned and the call needs to happen again!) GetTargetHistory() -- returns an ordered list of the last 3 npcIDs the player targeted The rest of the functions are built around the 325+ notable targets: AllTargets() -- iterator function to iterate over all npcIDs in the order they list IsNotable(npcID) -- returns true if the npcID (or its redirected target) is in targetData GetNpcInfo(npcID) -- returns the headerID,mapID,expansionID,questID for the given notable npcID GetNpcPets(npcID) -- returns an ordered table of petInfo-usable battlepet:etc strings for the notable npcID GetHeaderName(npcID) -- returns the name of the header (generally name of map used to group) for notable npcID GetExpansionName(npcID) -- returns the name of the expansion for the notable npcID GetQuestName(npcID) -- returns the name of the npcID's quest (nil if no quest or C.CACHE_RETRIEVING if not cached) GetLocations(npcID) -- returns a \n-delimited list of map names associated with the notable npcID ]] local targetIndexes = {} -- indexed by npcID, the index into targetData for that npcID local targetNameCache = {} -- indexed by npcID, the localized name of the npcID local targetsToCache = {} -- indexed by npcID, the number of cache attempts for this npcID local reusedPets = {} -- reused table of pets to reduce garbage creation local targetHistory = {} local targetHistoryLookup = {} -- reusable table for target history cleanup local wildPets = {} -- lookup by npcID, whether this is a wild pet local testModel = CreateFrame("PlayerModel") -- used to get displayIDs, a hidden model to SetCreature(npcID) and GetDisplayInfo() testModel:Hide() rematch.targetInfo.recentTarget = nil -- the last npcID targeted (can be nil but is generally not nil'ed by dropping target) rematch.targetInfo.currentTarget = nil -- the current npcID targeted (or nil if a player or no current target) -- on login, populate targetIndexes with indexes into notableTargets for each npcID rematch.events:Register(rematch.targetInfo,"PLAYER_LOGIN",function(self) for index,info in ipairs(rematch.targetData.notableTargets) do targetIndexes[info[2]] = index end -- if sometehing targeted while logging in, capture target if UnitExists("target") then self:PLAYER_TARGET_CHANGED() end end) -- does maintenance on the targetHistory list of targeted npcIDs local function cleanupTargetHistory() -- first remove any duplicates, keeping only the topmost (most recent) distinct copy wipe(targetHistoryLookup) for i=#targetHistory,1,-1 do local npcID = targetHistory[i] if targetHistoryLookup[npcID] then tremove(targetHistory,i) else targetHistoryLookup[npcID] = true end end -- then if there's more than 3 (C.TARGET_HISTORY_SIZE) remove earlier ones so at most 3 remain for i=1,(#targetHistory-C.TARGET_HISTORY_SIZE) do tremove(targetHistory,1) end end function rematch.targetInfo:PLAYER_TARGET_CHANGED() if UnitExists("target") then local npcID = rematch.targetInfo:GetUnitNpcID("target") if npcID then self.recentTarget = npcID rematch.loadedTargetPanel.teamMode = C.ENEMY_TEAM -- add npcID to history and come back in a while to do cleanup so list doesn't get huge -- and we don't waste time doing maintenance in a PLAYER_TARGET_CHANGED tinsert(targetHistory,npcID) rematch.timer:Start(30,cleanupTargetHistory) -- if the target is a wild pet that hasn't been saved to the wildPets lookup yet, add it if not wildPets[npcID] and UnitIsWildBattlePet("target") then wildPets[npcID] = UnitBattlePetSpeciesID("target") end end self.currentTarget = npcID else self.currentTarget = nil end rematch.events:Fire("REMATCH_TARGET_CHANGED") end -- rematch.targetInfo.recentTarget should be registered first (before PLAYER_LOGIN) so it can define recentTarget -- before anything else hears the target has changed rematch.events:Register(rematch.targetInfo,"PLAYER_TARGET_CHANGED",rematch.targetInfo.PLAYER_TARGET_CHANGED) -- sometimes this addon "targets" something via loadedTargetPanel:SetTarget(npcID); these should show in history also function rematch.targetInfo:SetRecentTarget(npcID) if not npcID then self.recentTarget = nil else if type(npcID)=="string" then npcID = rematch.targetInfo:GetNpcID(npcID) end if type(npcID)=="number" then self.recentTarget = npcID tinsert(targetHistory,npcID) end end end -- returns an ordered list of the 3 (C.TARGET_HISTORY_SIZE) most recent targets, with the most recent at the end of the list function rematch.targetInfo:GetTargetHistory() cleanupTargetHistory() return targetHistory end -- returns the npcID of the given unit ("target"/"mouseover"), or nil if unit doesn't exist/is a player function rematch.targetInfo:GetUnitNpcID(unit) if UnitExists(unit) then local npcID = tonumber((UnitGUID(unit) or ""):match(".-%-%d+%-%d+%-%d+%-%d+%-(%d+)")) if npcID and npcID~=0 then if rematch.targetData.redirects[npcID] then -- targeting a redirected target return rematch.targetData.redirects[npcID] -- return redirected npcID else return npcID -- otherwise return the npcID end end end end -- gets the localized name of an npcID from a tooltip scan. tooltip scans are computationally expensive so -- it will cache the result and use that in future calls. if the npcID didn't get a name the npcID will be -- added to targetsToCache and return "Retrieving name..." to display. in this case, the calling function -- can wait a second and re-update to try for the name again. once the number of attempts are exceeded, it -- will return "NPC ". (the goal here is to not cache all 300+ npcs on login. the calling function -- should do a delayed update if the name returned is C.CACHE_RETRIEVING) function rematch.targetInfo:GetNpcName(npcID,noDisplay) if type(npcID)=="string" then npcID = tonumber(npcID:match("target:(%d+)")) end -- if target has a subname like (Legendary) then it will be appended to name local subname = "" if rematch.targetData.subnames[npcID] then subname = format(" (%s)",rematch.targetData.subnames[npcID]) end if type(npcID)~="number" then return L["No Target"] elseif targetNameCache[npcID] then -- if name cached, return it return targetNameCache[npcID]..subname else local tooltip = RematchTooltipScan or CreateFrame("GameTooltip","RematchTooltipScan",nil,"GameTooltipTemplate") tooltip:SetOwner(UIParent,"ANCHOR_NONE") tooltip:SetHyperlink(format("unit:Creature-0-0-0-0-%d-0000000000",npcID)) if tooltip:NumLines()>0 then local name = RematchTooltipScanTextLeft1:GetText() if name and name:len()>0 then targetNameCache[npcID] = name targetsToCache[npcID] = nil return name..subname end end -- if we reached here, then this npcID is still not cached if not targetsToCache[npcID] then targetsToCache[npcID] = GetTime() end if GetTime()-targetsToCache[npcID] < C.CACHE_TIMEOUT then -- haven't exceeded timeout duration, return temp name if not noDisplay then -- if name wasn't cached and we're displaying it, come back in a bit and update UI (could be team or target list or elsewhere that needs update) rematch.timer:Start(C.CACHE_WAIT,rematch.frame.Update) end return C.CACHE_RETRIEVING else -- exceeded retry attempts, cache it as NPC and give up trying local name = format(L["%s (npc id %d)"],UNKNOWN,npcID) targetNameCache[npcID] = name targetsToCache[npcID] = nil return name..subname end end end -- gets the displayID for the given npcID, by setting a model to that npcID and then getting its displayID from that function rematch.targetInfo:GetNpcDisplayID(npcID) if type(npcID)=="number" then testModel:SetCreature(npcID) return testModel:GetDisplayInfo() end end --[[ notable npcs: the following only apply to the 300+ npcs in targetData ]] -- iterator function to iterate over all npcIDs in order in targetData -- usage: for npcID in rematch.targetInfo:AllTargets() do print(npcID) end function rematch.targetInfo:AllTargets() local i = 0 return function() local targetData = rematch.targetData.notableTargets i = i + 1 if i <= #targetData then return targetData[i][2] end end end -- returns true if the npcID is in the notableTargets table (with redirect too) function rematch.targetInfo:IsNotable(npcID) return rematch.targetInfo:GetNpcInfo(npcID) and true end function rematch.targetInfo:IsWildPet(npcID) return wildPets[npcID] and true or false end -- returns the headerID,mapID,expansionID,questID for the given notable npcID function rematch.targetInfo:GetNpcInfo(npcID) if type(npcID)=="string" then npcID = tonumber(npcID:match("target:(%d+)")) end if not npcID then return end if rematch.targetData.redirects[npcID] then npcID = rematch.targetData.redirects[npcID] end if targetIndexes[npcID] then local info = rematch.targetData.notableTargets[targetIndexes[npcID]] --info[1] = "header:"..info[1] -- make header into a headerID usable in lists return "header:"..info[1],info[3],info[4],info[5] end end -- converts target:12345 to numeric 12345 function rematch.targetInfo:GetNpcID(targetID) return type(targetID)=="string" and tonumber(targetID:match("target:(.+)")) or targetID end -- returns an ordered table of petInfo-usable battlepet:etc strings for the notable npc -- if numSlots is defined, the table is padded with empty slots before the pet(s) -- returns a single unnotable pet if the npc is not notable (use GetNumPets to get a real count) function rematch.targetInfo:GetNpcPets(npcID,numSlots) if type(npcID)=="string" then npcID = tonumber(npcID:match("target:(%d+)")) end if not npcID then return end if rematch.targetData.redirects[npcID] then npcID = rematch.targetData.redirects[npcID] end wipe(reusedPets) if targetIndexes[npcID] then -- if this is a notable npc, pets are known local info = rematch.targetData.notableTargets[targetIndexes[npcID]] for i=6,8 do if info[i] then tinsert(reusedPets,info[i]) end end elseif wildPets[npcID] then -- if not a notable npc but it is a seen wild pet, add a speciesID for the pet tinsert(reusedPets,wildPets[npcID]) else -- otherwise add unobtainable:npcID petID tinsert(reusedPets,"unnotable:"..npcID) end if numSlots then for i=#reusedPets+1,(numSlots or 3) do tinsert(reusedPets,1,"empty") end end return reusedPets end -- returns the number of pets this npcID is known to have, 0 if not notable or no pets function rematch.targetInfo:GetNumPets(npcID) if type(npcID)=="string" then npcID = tonumber(npcID:match("target:(%d+)")) end if not npcID then return 0 end if rematch.targetData.redirects[npcID] then npcID = rematch.targetData.redirects[npcID] end if targetIndexes[npcID] then return #rematch.targetData.notableTargets[targetIndexes[npcID]]-5 elseif wildPets[npcID] then return 1 -- wild pets have 1 pet, the speciesID else return 0 end end -- gets the headerID of the given npcID function rematch.targetInfo:GetHeaderID(npcID) return rematch.targetInfo:GetNpcInfo(npcID) end -- returns the name of the header from the headerID (first return of GetNpcInfo) function rematch.targetInfo:GetHeaderName(headerID) local name = headerID:match("header:(.+)") local mapID = tonumber(name) if mapID then local mapInfo = C_Map.GetMapInfo(mapID) return mapInfo and mapInfo.name or UNKNOWN else return L[name] end end -- returns the expansionID of the header function rematch.targetInfo:GetHeaderExpansionID(headerID) return headerID and rematch.targetData.headerExpansions[headerID] end -- returns the name of the expansion associated with the notable npcID function rematch.targetInfo:GetExpansionName(npcID) local _,_,expansionID = rematch.targetInfo:GetNpcInfo(npcID) return _G["EXPANSION_NAME"..expansionID] end -- returns the name of the quest associated with the notable npcID, if any. (if a quest name hasn't been -- cached yet, it will return nothing; todo: make it return CACHE_RETRIEVING with a timeout like name?) function rematch.targetInfo:GetQuestName(npcID) local _,_,_,questID = rematch.targetInfo:GetNpcInfo(npcID) if questID then local name = C_TaskQuest.GetQuestInfoByQuestID(questID) or C_QuestLog.GetTitleForQuestID(questID) return name --or C.CACHE_RETRIEVING end end function rematch.targetInfo:GetExpansionID(npcID) local _,_,expansionID = rematch.targetInfo:GetNpcInfo(npcID) return expansionID end -- for GetLocations(), a small recursive function to get a list of map names where mapID is local exploredIDs = {} -- reused ordered list of map names in the following local function exploreMapID(mapID) local mapInfo = C_Map.GetMapInfo(mapID) if mapInfo and mapInfo.name and mapID~=946 and mapID~=947 then local prefix = #exploredIDs>0 and "\124cffa0a0a0" or "" tinsert(exploredIDs,prefix..mapInfo.name) exploreMapID(mapInfo.parentMapID) end end -- returns a string of the map a notable npcID belongs to and its parent maps, up to but not -- including cosmic/azeroth function rematch.targetInfo:GetLocations(npcID) local _,mapID = rematch.targetInfo:GetNpcInfo(npcID) wipe(exploredIDs) exploreMapID(mapID) if #exploredIDs>0 then return table.concat(exploredIDs,"\n") else return UNKNOWN end end