Refactor nameplate cast tracking to be fully GUID-based

Problem:
Since a recent Turtle-WoW update, nameplates would show the same castbar on all mobs sharing a name (e.g. multiple "Defias Rogue Wizard"). Additionally, a targeted mob that was not casting would incorrectly display the castbar of a nearby mob with the same name.

Changes:
**`modules/nameplates.lua`**
- Removed `hasNampower` version-sniffing entirely. Nampower is a hard requirement for pfUI; all related guards have been removed and the code runs unconditionally.
- Castbar lookup now always uses `nameplate.cachedGuid` directly — never `GetUnitGUID("target")` or a unit name string.
- Removed dead fallback block using `UnitCastingInfo("target")`.
- Restored fallback via `pfGetCastInfo(cachedGuid)` / `pfGetChannelInfo(cachedGuid)` (see libcast changes below) for casts not tracked by libdebuff (e.g. already-in-progress casts on login).

**`libs/libcast.lua`**
- Renamed `UnitCastingInfo` → `pfGetCastInfo` and `UnitChannelInfo` → `pfGetChannelInfo` to avoid confusion with the Blizzard API of the same name, which does not exist in Vanilla.
- Name-based fallback lookup (`libcast.db[unitName]`) is now skipped when a GUID is available. This prevents cast bleed between mobs that share a name.

**`libs/libpredict.lua`, `modules/castbar.lua`, `modules/afkcam.lua`**
- Updated all call sites to use the renamed `pfGetCastInfo` / `pfGetChannelInfo`.
This commit is contained in:
Meow
2026-03-23 10:47:42 +01:00
parent cd4a3343cb
commit 2ef9c5ee1e
5 changed files with 84 additions and 135 deletions
+10 -8
View File
@@ -4,10 +4,10 @@ setfenv(1, pfUI:GetEnvironment())
--[[ libcast ]]-- --[[ libcast ]]--
-- A pfUI library that detects and saves all ongoing castbars of players, NPCs and enemies. -- A pfUI library that detects and saves all ongoing castbars of players, NPCs and enemies.
-- The library also includes spells that usually don't have a castbar like Multi-Shot and Aimed Shot. -- The library also includes spells that usually don't have a castbar like Multi-Shot and Aimed Shot.
-- This is exclusivly used for vanilla in order to provide UnitChannelInfo and UnitCastingInfo functions. -- This is exclusivly used for vanilla in order to provide pfGetChannelInfo and pfGetCastInfo functions.
-- --
-- External functions: -- External functions:
-- UnitChannelInfo(unit) -- pfGetChannelInfo(unit)
-- Returns information on the spell currently cast by the specified unit. -- Returns information on the spell currently cast by the specified unit.
-- Returns nil if no spell is being cast. -- Returns nil if no spell is being cast.
-- --
@@ -19,7 +19,7 @@ setfenv(1, pfUI:GetEnvironment())
-- endTime[Number] - Specifies when casting will end, in milliseconds. -- endTime[Number] - Specifies when casting will end, in milliseconds.
-- isTradeSkill[Boolean] - (DUMMY) Specifies if the cast is a tradeskill -- isTradeSkill[Boolean] - (DUMMY) Specifies if the cast is a tradeskill
-- --
-- UnitCastingInfo(unit) -- pfGetCastInfo(unit)
-- Returns information on the spell currently channeled by the specified unit. -- Returns information on the spell currently channeled by the specified unit.
-- Returns nil if no spell is being channeled. -- Returns nil if no spell is being channeled.
-- --
@@ -53,7 +53,7 @@ local scanner = libtipscan:GetScanner("libcast")
local libcast = CreateFrame("Frame", "pfEnemyCast") local libcast = CreateFrame("Frame", "pfEnemyCast")
local player = UnitName("player") local player = UnitName("player")
UnitChannelInfo = function(unit) pfGetChannelInfo = function(unit)
-- convert to name if unitstring was given -- convert to name if unitstring was given
local unitName = pfValidUnits[unit] and UnitName(unit) or unit local unitName = pfValidUnits[unit] and UnitName(unit) or unit
@@ -117,7 +117,8 @@ UnitChannelInfo = function(unit)
end end
-- Fallback to name-based lookup (CHAT_MSG castbars) -- Fallback to name-based lookup (CHAT_MSG castbars)
if not db and libcast.db[unitName] then -- Skip when GUID is available to avoid same-name mob bleed
if not db and not guid and libcast.db[unitName] then
db = libcast.db[unitName] db = libcast.db[unitName]
end end
@@ -145,7 +146,7 @@ UnitChannelInfo = function(unit)
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end end
UnitCastingInfo = function(unit) pfGetCastInfo = function(unit)
-- convert to name if unitstring was given -- convert to name if unitstring was given
local unitName = pfValidUnits[unit] and UnitName(unit) or unit local unitName = pfValidUnits[unit] and UnitName(unit) or unit
@@ -209,7 +210,8 @@ UnitCastingInfo = function(unit)
end end
-- Fallback to name-based lookup (CHAT_MSG castbars) -- Fallback to name-based lookup (CHAT_MSG castbars)
if not db and libcast.db[unitName] then -- Skip when GUID is available to avoid same-name mob bleed
if not db and not guid and libcast.db[unitName] then
db = libcast.db[unitName] db = libcast.db[unitName]
end end
@@ -533,7 +535,7 @@ local function CastCustom(id, bookType, rawSpellName, rank, texture, castingTime
local func = libcast.customcast[strlower(rawSpellName)] local func = libcast.customcast[strlower(rawSpellName)]
if not func then return end if not func then return end
if GetSpellCooldown(id, bookType) == 0 or UnitCastingInfo(player) then return end -- detect casting if GetSpellCooldown(id, bookType) == 0 or pfGetCastInfo(player) then return end -- detect casting
func(true) func(true)
end end
+5 -5
View File
@@ -446,7 +446,7 @@ function libpredict:ParseComm(sender, msg)
rank = tonumber(rankStr) rank = tonumber(rankStr)
end end
end end
elseif select and UnitCastingInfo then elseif select and pfGetCastInfo then
-- latest healcomm -- latest healcomm
msgtype = tonumber(string.sub(msg, 1, 3)) msgtype = tonumber(string.sub(msg, 1, 3))
if not msgtype then return end if not msgtype then return end
@@ -456,8 +456,8 @@ function libpredict:ParseComm(sender, msg)
heal = tonumber(string.sub(msg, 4, 8)) heal = tonumber(string.sub(msg, 4, 8))
target = string.sub(msg,9, -1) target = string.sub(msg,9, -1)
local starttime = select(5, UnitCastingInfo(sender)) local starttime = select(5, pfGetCastInfo(sender))
local endtime = select(6, UnitCastingInfo(sender)) local endtime = select(6, pfGetCastInfo(sender))
if not starttime or not endtime then return end if not starttime or not endtime then return end
time = endtime - starttime time = endtime - starttime
elseif msgtype == 1 then elseif msgtype == 1 then
@@ -466,8 +466,8 @@ function libpredict:ParseComm(sender, msg)
msgtype = "Heal" msgtype = "Heal"
heal = tonumber(string.sub(msg,4, 8)) heal = tonumber(string.sub(msg,4, 8))
target = {strsplit(":", string.sub(msg,9, -1))} target = {strsplit(":", string.sub(msg,9, -1))}
local starttime = select(5, UnitCastingInfo(sender)) local starttime = select(5, pfGetCastInfo(sender))
local endtime = select(6, UnitCastingInfo(sender)) local endtime = select(6, pfGetCastInfo(sender))
if not starttime or not endtime then return end if not starttime or not endtime then return end
time = endtime - starttime time = endtime - starttime
end end
+2 -2
View File
@@ -170,8 +170,8 @@ pfUI:RegisterModule("afkcam", "vanilla:tbc", function ()
if ( this.tick or 0) > GetTime() then return else this.tick = GetTime() + 1 end if ( this.tick or 0) > GetTime() then return else this.tick = GetTime() + 1 end
local name = UnitName("player") local name = UnitName("player")
local cast = UnitCastingInfo(name) local cast = pfGetCastInfo(name)
if not cast then cast = UnitChannelInfo(name) end if not cast then cast = pfGetChannelInfo(name) end
if not this.delay then this.delay = 0 end if not this.delay then this.delay = 0 end
if cast then if cast then
+7 -7
View File
@@ -139,13 +139,13 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
query = UnitName("player") query = UnitName("player")
end end
-- Fallback: UnitCastingInfo only when no focusGuid (Nampower not available for this unit) -- Fallback: pfGetCastInfo only when no focusGuid (Nampower not available for this unit)
if not cast and not castBlocked and not focusGuid and UnitCastingInfo then if not cast and not castBlocked and not focusGuid and pfGetCastInfo then
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(query) cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetCastInfo(query)
end end
if not cast and not castBlocked and not focusGuid and UnitChannelInfo then if not cast and not castBlocked and not focusGuid and pfGetChannelInfo then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo(this.unitstr or this.unitname) channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetChannelInfo(this.unitstr or this.unitname)
cast = channel cast = channel
end end
@@ -254,12 +254,12 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
playerarg = pfUI.client <= 11200 or arg1 == "player" and true or nil playerarg = pfUI.client <= 11200 or arg1 == "player" and true or nil
if event == CASTBAR_EVENT_CAST_DELAY and playerarg then if event == CASTBAR_EVENT_CAST_DELAY and playerarg then
local isCast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(this.unitstr or this.unitname) local isCast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetCastInfo(this.unitstr or this.unitname)
if not isCast then return end if not isCast then return end
if not this.endTime then return end if not this.endTime then return end
this.delay = this.delay + (endTime - this.endTime) / 1000 this.delay = this.delay + (endTime - this.endTime) / 1000
elseif event == CASTBAR_EVENT_CHANNEL_DELAY and playerarg then elseif event == CASTBAR_EVENT_CHANNEL_DELAY and playerarg then
local isChannel, _, _, _, startTime, endTime = UnitChannelInfo(this.unitstr or this.unitname) local isChannel, _, _, _, startTime, endTime = pfGetChannelInfo(this.unitstr or this.unitname)
if not isChannel then return end if not isChannel then return end
this.delay = ( this.delay or 0 ) + this.bar:GetValue() - (endTime/1000 - GetTime()) this.delay = ( this.delay or 0 ) + this.bar:GetValue() - (endTime/1000 - GetTime())
elseif playerarg then elseif playerarg then
+60 -113
View File
@@ -2,17 +2,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
-- disable original castbars -- disable original castbars
pcall(SetCVar, "ShowVKeyCastbar", 0) pcall(SetCVar, "ShowVKeyCastbar", 0)
-- Check for Nampower support (preferred)
local hasNampower = false
if GetNampowerVersion then
local major, minor, patch = GetNampowerVersion()
patch = patch or 0
if major > 2 or (major == 2 and minor > 27) or (major == 2 and minor == 27 and patch >= 2) then
hasNampower = true
end
end
-- Local function references for performance -- Local function references for performance
local pfGetCastInfo = pfGetCastInfo -- provided by libcast for vanilla
local pfGetChannelInfo = pfGetChannelInfo -- provided by libcast for vanilla
local GetTime = GetTime local GetTime = GetTime
local UnitExists = UnitExists local UnitExists = UnitExists
local UnitName = UnitName local UnitName = UnitName
@@ -23,8 +16,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local UnitAffectingCombat = UnitAffectingCombat local UnitAffectingCombat = UnitAffectingCombat
local UnitIsUnit = UnitIsUnit local UnitIsUnit = UnitIsUnit
local UnitCanAssist = UnitCanAssist local UnitCanAssist = UnitCanAssist
local UnitCastingInfo = UnitCastingInfo
local UnitChannelInfo = UnitChannelInfo
local UnitHealth = UnitHealth local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax local UnitHealthMax = UnitHealthMax
local UnitMana = UnitMana local UnitMana = UnitMana
@@ -503,6 +494,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
_, PlayerGUID = UnitExists("player") _, PlayerGUID = UnitExists("player")
CacheConfig() CacheConfig()
this:SetGameVariables() this:SetGameVariables()
end end
-- Handle friendly zone nameplate disable feature -- Handle friendly zone nameplate disable feature
@@ -945,8 +937,8 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
local unittype = GetUnitType(red, green, blue) or "ENEMY_NPC" local unittype = GetUnitType(red, green, blue) or "ENEMY_NPC"
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
-- use superwow unit guid as unitstr if possible -- use unit guid as unitstr if possible
if hasNampower and not unitstr then if not unitstr then
unitstr = plate.parent:GetName(1) unitstr = plate.parent:GetName(1)
end end
@@ -997,7 +989,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
end end
-- target indicator -- target indicator
if hasNampower and cfg.outcombatstate then if cfg.outcombatstate then
local guid = plate.parent:GetName(1) or "" local guid = plate.parent:GetName(1) or ""
-- determine color based on combat state -- determine color based on combat state
@@ -1087,7 +1079,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
local rhp, rhpmax, estimated local rhp, rhpmax, estimated
-- Try Nampower first for real HP values via GUID -- Try Nampower first for real HP values via GUID
local guid = hasNampower and plate.parent:GetName(1) or nil local guid = plate.parent:GetName(1)
if guid and GetUnitField then if guid and GetUnitField then
local npHp = GetUnitField(guid, "health") local npHp = GetUnitField(guid, "health")
local npMaxHp = GetUnitField(guid, "maxHealth") local npMaxHp = GetUnitField(guid, "maxHealth")
@@ -1137,11 +1129,11 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
r, g, b, a = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b, 1 r, g, b, a = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b, 1
end end
if hasNampower and unitstr and UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then if unitstr and UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then
r, g, b, a = .5, .5, .5, .8 r, g, b, a = .5, .5, .5, .8
end end
if hasNampower and cfg.barcombatstate then if cfg.barcombatstate then
local guid = plate.parent:GetName(1) or "" local guid = plate.parent:GetName(1) or ""
local color = GetCombatStateColor(guid) local color = GetCombatStateColor(guid)
@@ -1248,12 +1240,10 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
local nameplate = frame.nameplate local nameplate = frame.nameplate
-- Register GUID when plate becomes visible -- Register GUID when plate becomes visible
if hasNampower then local guid = frame:GetName(1)
local guid = frame:GetName(1) if guid then
if guid then nameplate.cachedGuid = guid
nameplate.cachedGuid = guid guidRegistry[guid] = frame
guidRegistry[guid] = frame
end
end end
nameplates:OnDataChanged(nameplate) nameplates:OnDataChanged(nameplate)
@@ -1264,15 +1254,13 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
local now = state and state.now or GetTime() local now = state and state.now or GetTime()
-- Update GUID registry (lightweight, needed for event routing) -- Update GUID registry (lightweight, needed for event routing)
if hasNampower then local guid = frame:GetName(1)
local guid = frame:GetName(1) if guid and guid ~= nameplate.cachedGuid then
if guid and guid ~= nameplate.cachedGuid then if nameplate.cachedGuid and guidRegistry[nameplate.cachedGuid] == frame then
if nameplate.cachedGuid and guidRegistry[nameplate.cachedGuid] == frame then guidRegistry[nameplate.cachedGuid] = nil
guidRegistry[nameplate.cachedGuid] = nil
end
nameplate.cachedGuid = guid
guidRegistry[guid] = frame
end end
nameplate.cachedGuid = guid
guidRegistry[guid] = frame
end end
-- PERF: Intelligent throttling based on target/castbar status and plate count -- PERF: Intelligent throttling based on target/castbar status and plate count
@@ -1407,7 +1395,7 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
-- trigger update when name color changed (includes combat state check) -- trigger update when name color changed (includes combat state check)
local r, g, b = original.name:GetTextColor() local r, g, b = original.name:GetTextColor()
local inCombatWithPlayer = false local inCombatWithPlayer = false
if hasNampower and cfg.namefightcolor then if cfg.namefightcolor then
local guid = nameplate.cachedGuid local guid = nameplate.cachedGuid
if guid then if guid then
inCombatWithPlayer = UnitAffectingCombat(guid) and UnitAffectingCombat("player") inCombatWithPlayer = UnitAffectingCombat(guid) and UnitAffectingCombat("player")
@@ -1513,25 +1501,11 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
-- Use multiple checks for target detection (target variable, istarget flag, or zoomed state) -- Use multiple checks for target detection (target variable, istarget flag, or zoomed state)
local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed) local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed)
if cfg.showcastbar and ( not cfg.targetcastbar or isTargetPlate ) then if cfg.showcastbar and ( not cfg.targetcastbar or isTargetPlate ) then
local unitstr = nil -- Always use the plate's own cachedGuid for the cast lookup.
local targetGUID = nil -- Never fall back to GetUnitGUID("target") - that would show a cast from
-- a different mob just because it shares a name with the targeted plate.
-- Get GUID for CastEvents lookup - use cached GUID when available local unitstr = nameplate.cachedGuid
if isTargetPlate then local castInfo = unitstr and GetCastInfo(unitstr)
targetGUID = state and state.targetGuid
if not targetGUID then
local guid = GetUnitGUID("target")
targetGUID = guid
end
end
-- Use cached GUID for non-target plates
if not isTargetPlate then
unitstr = nameplate.cachedGuid
end
-- Check event-based cast cache first (use GUID)
local castInfo = GetCastInfo(targetGUID) or (unitstr and GetCastInfo(unitstr))
if castInfo and castInfo.spellID then if castInfo and castInfo.spellID then
-- Check if cast is still valid -- Check if cast is still valid
@@ -1575,72 +1549,45 @@ nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
nameplate.castbar:Show() nameplate.castbar:Show()
end end
else else
-- Fallback to API calls only when no GUID available (Nampower not tracking this unit) -- libcast provides pfGetCastInfo/pfGetChannelInfo for vanilla.
local channel, cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill -- Pass cachedGuid directly so it's always mob-specific, never name-based.
if unitstr then
if targetGUID then local cast, _, _, texture, startTime, endTime = pfGetCastInfo(unitstr)
-- We have a GUID but Nampower has no cast info. local channel
-- Fall back to API for the target plate (channels may not be in cast cache) if not cast then
if isTargetPlate and UnitExists("target") then channel, _, _, texture, startTime, endTime = pfGetChannelInfo(unitstr)
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo("target")
if not cast then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo("target")
end
end end
if not cast and not channel then
if cast or channel then
local effect = cast or channel
local duration = endTime - startTime
local max = duration / 1000
local cur = GetTime() - startTime / 1000
if channel then cur = max + startTime / 1000 - GetTime() end
nameplate.castbar:SetMinMaxValues(0, max)
nameplate.castbar:SetValue(cur)
local remaining = channel and cur or (max - cur)
if C.unitframes.castbardecimals == "1" then
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
else
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
end
if C.nameplates.spellname == "1" then
nameplate.castbar.spell:SetText(effect)
else
nameplate.castbar.spell:SetText("")
end
if texture then
nameplate.castbar.icon.tex:SetTexture(texture)
nameplate.castbar.icon.tex:SetTexCoord(.1, .9, .1, .9)
end
nameplate.castbar:Show()
else
nameplate.castbar:Hide() nameplate.castbar:Hide()
end end
elseif isTargetPlate and UnitExists("target") then
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo("target")
if not cast then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo("target")
end
elseif unitstr then
local guid = GetUnitGUID(unitstr)
local q = guid or unitstr
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(q)
if not cast then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo(q)
end
elseif name then
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitCastingInfo(name)
if not cast then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo(name)
end
end
if not cast and not channel then
nameplate.castbar:Hide()
else else
local effect = cast or channel nameplate.castbar:Hide()
local duration = endTime - startTime
local max = duration / 1000
local cur = GetTime() - startTime / 1000
if channel then cur = max + startTime/1000 - GetTime() end
nameplate.castbar:SetMinMaxValues(0, duration/1000)
nameplate.castbar:SetValue(cur)
local remaining = max - cur
if channel then remaining = cur end
if C.unitframes.castbardecimals == "1" then
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
else
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
end
if C.nameplates.spellname == "1" then
nameplate.castbar.spell:SetText(effect)
else
nameplate.castbar.spell:SetText("")
end
nameplate.castbar:Show()
if texture then
nameplate.castbar.icon.tex:SetTexture(texture)
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
end end
end end
else else