Read nameplate units directly instead of scanning by name

This commit is contained in:
Brues
2026-10-31 15:08:39 -05:00
parent f81ea09d1c
commit e43ddd31fd
3 changed files with 63 additions and 138 deletions
+42 -101
View File
@@ -2,37 +2,26 @@
setfenv(1, pfUI:GetEnvironment()) setfenv(1, pfUI:GetEnvironment())
--[[ libunitscan ]]-- --[[ libunitscan ]]--
-- A pfUI library that detects and saves all kind of unit related informations. -- A pfUI library that remembers the class, level and guild of every player it
-- Such as level, class, elite-state and playertype. Each query causes the library -- sees: in the friends, guild, raid and party rosters, in /who results, and as
-- to automatically scan for the target if not already existing. Player-data is -- target, mouseover or nameplate. The data is persisted within pfUI_playerDB,
-- persisted within the pfUI_playerDB where the mob data is a throw-away table. -- for the places that only have a player's name, such as chat links and
-- The automatic target scanner is only working for vanilla due to client limitations -- offline friends. Anything with a unit reads the unit itself instead.
-- on further expansions.
-- --
-- External functions: -- External functions:
-- GetUnitInfo(name, active, isPlayer) -- GetUnitInfo(name)
-- Returns information of the given unitname. Returns nil if no match is found. -- Returns information of the given player name. Returns nil if the player
-- When nothing is found and the active flag is set, the autoscanner will -- was never seen.
-- automatically pick it up and try to fill the missing entry by targetting the unit.
-- Pass isPlayer=true/false to restrict the lookup to the players or mobs table
-- (avoids name-collision false positives, e.g. a player and an NPC named Chromie).
-- --
-- class[String] - The class of the unit -- class[String] - The class of the player
-- level[Number] - The level of the unit -- level[Number] - The level of the player
-- elite[String] - The elite state of the unit (See UnitClassification()) -- guild[String] - The guild name of the player
-- player[Boolean] - Returns true if unit is a player
-- guild[String] - Returns guild name of unit is a player
--
-- Internal functions:
-- libunitscan:AddData(db, name, class, level, elite)
-- Adds unit data to a given db. Where db should be either "players" or "mobs"
-- --
-- return instantly when another libunitscan is already active -- return instantly when another libunitscan is already active
if pfUI.api.libunitscan then return end if pfUI.api.libunitscan then return end
local units = { players = {}, mobs = {} } local players = {}
local queue = { }
-- Feed (guid, name, classToken) into ClassicAPI's persistent name -- Feed (guid, name, classToken) into ClassicAPI's persistent name
-- cache when the unit's GUID is resolvable. Roster-style packets -- cache when the unit's GUID is resolvable. Roster-style packets
@@ -46,28 +35,19 @@ local function RememberByUnit(unit, name, class)
C_PlayerCache.RememberPlayer(guid, name, class, UnitRace(unit), UnitSex(unit)) C_PlayerCache.RememberPlayer(guid, name, class, UnitRace(unit), UnitSex(unit))
end end
function GetUnitInfo(name, active, isPlayer) function GetUnitInfo(name)
if isPlayer ~= false and units["players"][name] then local ret = players[name]
local ret = units["players"][name] if ret then
return ret.class, ret.level, ret.elite, true, ret.guild return ret.class, ret.level, ret.guild
elseif isPlayer ~= true and units["mobs"][name] then
local ret = units["mobs"][name]
return ret.class, ret.level, ret.elite, nil, ret.guild
elseif active then
queue[name] = true
libunitscan:Show()
end end
end end
local function AddData(db, name, class, level, elite, guild) local function AddData(name, class, level, guild)
if not name or not db then return end if not name then return end
units[db] = units[db] or {} players[name] = players[name] or {}
units[db][name] = units[db][name] or {} players[name].class = class or players[name].class
units[db][name].class = class or units[db][name].class players[name].level = level or players[name].level
units[db][name].level = level or units[db][name].level players[name].guild = guild or players[name].guild
units[db][name].elite = elite or units[db][name].elite
units[db][name].guild = guild or units[db][name].guild
queue[name] = nil
end end
local libunitscan = CreateFrame("Frame", "pfUnitScan", UIParent) local libunitscan = CreateFrame("Frame", "pfUnitScan", UIParent)
@@ -85,14 +65,14 @@ libunitscan:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then if event == "PLAYER_ENTERING_WORLD" then
-- load pfUI_playerDB -- load pfUI_playerDB
units.players = pfUI_playerDB players = pfUI_playerDB
-- update own character details -- update own character details
local name = UnitName("player") local name = UnitName("player")
local class = UnitClassBase("player") local class = UnitClassBase("player")
local level = UnitLevel("player") local level = UnitLevel("player")
local guild = GetGuildInfo("player") local guild = GetGuildInfo("player")
AddData("players", name, class, level, nil, guild) AddData(name, class, level, guild)
RememberByUnit("player", name, class) RememberByUnit("player", name, class)
elseif event == "FRIENDLIST_UPDATE" then elseif event == "FRIENDLIST_UPDATE" then
@@ -100,38 +80,38 @@ libunitscan:SetScript("OnEvent", function()
local info = C_FriendList.GetFriendInfoByIndex(i) local info = C_FriendList.GetFriendInfoByIndex(i)
if info then if info then
local level = info.level > 0 and info.level or nil local level = info.level > 0 and info.level or nil
AddData("players", info.name, info.classFilename, level) AddData(info.name, info.classFilename, level)
end end
end end
elseif event == "GUILD_ROSTER_UPDATE" then elseif event == "GUILD_ROSTER_UPDATE" then
local name, class, level, _, guild local guild = GetGuildInfo("player")
local name, class, level, _
for i = 1, GetNumGuildMembers() do for i = 1, GetNumGuildMembers() do
name, _, _, level, class = GetGuildRosterInfo(i) name, _, _, level, class = GetGuildRosterInfo(i)
guild = GetGuildInfo("player")
class = L["class"][class] or nil class = L["class"][class] or nil
AddData("players", name, class, level, nil, guild) AddData(name, class, level, guild)
end end
elseif event == "RAID_ROSTER_UPDATE" then elseif event == "RAID_ROSTER_UPDATE" then
local name, class, SubGroup, level, unit, _ local name, class, level, unit, _
for i = 1, GetNumRaidMembers() do for i = 1, GetNumRaidMembers() do
unit = "raid" .. i unit = "raid" .. i
name, _, SubGroup, level, class = GetRaidRosterInfo(i) name, _, _, level, class = GetRaidRosterInfo(i)
class = L["class"][class] or nil class = L["class"][class] or nil
AddData("players", name, class, level) AddData(name, class, level)
RememberByUnit(unit, name, class) RememberByUnit(unit, name, class)
end end
elseif event == "PARTY_MEMBERS_CHANGED" then elseif event == "PARTY_MEMBERS_CHANGED" then
local name, class, level, unit, _, guild local name, class, level, unit, guild
for i = 1, GetNumPartyMembers() do for i = 1, GetNumPartyMembers() do
unit = "party" .. i unit = "party" .. i
class = UnitClassBase(unit) class = UnitClassBase(unit)
name = UnitName(unit) name = UnitName(unit)
level = UnitLevel(unit) level = UnitLevel(unit)
guild = GetGuildInfo(unit) guild = GetGuildInfo(unit)
AddData("players", name, class, level, nil, guild) AddData(name, class, level, guild)
RememberByUnit(unit, name, class) RememberByUnit(unit, name, class)
end end
@@ -139,7 +119,7 @@ libunitscan:SetScript("OnEvent", function()
for i = 1, C_FriendList.GetNumWhoResults() do for i = 1, C_FriendList.GetNumWhoResults() do
local info = C_FriendList.GetWhoInfo(i) local info = C_FriendList.GetWhoInfo(i)
if info then if info then
AddData("players", info.fullName, info.filename, info.level, nil, info.fullGuildName) AddData(info.fullName, info.filename, info.level, info.fullGuildName)
end end
end end
@@ -147,55 +127,16 @@ libunitscan:SetScript("OnEvent", function()
local scan = event == "PLAYER_TARGET_CHANGED" and "target" local scan = event == "PLAYER_TARGET_CHANGED" and "target"
or event == "NAME_PLATE_UNIT_ADDED" and arg1 or event == "NAME_PLATE_UNIT_ADDED" and arg1
or "mouseover" or "mouseover"
local name, class, level, elite, guild, _ if UnitIsPlayer(scan) then
if UnitExists(scan) then local name = UnitName(scan)
if UnitIsPlayer(scan) then local class = UnitClassBase(scan)
class = UnitClassBase(scan) -- UnitLevel returns -1 for unknown levels, don't overwrite known values
level = UnitLevel(scan) local level = UnitLevel(scan)
-- UnitLevel returns -1 for unknown levels, don't overwrite known values level = level > 0 and level or nil
level = level > 0 and level or nil AddData(name, class, level, GetGuildInfo(scan))
name = UnitName(scan) RememberByUnit(scan, name, class)
guild = GetGuildInfo(scan)
AddData("players", name, class, level, nil, guild)
RememberByUnit(scan, name, class)
else
class = UnitClassBase(scan)
elite = UnitClassification(scan)
level = UnitLevel(scan)
-- UnitLevel returns -1 for unknown levels, don't overwrite known values
level = level > 0 and level or nil
name = UnitName(scan)
guild = UnitSubName(scan)
AddData("mobs", name, class, level, elite, guild)
end
end end
end end
end) end)
-- setup sound function switches
local SoundOn = PlaySound
local SoundOff = function() return end
libunitscan:SetScript("OnUpdate", function()
-- don't scan when another unit is in target
if UnitExists("target") or UnitName("target") then return end
local name = next(queue)
if name then
-- disable sound
_G.PlaySound = SoundOff
-- try to target the unknown unit
TargetByName(name, true)
ClearTarget()
-- enable sound again
_G.PlaySound = SoundOn
queue[name] = nil
end
this:Hide()
end)
pfUI.api.libunitscan = libunitscan pfUI.api.libunitscan = libunitscan
+4 -1
View File
@@ -59,7 +59,10 @@ pfUI:RegisterModule("afkcam", function ()
if not this:IsVisible() then return end if not this:IsVisible() then return end
local info = _G.ChatTypeInfo[string.gsub(event,"CHAT_MSG_","")] local info = _G.ChatTypeInfo[string.gsub(event,"CHAT_MSG_","")]
if not info then return end if not info then return end
local class = GetUnitInfo(arg2 or "") -- the engine name-queries a chat sender before the event fires, so the
-- sender's GUID (arg12) always resolves
local _, class
if arg12 and arg12 ~= "" then _, class = GetPlayerInfoByGUID(arg12) end
local author, msg local author, msg
if class and class ~= UNKNOWN then if class and class ~= UNKNOWN then
local class_color = PFUI_CLASS_COLORS[class]:GenerateHexColorMarkup() local class_color = PFUI_CLASS_COLORS[class]:GenerateHexColorMarkup()
+17 -36
View File
@@ -1040,9 +1040,7 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
local level = plate.original.level:IsShown() and plate.original.level:GetObjectType() == "FontString" and tonumber(plate.original.level:GetText()) or "??" local level = plate.original.level:IsShown() and plate.original.level:GetObjectType() == "FontString" and tonumber(plate.original.level:GetText()) or "??"
-- reset per-unit cache when the plate is reassigned. Gate on GUID *and* -- reset per-unit cache when the plate is reassigned. Gate on GUID *and*
-- name — name alone misses pool reuse between same-named units (e.g. plate -- name — name alone misses pool reuse between same-named units.
-- held a player "Ironforge Guard" and is now reassigned to the NPC by the
-- same name), which would leak a stale "PLAYER" hint into GetUnitInfo.
-- Wipe the whole cache table: the PERF gates below ("only update X when -- Wipe the whole cache table: the PERF gates below ("only update X when
-- X changed") would otherwise skip bar/color/text updates when the new -- X changed") would otherwise skip bar/color/text updates when the new
-- unit happens to share a cached value with the previous occupant -- unit happens to share a cached value with the previous occupant
@@ -1065,27 +1063,26 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end if event == "PLAYER_TARGET_CHANGED" then unitstr = nil end
-- remove unitstr when it doesn't resolve to this plate's unit (stale istarget, -- remove unitstr when it doesn't resolve to this plate's unit (stale istarget,
-- stale mouseover guid or a despawned unit). Must run before the cache fills -- stale mouseover guid or a despawned unit)
-- below -- an unrelated unitstr would poison cache.player/minion with the
-- *other* unit's answer, and the nil-gate would then never recompute it.
if unitstr and UnitName(unitstr) ~= name then unitstr = nil end if unitstr and UnitName(unitstr) ~= name then unitstr = nil end
-- resolve player vs npc from plate's own unit so libunitscan can't return -- the plate's own unit token says who it is; a player too high to show a
-- a player record for an NPC sharing the same name (e.g. Chromie). Stored -- level keeps the one the player database last saw
-- as true/false/nil so it doubles as the GetUnitInfo hint. local unit = plate.unit
if plate.cache.player == nil and unitstr then local player, class, elite, guild, ulevel
plate.cache.player = UnitIsPlayer(unitstr) and true or false if unit then
player = UnitIsPlayer(unit)
class = player and UnitClassBase(unit)
elite = UnitClassification(unit)
guild = player and GetGuildInfo(unit) or UnitSubName(unit)
ulevel = UnitLevel(unit)
if player and ulevel <= 0 then
local _
_, ulevel = GetUnitInfo(name)
end
end end
if plate.cache.minion == nil and unitstr then
plate.cache.minion = UnitIsMinion(unitstr) and true or false
end
local class, ulevel, elite, player, guild
if not plate.cache.minion then
class, ulevel, elite, player, guild = GetUnitInfo(name, true, plate.cache.player)
end
if plate.cache.player ~= nil then player = plate.cache.player or nil end
-- Use database level ONLY if current level is ?? (fixes ?? after reload, but doesn't override visible levels) -- Use the unit's level ONLY if current level is ?? (fixes ?? after reload, but doesn't override visible levels)
local levelFromDB = false local levelFromDB = false
if level == "??" and ulevel and ulevel > 0 then if level == "??" and ulevel and ulevel > 0 then
level = ulevel level = ulevel
@@ -1095,13 +1092,9 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
local unittype = GetUnitType(plate) local unittype = GetUnitType(plate)
local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size local font_size = C.nameplates.use_unitfonts == "1" and C.global.font_unit_size or C.global.font_size
-- ignore players with npc names if plate level is lower than player level
if ulevel and ulevel > (level == "??" and -1 or level) then player = nil end
if player and unittype == "ENEMY_NPC" then unittype = "ENEMY_PLAYER" end if player and unittype == "ENEMY_NPC" then unittype = "ENEMY_PLAYER" end
if player and unittype == "FRIENDLY_NPC" then unittype = "FRIENDLY_PLAYER" end if player and unittype == "FRIENDLY_NPC" then unittype = "FRIENDLY_PLAYER" end
elite = plate.original.levelicon:IsShown() and not player and "boss" or elite elite = plate.original.levelicon:IsShown() and not player and "boss" or elite
if not class and not plate.cache.minion then plate.wait_for_scan = true end
-- skip data updates on invisible frames -- skip data updates on invisible frames
if not visible then return end if not visible then return end
@@ -1219,7 +1212,6 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
if cfg.showhp then if cfg.showhp then
local rhp, rhpmax, estimated local rhp, rhpmax, estimated
local unit = plate.unit
if unit then if unit then
local npHp = UnitHealth(unit) local npHp = UnitHealth(unit)
local npMaxHp = UnitHealthMax(unit) local npMaxHp = UnitHealthMax(unit)
@@ -1491,7 +1483,6 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
local update local update
local original = nameplate.original local original = nameplate.original
local name = original.name:GetText()
local mouseover = nameplate.cachedGuid and nameplate.cachedGuid == frameState.mouseoverGuid or nil local mouseover = nameplate.cachedGuid and nameplate.cachedGuid == frameState.mouseoverGuid or nil
-- trigger queued event update -- trigger queued event update
@@ -1575,16 +1566,6 @@ nameplates:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "target", "focu
update = true update = true
end end
-- trigger update when unit was found. Pass the cached player hint so the
-- retry probes the same table OnDataChanged will read on the next pass —
-- otherwise an NPC sharing a name with a known player flips wait_for_scan
-- off here, then OnDataChanged re-sets it, every frame until the mob scan
-- lands.
if nameplate.wait_for_scan and not nameplate.cache.minion and GetUnitInfo(name, true, nameplate.cache.player) then
nameplate.wait_for_scan = nil
update = true
end
-- trigger update when name color changed (includes combat state check). -- trigger update when name color changed (includes combat state check).
-- Skipped once OnDataChanged owns the name colour (class-coloured friendly -- Skipped once OnDataChanged owns the name colour (class-coloured friendly
-- players), so the two writers cannot overwrite each other. -- players), so the two writers cannot overwrite each other.