Author SHA1 Message Date
Brues 2dbec4b8d6 Drive unit-frame clicks via secure attributes
Replace the Lua OnClick dispatch (OnClick/ClickAction/RightClickAction) with
secure frame attributes: default type1=target / type2=menu, and EnableClickCast
now writes type/spell/macrotext/menu/target/focus attributes per button+modifier
instead of caching a clickactions table read at click time. Drops the now-dead
clickactions table and buttons list.

Also set a "unit" attribute on each frame (at creation and re-synced in
UpdateVisibility to the live token, so a party shown on the raid grid reports
partyN, not raidN) so the hovered unit resolves from the frame.

Match the target/focus/menu click keywords exactly rather than by prefix, so a
spell whose name starts with "focus"/"target"/"menu" (e.g. Focus Magic) casts
instead of being swallowed as the keyword action.
2026-08-01 18:12:28 -05:00
Brues 0b76973111 Use SetShown and simplify component default
Replace explicit Show/Hide conditionals in api/unitframes.lua with SetShown(...) for ressIcon, leaderIcon, lootIcon, pvpIcon, and restIcon to reduce branching and improve readability. Also remove the local shadowing of the parameter 'component' by using assignment (component = component or ""). No intended behavior changes.
2026-08-01 14:37:23 -05:00
Brues f62e50d592 Use secure unit attributes; remove mouseover scripts
Switch unitframes to attribute-driven clicks and remove mouseover tooltip handlers. Adds SetAttribute("unit") in UpdateVisibility and sets frame attributes (unit and type1='target') when creating frames. Removes OnEnter/OnLeave functions and their SetScript registrations and eliminates direct TargetUnit calls in ClickAction
2026-08-01 13:12:33 -05:00
Brues 68e2d72367 We no longer use these features from SuperWoW/Nampower 2026-08-01 13:12:33 -05:00
Brues 714faad4a7 Drive nameplate castbars off UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for nameplate tokens, so populate the
per-GUID castState cache from UNIT_SPELLCAST_{START,CHANNEL_START} (poll
PollCastInfo for the timing the payload omits) and clear it on
{STOP,CHANNEL_STOP} -- the remote poll fires STOP for natural end, interrupt,
and cancel alike. Replaces the nampower SPELL_{START,FAILED}_OTHER path; the
^nameplate arg1 gate ignores the target/focus/party fires of the same events,
and the NAME_PLATE_UNIT_ADDED seed still catches a plate spawning mid-cast.
Nameplate cast detection is now nampower-free.
2026-08-01 13:12:33 -05:00
Brues e8764a4b56 Route target/focus castbars through remote UNIT_SPELLCAST_*
ClassicAPI now fires UNIT_SPELLCAST_* for remote tokens (target/focus/...)
via PollRemote, so drop the nampower SPELL_*_OTHER path and drive all three
bars off one event model. The gate is now token-based (arg1 == this.unitstr,
plus the arg1=="player" + UnitIsUnit case for target=self); the RunNextFrame
defer is gone since the remote poll fires after UnitCastingInfo is readable.
Cast detection is now nampower-free across every bar.

Use the event's rank payload (arg5): thread it through RefreshBar into
StampBar, which now only calls C_Spell.GetSpellSubtext as a fallback on the
retarget re-poll (PLAYER_TARGET/FOCUS_CHANGED) where no event is in hand.
2026-08-01 13:12:33 -05:00
Brues 63b9756de2 Migrate player castbar to ClassicAPI UNIT_SPELLCAST_* events
Drive the player cast path off ClassicAPI's synthesized, player-only
UNIT_SPELLCAST_* events instead of the vanilla SPELLCAST_* plus nampower
SPELL_{START,GO,DELAYED}_SELF mix. START/CHANNEL_START poll UnitCastingInfo/
UnitChannelInfo and stamp; STOP/CHANNEL_STOP/FAILED/INTERRUPTED clear;
SUCCEEDED drives tradeskill craft counting; DELAYED/CHANNEL_UPDATE re-poll.
Since UNIT_SPELLCAST_START fires for chained same-spell recasts, the nampower
SELF deps and their RunNextFrame co-hook-ordering workarounds are gone.
Remote target/focus bars keep SPELL_*_OTHER (the new events are player-only).

Fix the cast-start flash: StampBar now primes the fill on the stamp frame,
and ClearBar no longer snaps the bar to full (that snap lingered in the fade
tail and flashed when the next cast stamped).

Add a red flash on a cancelled cast: new appearance.castbar.failcolor,
flashed by ClearBar only when a cast was actually in progress.

Handle pushback Quartz-style: DELAYED/CHANNEL_UPDATE re-poll just the times
and accumulate the endMs shift into this.delay for the +/- indicator (cast
+X, channel -X), rather than a full restamp.
2026-08-01 13:12:33 -05:00
5 changed files with 181 additions and 338 deletions
-3
View File
@@ -41,7 +41,6 @@ Provides:
### [SuperWoW](https://github.com/balakethelock/SuperWoW) ### [SuperWoW](https://github.com/balakethelock/SuperWoW)
Provides: Provides:
- UnitPosition for distance calculations
- SetMouseoverUnit for improved targeting - SetMouseoverUnit for improved targeting
### [Nampower](https://github.com/brues-code/nampower) ### [Nampower](https://github.com/brues-code/nampower)
@@ -49,8 +48,6 @@ Provides:
Provides: Provides:
- Spell queue indicator - Spell queue indicator
- GCD indicator - GCD indicator
- Reactive spell detection
- Enhanced cast information
### [UnitXP_SP3](https://codeberg.org/konaka/UnitXP_SP3) ### [UnitXP_SP3](https://codeberg.org/konaka/UnitXP_SP3)
+1
View File
@@ -155,6 +155,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("appearance", "cd", "dynamicsize", "1") pfUI:UpdateConfig("appearance", "cd", "dynamicsize", "1")
pfUI:UpdateConfig("appearance", "castbar", "castbarcolor", ".7,.7,.9,.8") pfUI:UpdateConfig("appearance", "castbar", "castbarcolor", ".7,.7,.9,.8")
pfUI:UpdateConfig("appearance", "castbar", "channelcolor", ".9,.9,.7,.8") pfUI:UpdateConfig("appearance", "castbar", "channelcolor", ".9,.9,.7,.8")
pfUI:UpdateConfig("appearance", "castbar", "failcolor", "1,.2,.2,.8")
pfUI:UpdateConfig("appearance", "castbar", "texture", "Interface\\AddOns\\pfUI\\img\\bar") pfUI:UpdateConfig("appearance", "castbar", "texture", "Interface\\AddOns\\pfUI\\img\\bar")
pfUI:UpdateConfig("appearance", "infight", "screen", "0") pfUI:UpdateConfig("appearance", "infight", "screen", "0")
pfUI:UpdateConfig("appearance", "infight", "aggro", "0") pfUI:UpdateConfig("appearance", "infight", "aggro", "0")
+31 -157
View File
@@ -238,6 +238,7 @@ function pfUI.uf:UpdateVisibility()
end end
local unitstr = string.format("%s%s", self.label or "", self.id or "") local unitstr = string.format("%s%s", self.label or "", self.id or "")
self:SetAttribute("unit", unitstr ~= "" and unitstr or nil)
local visibility = string.format("[target=%s,exists] show; hide", unitstr) local visibility = string.format("[target=%s,exists] show; hide", unitstr)
-- Group frames are redundant when the group is already shown as a raid grid: -- Group frames are redundant when the group is already shown as a raid grid:
@@ -1110,11 +1111,7 @@ function pfUI.uf.OnUpdate()
-- update ressurections -- update ressurections
local ress = libpredict:UnitHasIncomingResurrection(unit) local ress = libpredict:UnitHasIncomingResurrection(unit)
if ress and UnitIsDeadOrGhost(unit) then this.ressIcon:SetShown(ress and UnitIsDeadOrGhost(unit))
this.ressIcon:Show()
else
this.ressIcon:Hide()
end
end end
end end
end end
@@ -1177,52 +1174,6 @@ function pfUI.uf.OnUpdate()
end end
end end
function pfUI.uf.OnEnter()
if not this.label then return end
local unitstr = this.label .. this.id
if UnitExists(unitstr) then
SetMouseoverUnit(unitstr)
end
if this.config.showtooltip == "0" then return end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.label .. this.id)
GameTooltip:Show()
end
function pfUI.uf.OnLeave()
SetMouseoverUnit("")
GameTooltip:FadeOut()
end
function pfUI.uf.OnClick()
if not this.label and this.unitname then
TargetByName(this.unitname, true)
else
pfUI.uf:ClickAction(arg1)
end
end
function pfUI.uf:RightClickAction(unit)
if unit == "player" then
ToggleDropDownMenu(1, nil, PlayerFrameDropDown, "cursor")
elseif unit == "target" then
ToggleDropDownMenu(1, nil, TargetFrameDropDown, "cursor")
elseif unit == "pet" then
ToggleDropDownMenu(1, nil, PetFrameDropDown, "cursor")
elseif unit == "party" or strfind(unit, "party%d") then
ToggleDropDownMenu(1, nil, getglobal("PartyMemberFrame" .. this.id .. "DropDown"), "cursor")
elseif unit == "raid" or strfind(unit, "raid%d") then
local name = this.lastUnit
local unitstr = this.label .. this.id
FriendsDropDown.displayMode = "MENU"
FriendsDropDown.initialize = function() UnitPopup_ShowMenu(_G[UIDROPDOWNMENU_OPEN_MENU], "PARTY", unitstr, name, id) end
ToggleDropDownMenu(1, nil, FriendsDropDown, "cursor")
end
end
function pfUI.uf:EnableEvents() function pfUI.uf:EnableEvents()
local f = self local f = self
@@ -1261,13 +1212,12 @@ end
function pfUI.uf:EnableScripts() function pfUI.uf:EnableScripts()
local f = self local f = self
f:SetScript("OnClick", pfUI.uf.OnClick) f:SetAttribute("type1", "target")
f:SetAttribute("type2", "menu")
f:SetScript("OnShow", pfUI.uf.OnShow) f:SetScript("OnShow", pfUI.uf.OnShow)
f:SetScript("OnEvent", pfUI.uf.OnEvent) f:SetScript("OnEvent", pfUI.uf.OnEvent)
f:SetScript("OnUpdate", pfUI.uf.OnUpdate) f:SetScript("OnUpdate", pfUI.uf.OnUpdate)
f:SetScript("OnEnter", pfUI.uf.OnEnter)
f:SetScript("OnLeave", pfUI.uf.OnLeave)
f:EnableClickCast() f:EnableClickCast()
-- add frame to visibility refresh handler -- add frame to visibility refresh handler
@@ -1423,6 +1373,10 @@ function pfUI.uf:CreateUnitFrame(unit, id, config, tick)
f:Hide() f:Hide()
end end
if f.label ~= "" then
f:SetAttribute("unit", f.label .. f.id)
end
-- register frame for clique -- register frame for clique
_G.ClickCastFrames = ClickCastFrames or {} _G.ClickCastFrames = ClickCastFrames or {}
ClickCastFrames[f] = true ClickCastFrames[f] = true
@@ -1477,11 +1431,7 @@ function pfUI.uf:RefreshIndicators(unit)
local unitstr = unit.label .. unit.id local unitstr = unit.label .. unit.id
if unit.leaderIcon then -- Leader Icon if unit.leaderIcon then -- Leader Icon
if unit.config.leadericon == "1" and UnitIsPartyLeader(unitstr) and IsInGroup() then unit.leaderIcon:SetShown(unit.config.leadericon == "1" and UnitIsPartyLeader(unitstr) and IsInGroup())
unit.leaderIcon:Show()
else
unit.leaderIcon:Hide()
end
end end
if unit.lootIcon then -- Loot Icon if unit.lootIcon then -- Loot Icon
@@ -1491,29 +1441,16 @@ function pfUI.uf:RefreshIndicators(unit)
-- no third return value here.. but leaving this as a hint -- no third return value here.. but leaving this as a hint
local method, group, raid = GetLootMethod() local method, group, raid = GetLootMethod()
local name = group and UnitName(group == 0 and "player" or "party"..group) or raid and UnitName("raid"..raid) or nil local name = group and UnitName(group == 0 and "player" or "party"..group) or raid and UnitName("raid"..raid) or nil
unit.lootIcon:SetShown(name and name == UnitName(unitstr))
if name and name == UnitName(unitstr) then
unit.lootIcon:Show()
else
unit.lootIcon:Hide()
end
end end
end end
if unit.pvpIcon then -- PvP Icon if unit.pvpIcon then -- PvP Icon
if unit.config.showPVP == "1" and UnitIsPVP(unitstr) then unit.pvpIcon:SetShown(unit.config.showPVP == "1" and UnitIsPVP(unitstr))
unit.pvpIcon:Show()
else
unit.pvpIcon:Hide()
end
end end
if unit.restIcon and unit:GetName() == "pfPlayer" then -- Rest Icon if unit.restIcon and unit:GetName() == "pfPlayer" then -- Rest Icon
if C.unitframes.player.showRest == "1" and UnitIsUnit(unitstr, "player") and IsResting() then unit.restIcon:SetShown(C.unitframes.player.showRest == "1" and UnitIsUnit(unitstr, "player") and IsResting())
unit.restIcon:Show()
else
unit.restIcon:Hide()
end
end end
if unit.happinessIcon and unit:GetName() == "pfPet" then -- Happiness Icon if unit.happinessIcon and unit:GetName() == "pfPet" then -- Happiness Icon
@@ -1583,7 +1520,7 @@ function pfUI.uf:RefreshUnit(unit, component)
if not unit.hp then return end if not unit.hp then return end
if not unit.power then return end if not unit.power then return end
if not unit.id then unit.id = "" end if not unit.id then unit.id = "" end
local component = component or "" component = component or ""
-- don't update scanner activity -- don't update scanner activity
if unit.label == "target" or unit.label == "targettarget" or unit.label == "targettargettarget" then if unit.label == "target" or unit.label == "targettarget" or unit.label == "targettargettarget" then
@@ -2126,14 +2063,6 @@ function pfUI.uf:RefreshUnit(unit, component)
end end
end end
local buttons = {
[1] = "LeftButton",
[2] = "RightButton",
[3] = "MiddleButton",
[4] = "Button4",
[5] = "Button5",
}
local modifiers = { local modifiers = {
[""] = "", [""] = "",
["alt"] = "_alt", ["alt"] = "_alt",
@@ -2143,86 +2072,31 @@ local modifiers = {
function pfUI.uf:EnableClickCast() function pfUI.uf:EnableClickCast()
if self.config.clickcast ~= "1" then return end if self.config.clickcast ~= "1" then return end
for bid, button in pairs(buttons) do for bid = 1, 5 do -- LeftButton, RightButton, MiddleButton, Button4, Button5
for modifier, mconf in pairs(modifiers) do for modifier, mconf in pairs(modifiers) do
local bconf = bid == 1 and "" or bid local bconf = bid == 1 and "" or bid
if pfUI_config.unitframes["clickcast"..bconf..mconf] ~= "" then local action = pfUI_config.unitframes["clickcast"..bconf..mconf]
-- fill clickaction table if action and action ~= "" then
self.clickactions = self.clickactions or {} local prefix = modifier ~= "" and (modifier .. "-") or ""
self.clickactions[modifier..button] = pfUI_config.unitframes["clickcast"..bconf..mconf] local low = string.lower(action)
if low == "menu" then
self:SetAttribute(prefix .. "type" .. bid, "menu")
elseif low == "target" then
self:SetAttribute(prefix .. "type" .. bid, "target")
elseif low == "focus" then
self:SetAttribute(prefix .. "type" .. bid, "focus")
elseif string.find(action, "^/") then
self:SetAttribute(prefix .. "type" .. bid, "macro")
self:SetAttribute(prefix .. "macrotext" .. bid, action)
else
self:SetAttribute(prefix .. "type" .. bid, "spell")
self:SetAttribute(prefix .. "spell" .. bid, action)
end
end end
end end
end end
end end
function pfUI.uf:ClickAction(button)
local label = this.label or ""
local id = this.id or ""
local unitstr = label .. id
local showmenu = button == "RightButton" and true or nil
if SpellIsTargeting() and button == "RightButton" then
SpellStopTargeting()
return
end
if SpellIsTargeting() and button == "LeftButton" then
SpellTargetUnit(unitstr)
elseif CursorHasItem() then
DropItemOnUnit(unitstr)
end
-- run click casting if enabled
local modstring = ""
modstring = IsAltKeyDown() and modstring.."alt" or modstring
modstring = IsControlKeyDown() and modstring.."ctrl" or modstring
modstring = IsShiftKeyDown() and modstring.."shift" or modstring
modstring = modstring..button
if this.clickactions and this.clickactions[modstring] then
local action_lower = string.lower(this.clickactions[modstring])
if string.find(action_lower, "^menu") then
-- show menu
showmenu = true
elseif string.find(action_lower, "^target") then
-- target unit
showmenu = nil
else
-- run click cast action
local is_macro = string.find(this.clickactions[modstring], "^%/(.+)")
local tswitch = UnitIsUnit(unitstr, "target")
TargetUnit(unitstr)
if is_macro then
RunMacroText(this.clickactions[modstring])
else
CastSpellByName(this.clickactions[modstring])
end
if not tswitch then TargetLastTarget() end
return
end
end
-- dropdown menus
if showmenu then
pfUI.uf:RightClickAction(label)
return
end
-- drop food on petframe
if label == "pet" and CursorHasItem() then
local playerClass = UnitClassBase("player")
if playerClass == "HUNTER" then
DropItemOnUnit("pet")
return
end
end
-- default click
TargetUnit(unitstr)
end
function pfUI.uf:AddIcon(frame, pos, icon, timeleft, stacks, start, duration) function pfUI.uf:AddIcon(frame, pos, icon, timeleft, stacks, start, duration)
local showtime = frame.config.indicator_time == "1" and true or nil local showtime = frame.config.indicator_time == "1" and true or nil
local showstacks = frame.config.indicator_stacks == "1" and true or nil local showstacks = frame.config.indicator_stacks == "1" and true or nil
+112 -137
View File
@@ -16,9 +16,13 @@ pfUI:RegisterModule("castbar", function ()
end end
end end
-- Clear cast state on the bar. Shows the bar full for one frame, then -- Clear cast state on the bar and start the fade-out. On a normal end the bar
-- OnUpdate fades it out. -- is left at its current fill (a completed cast is already ~full; nothing
local function ClearBar(cb) -- snaps to full, which used to flash for a frame when the next cast stamped
-- the bar). When `failed` is set for a cast that was actually in progress, the
-- bar flashes full red before fading — the cancelled-cast indicator.
local function ClearBar(cb, failed)
local wasActive = cb.endTime ~= nil
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil
cb.activeName, cb.spellID = nil, nil cb.activeName, cb.spellID = nil, nil
cb.isTradeskill = nil cb.isTradeskill = nil
@@ -26,8 +30,11 @@ pfUI:RegisterModule("castbar", function ()
cb.tradeskillSingleMs, cb.currentCraftStart = nil, nil cb.tradeskillSingleMs, cb.currentCraftStart = nil, nil
cb.lastMax = nil cb.lastMax = nil
cb.delay = 0 cb.delay = 0
cb.bar:SetMinMaxValues(1, 100) if failed and wasActive then
cb.bar:SetValue(100) cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar.failcolor))
cb.bar:SetMinMaxValues(0, 1)
cb.bar:SetValue(1)
end
if cb.bar.spark then cb.bar.spark:Hide() end if cb.bar.spark then cb.bar.spark:Hide() end
cb.fadeout = 1 cb.fadeout = 1
end end
@@ -65,8 +72,8 @@ pfUI:RegisterModule("castbar", function ()
end end
-- Mark the start of craft 2..N within an active merge (called from the -- Mark the start of craft 2..N within an active merge (called from the
-- SPELLCAST_START / SPELL_START_SELF handlers). Resets the per-craft -- UNIT_SPELLCAST_START handler). Resets the per-craft spark to the left
-- spark to the left edge of the bar. -- edge of the bar.
local function StartTradeskillCraft(cb) local function StartTradeskillCraft(cb)
cb.currentCraftStart = GetTime() * 1000 cb.currentCraftStart = GetTime() * 1000
local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0) local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0)
@@ -79,7 +86,7 @@ pfUI:RegisterModule("castbar", function ()
-- Stamp the bar with cast data and render text/icon/lag once. OnUpdate -- Stamp the bar with cast data and render text/icon/lag once. OnUpdate
-- then animates the fill from this state without touching C_Spell. -- then animates the fill from this state without touching C_Spell.
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill) local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill, rank)
cb.startTime = startMs cb.startTime = startMs
cb.endTime = endMs cb.endTime = endMs
cb.isChannel = isChannel cb.isChannel = isChannel
@@ -92,10 +99,13 @@ pfUI:RegisterModule("castbar", function ()
cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"])) cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"]))
local rank = "" -- Rank: prefer the value the UNIT_SPELLCAST_* event delivered (arg5, passed
if spellID then -- through by RefreshBar). Only the retarget re-poll has no event in hand, so
-- it falls back to a lookup.
if not rank and spellID then
rank = C_Spell.GetSpellSubtext(spellID) or "" rank = C_Spell.GetSpellSubtext(spellID) or ""
end end
rank = rank or ""
local spellname = (cb.showname and name) and (name .. " ") or "" local spellname = (cb.showname and name) and (name .. " ") or ""
local rankstr = (cb.showrank and rank ~= "") and string.format("|cffaaffcc[%s]|r", rank) or "" local rankstr = (cb.showrank and rank ~= "") and string.format("|cffaaffcc[%s]|r", rank) or ""
cb.bar.left:SetText(spellname .. rankstr) cb.bar.left:SetText(spellname .. rankstr)
@@ -122,11 +132,21 @@ pfUI:RegisterModule("castbar", function ()
cb.bar:SetMinMaxValues(0, duration) cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration cb.lastMax = duration
-- Prime the fill on this frame. StampBar otherwise leaves the previous
-- value in place (ClearBar, run on the prior cast's STOP, leaves it full),
-- so the bar would flash full for the frame between here and the next
-- OnUpdate tick. Reset the throttle too so the timer text updates promptly.
local nowSec = GetTime()
local cur = isChannel and (endMs / 1000 - nowSec) or (nowSec - startMs / 1000)
if cur < 0 then cur = 0 elseif cur > duration then cur = duration end
cb.bar:SetValue(cur)
cb.tick = 0
end end
-- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called -- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called
-- from event handlers (cast start, target/focus change), never per-frame. -- from event handlers (cast start, target/focus change), never per-frame.
local function RefreshBar(cb) local function RefreshBar(cb, rank)
local query = cb.unitstr ~= "" and cb.unitstr or cb.unitname local query = cb.unitstr ~= "" and cb.unitstr or cb.unitname
if not query or (cb.unitstr ~= "" and not UnitExists(cb.unitstr)) then if not query or (cb.unitstr ~= "" and not UnitExists(cb.unitstr)) then
ClearBar(cb) ClearBar(cb)
@@ -149,7 +169,7 @@ pfUI:RegisterModule("castbar", function ()
end end
end end
if name and startMs and endMs then if name and startMs and endMs then
StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs, isTradeskill) StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs, isTradeskill, rank)
else else
ClearBar(cb) ClearBar(cb)
end end
@@ -294,35 +314,27 @@ pfUI:RegisterModule("castbar", function ()
end end
end) end)
-- Cast lifecycle events. Player bars react to vanilla SPELLCAST_*; non- -- Cast lifecycle, entirely on ClassicAPI's UNIT_SPELLCAST_* events. They
-- player bars also react to Nampower SPELL_*_OTHER (gated by the -- fire per unit token: arg1=="player" for the player's own casts, and the
-- NP_EnableSpell{Start,Go}Events CVars, enabled by libdebuff) plus the -- remote token(s) ("target", "focus", ...) for other units -- so one set of
-- retarget event. Player events also feed non-player bars for the -- events drives every bar with no Nampower dependency. The handler routes an
-- target=self case. -- event to this bar when arg1 matches its unit, or -- since the player's own
cb:RegisterEvent("SPELLCAST_START") -- casts only ever fire arg1=="player" -- when the bar's unit resolves to the
cb:RegisterEvent("SPELLCAST_STOP") -- player (target=self). PLAYER_TARGET/FOCUS_CHANGED re-polls so a unit
cb:RegisterEvent("SPELLCAST_FAILED") -- already mid-cast when it becomes the target/focus still shows.
cb:RegisterEvent("SPELLCAST_INTERRUPTED") cb:RegisterEvent("UNIT_SPELLCAST_START")
cb:RegisterEvent("SPELLCAST_CHANNEL_START") cb:RegisterEvent("UNIT_SPELLCAST_STOP")
cb:RegisterEvent("SPELLCAST_CHANNEL_STOP") cb:RegisterEvent("UNIT_SPELLCAST_FAILED")
cb:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") cb:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
cb:RegisterEvent("SPELL_DELAYED_SELF") cb:RegisterEvent("UNIT_SPELLCAST_DELAYED")
-- Chained same-spell recasts never run the client cast path (the 1.12 cb:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
-- engine short-circuits at spellID == current-cast), so vanilla cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
-- SPELLCAST_START never fires for them. nampower's SPELL_START_SELF cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
-- (server-driven) is the only signal that shows them. cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
cb:RegisterEvent("SPELL_START_SELF") if unitstr == "target" then
if unitstr == "player" then cb:RegisterEvent("PLAYER_TARGET_CHANGED")
cb:RegisterEvent("SPELL_GO_SELF") elseif unitstr == "focus" then
end cb:RegisterEvent("PLAYER_FOCUS_CHANGED")
if unitstr ~= "player" and unitstr ~= "" then
cb:RegisterEvent("SPELL_START_OTHER")
cb:RegisterEvent("SPELL_FAILED_OTHER")
if unitstr == "target" then
cb:RegisterEvent("PLAYER_TARGET_CHANGED")
elseif unitstr == "focus" then
cb:RegisterEvent("PLAYER_FOCUS_CHANGED")
end
end end
cb:SetScript("OnEvent", function() cb:SetScript("OnEvent", function()
@@ -333,83 +345,33 @@ pfUI:RegisterModule("castbar", function ()
return return
end end
if event == "SPELL_START_OTHER" then -- UNIT_SPELLCAST_* fire per unit token (arg1). Handle an event when it's
-- arg3=casterGuid. Defer one frame so ClassicAPI's UnitChannelInfo -- for this bar's unit, or -- since the player's own casts only ever fire
-- can see the engine's +0x228 broadcast for remote-unit channels -- arg1=="player" -- when this bar's unit currently resolves to the player
-- (the cohook+packet handler runs in the same frame; the broadcast -- (target=self / focus=self).
-- propagates after). -- Args: arg1=unit, arg2=castGUID, arg3=spellID, arg4=name, arg5=rank.
if arg3 == UnitGUID(unit) then if arg1 ~= unit and not (arg1 == "player" and UnitIsUnit(unit, "player")) then
local target = this
RunNextFrame(function() RefreshBar(target) end)
end
return return
end end
if event == "SPELL_FAILED_OTHER" then if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
if arg1 == UnitGUID(unit) then ClearBar(this) end -- START also fires per craft in a same-spell chain, so an active merge
return -- just resyncs the current craft's spark/label instead of restamping.
end
-- Vanilla SPELLCAST_* + SPELL_DELAYED_SELF fire only for the local
-- player. Non-player bars handle them only when their unit currently
-- resolves to the player (target=self / focus=self).
if not UnitIsUnit(unit, 'player') then return end
if event == "SPELLCAST_START" or event == "SPELLCAST_CHANNEL_START" then
if this.tradeskillTotal then if this.tradeskillTotal then
-- Mid-chain craft N+1 of N. Keep the merged bar; resync the spark
-- to the new craft's start and refresh the "(N)" count label.
StartTradeskillCraft(this) StartTradeskillCraft(this)
else else
RefreshBar(this) RefreshBar(this, arg5)
if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1 if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount) EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount)
end end
this.pendingTradeskillCount = nil this.pendingTradeskillCount = nil
end end
elseif event == "SPELL_START_SELF" then elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
-- Catches chained same-spell recasts (no SPELLCAST_START fires) — -- Per-craft completion during a tradeskill merge (arg3 = spellID):
-- including tradeskill chaining where craft 2..N reuse one spell. -- count and clear when the chain is done. A no-op for normal casts,
-- Defer one frame so ClassicAPI's SMSG_SPELL_START co-hook has -- which are cleared by UNIT_SPELLCAST_STOP.
-- stamped g_cast before we poll, regardless of co-hook order. if this.tradeskillTotal and arg3 == this.tradeskillSpellID then
if this.tradeskillTotal then
StartTradeskillCraft(this)
else
local target = this
RunNextFrame(function()
-- Re-check: SPELL_START_SELF (nampower co-hook) fires before vanilla
-- SPELLCAST_START on the same packet, so SPELLCAST_START may have
-- entered merge in this same frame. Don't restamp over it.
if target.tradeskillTotal then
StartTradeskillCraft(target)
return
end
RefreshBar(target)
if target.unitstr == "player" and target.isTradeskill
and (target.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(target, target.startTime, target.endTime, target.pendingTradeskillCount)
end
target.pendingTradeskillCount = nil
end)
end
elseif event == "SPELLCAST_CHANNEL_STOP" then
-- A channel's stop can arrive after a following cast already claimed
-- the bar (channel->cast transition); only clear if a channel is
-- actually being shown, so it doesn't wipe an active cast bar.
if this.isChannel then ClearBar(this) end
elseif event == "SPELLCAST_STOP" then
-- During a tradeskill chain, SPELL_GO_SELF already counted this craft
-- and either cleared the bar (chain done) or kept it running. Only a
-- non-merge cast clears here.
if not this.tradeskillTotal then ClearBar(this) end
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
ClearBar(this)
elseif event == "SPELL_GO_SELF" then
-- arg2 = spellId. In a tradeskill merge, count each successful craft
-- and clear when the chain is done.
if this.tradeskillTotal and arg2 == this.tradeskillSpellID then
this.tradeskillCompleted = (this.tradeskillCompleted or 0) + 1 this.tradeskillCompleted = (this.tradeskillCompleted or 0) + 1
if this.tradeskillCompleted >= this.tradeskillTotal then if this.tradeskillCompleted >= this.tradeskillTotal then
ClearBar(this) ClearBar(this)
@@ -417,33 +379,46 @@ pfUI:RegisterModule("castbar", function ()
UpdateTradeskillLabel(this) UpdateTradeskillLabel(this)
end end
end end
elseif event == "SPELL_DELAYED_SELF" then elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
-- Cast pushback. nampower's event carries the delay (arg2); apply it -- A channel's stop can arrive after a following cast already claimed
-- locally rather than re-polling, so the bar doesn't depend on -- the bar (channel->cast transition); only clear if a channel is
-- ClassicAPI's SMSG_SPELL_DELAYED co-hook having bumped g_cast before -- actually being shown, so it doesn't wipe an active cast bar.
-- this fires (co-hook order vs nampower is not guaranteed). if this.isChannel then ClearBar(this) end
if not this.endTime or not arg2 then return end elseif event == "UNIT_SPELLCAST_STOP" then
local delayMs = tonumber(arg2) or 0 -- STOP fires between crafts in a merge too (each craft is a new cast);
if delayMs > 0 then -- UNIT_SPELLCAST_SUCCEEDED owns the count, so only a non-merge cast
this.delay = (this.delay or 0) + delayMs / 1000 -- clears here.
this.endTime = this.endTime + delayMs if not this.tradeskillTotal then ClearBar(this) end
local newDuration = (this.endTime - this.startTime) / 1000 elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then
this.bar:SetMinMaxValues(0, newDuration) ClearBar(this, true)
this.lastMax = newDuration elseif event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
end -- Pushback: a cast delayed later or a channel shortened. Following
elseif event == "SPELLCAST_CHANNEL_UPDATE" then -- Quartz, re-poll just the times and accumulate the shift into a running
-- Channel pushback. ClassicAPI doesn't track channel delay in -- this.delay for the +/- indicator, rather than a full restamp (which
-- g_channel, so we adjust endTime + delay locally and resize the -- would reset that total and re-render icon/text). ClassicAPI moves
-- bar so OnUpdate animates against the new total. -- endMs (SpellDelayed_h bumps g_cast.endMs; the MSG_CHANNEL_UPDATE
if not this.endTime or not arg1 then return end -- co-hook rewrites g_channel.endMs) while startMs stays put, so we diff
local newEndMs = GetTime() * 1000 + arg1 -- endMs. delay is a positive magnitude; OnUpdate signs it "+" for casts
local diff = this.endTime - newEndMs -- and "-" for channels. Skipped during a tradeskill merge.
if diff > 50 then if this.endTime and not this.tradeskillTotal then
this.delay = (this.delay or 0) + diff / 1000 local query = this.unitstr ~= "" and this.unitstr or this.unitname
this.endTime = newEndMs local startMs, endMs
local newDuration = (this.endTime - this.startTime) / 1000 if this.isChannel then
this.bar:SetMinMaxValues(0, newDuration) local _, _, _, s, e = C_Spell.UnitChannelInfo(query)
this.lastMax = newDuration startMs, endMs = s, e
else
local _, _, _, s, e = C_Spell.UnitCastingInfo(query)
startMs, endMs = s, e
end
if startMs and endMs then
local shift = this.isChannel and (this.endTime - endMs) or (endMs - this.endTime)
this.delay = (this.delay or 0) + shift / 1000
this.startTime = startMs
this.endTime = endMs
local newDuration = (endMs - startMs) / 1000
this.bar:SetMinMaxValues(0, newDuration)
this.lastMax = newDuration
end
end end
end end
end) end)
@@ -489,10 +464,10 @@ pfUI:RegisterModule("castbar", function ()
UpdateMovable(pfUI.castbar.player) UpdateMovable(pfUI.castbar.player)
-- Tradeskill merge: hook DoTradeSkill so the player castbar knows the -- Tradeskill merge: hook DoTradeSkill so the player castbar knows the
-- requested count before the first SPELLCAST_START fires. Always-on hook -- requested count before the first UNIT_SPELLCAST_START fires. Always-on
-- (the config knob is read at event time so toggling takes effect on the -- hook (the config knob is read at event time so toggling takes effect on
-- next craft without a /reload). DoTradeSkill is synchronous; the server -- the next craft without a /reload). DoTradeSkill is synchronous; the
-- roundtrip to SPELLCAST_START gives us plenty of time after this hook. -- server roundtrip to UNIT_SPELLCAST_START gives us plenty of time.
hooksecurefunc("DoTradeSkill", function(index, num) hooksecurefunc("DoTradeSkill", function(index, num)
if pfUI.castbar.player then if pfUI.castbar.player then
pfUI.castbar.player.pendingTradeskillCount = tonumber(num) or 1 pfUI.castbar.player.pendingTradeskillCount = tonumber(num) or 1
+37 -41
View File
@@ -64,18 +64,19 @@ pfUI:RegisterModule("nameplates", function ()
local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED) local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
-- Per-GUID cast state, populated by nampower's SPELL_START_OTHER events and -- Per-GUID cast state, populated from ClassicAPI's UNIT_SPELLCAST_* events
-- cleared on SPELL_FAILED_OTHER / plate removal / expiry. This replaces the -- (which now fire for nameplate tokens) and cleared on STOP / plate removal /
-- old per-tick C_Spell poll on every visible plate: cast detection is now -- expiry. This replaces the old per-tick C_Spell poll on every visible plate:
-- event driven, and GetCastInfo just reads this cache. -- cast detection is event driven, and GetCastInfo just reads this cache.
local castState = {} local castState = {}
-- guid -> nameplate, maintained on NAME_PLATE_UNIT_ADDED/_REMOVED so a cast -- guid -> nameplate, maintained on NAME_PLATE_UNIT_ADDED/_REMOVED so a cast
-- event can find its plate in O(1) and only cache casts we actually show. -- event can find its plate in O(1) and only cache casts we actually show.
local plateByGuid = {} local plateByGuid = {}
-- One-shot C_Spell poll. Only used to seed a plate that spawns while its -- One-shot C_Spell poll. Builds the cast struct for a UNIT_SPELLCAST_* event
-- unit is already mid-cast (its SPELL_START_OTHER fired before the plate -- (the payload carries no timing) and seeds a plate that spawns while its unit
-- existed). Never called per frame. -- is already mid-cast (its START fired before the plate existed). Picks cast
-- vs channel itself. Never called per frame.
local function PollCastInfo(unit) local function PollCastInfo(unit)
if not unit then return nil end if not unit then return nil end
local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit) local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit)
@@ -477,8 +478,10 @@ nameplates:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
nameplates:RegisterEvent("UNIT_AURA") nameplates:RegisterEvent("UNIT_AURA")
nameplates:RegisterEvent("UNIT_FLAGS") nameplates:RegisterEvent("UNIT_FLAGS")
nameplates:RegisterEvent("UPDATE_MOUSEOVER_UNIT") nameplates:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
nameplates:RegisterEvent("SPELL_START_OTHER") nameplates:RegisterEvent("UNIT_SPELLCAST_START")
nameplates:RegisterEvent("SPELL_FAILED_OTHER") nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
nameplates:RegisterEvent("UNIT_SPELLCAST_STOP")
nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
nameplates:SetScript("OnEvent", function() nameplates:SetScript("OnEvent", function()
-- Stop event handling during logout to prevent crash 132 -- Stop event handling during logout to prevent crash 132
@@ -562,9 +565,9 @@ nameplates:RegisterEvent("SPELL_FAILED_OTHER")
plate.nameplate.unit = arg1 plate.nameplate.unit = arg1
if guid then if guid then
plateByGuid[guid] = plate.nameplate plateByGuid[guid] = plate.nameplate
-- Seed: the unit may already be mid-cast (its SPELL_START_OTHER fired -- Seed: the unit may already be mid-cast (its UNIT_SPELLCAST_START
-- before this plate existed). One poll here catches that; ongoing -- fired before this plate existed). One poll here catches that;
-- casts arrive via the event. -- ongoing casts arrive via the event.
castState[guid] = PollCastInfo(arg1) castState[guid] = PollCastInfo(arg1)
end end
nameplates.OnShow(plate) nameplates.OnShow(plate)
@@ -609,39 +612,32 @@ nameplates:RegisterEvent("SPELL_FAILED_OTHER")
if pn then pn.eventcache = true end if pn then pn.eventcache = true end
end end
elseif event == "SPELL_START_OTHER" then elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
-- nampower: arg2=spellId, arg3=casterGuid, arg6=castTime(ms), -- ClassicAPI fires UNIT_SPELLCAST_* per unit token, including the caster's
-- arg7=channel duration(ms, 0 if not a channel), arg8=spellType -- "nameplateN". The payload has no timing, so poll it (PollCastInfo picks
-- (1 = channel). Cache the cast only for a unit we have a plate for, so -- cast vs channel) and cache -- only for a unit we have a plate for, so
-- the table stays bounded to on-screen casters. -- the table stays bounded to on-screen casters.
local casterGuid = arg3 if arg1 and strfind(arg1, "^nameplate") then
local plate = casterGuid and plateByGuid[casterGuid] local guid = UnitGUID(arg1)
if plate then local plate = guid and plateByGuid[guid]
local isChannel = arg8 == 1 if plate then
local durationMs = isChannel and arg7 or arg6 castState[guid] = PollCastInfo(arg1)
if durationMs and durationMs > 0 then if castState[guid] then
local spellId = arg2 plate.castUpdate = true -- bypass the throttle so the bar shows now
local now = GetTime() end
castState[casterGuid] = {
spellName = C_Spell.GetSpellName(spellId),
spellID = spellId,
icon = C_Spell.GetSpellTexture(spellId),
startTime = now,
endTime = now + durationMs / 1000,
duration = durationMs / 1000,
isChannel = isChannel,
}
plate.castUpdate = true -- bypass the throttle so the bar shows now
end end
end end
elseif event == "SPELL_FAILED_OTHER" then elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then
-- nampower: arg1=casterGuid, arg2=spellId. Clear on interrupt/failure. -- Cast/channel ended (natural, interrupted, or cancelled -- the poll fires
local casterGuid = arg1 -- STOP for all three). Clear the cached cast and refresh its plate.
if casterGuid and castState[casterGuid] then if arg1 and strfind(arg1, "^nameplate") then
castState[casterGuid] = nil local guid = UnitGUID(arg1)
local plate = plateByGuid[casterGuid] if guid and castState[guid] then
if plate then plate.castUpdate = true end castState[guid] = nil
local plate = plateByGuid[guid]
if plate then plate.castUpdate = true end
end
end end
elseif event == "UNIT_AURA" then elseif event == "UNIT_AURA" then