mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 15:46:56 +00:00
f497c71523
Eight commits off classicapi_next. ClassicAPI's RegisterUnitEvent registers for an event but only delivers it when arg1 is one of the given units, so a handler for one unit stops waking for every other one in the world. The 26 registrations whose unit set is fixed at registration time now name it. The rule throughout is register the superset and keep the handler's own check -- the filter narrows what arrives, it does not decide what to act on. Guards that look unreachable stay put: the filter applies only when arg1 is a string, so an event that fires with a number or no argument is delivered as if plainly registered. Frames whose unit changes at runtime own their subscriptions instead of sorting events out per event. unitframes points them at the unitstr UpdateVisibility already computes -- replacing a string concat, and on a miss a second concat plus a UnitGUID call, for every frame on every unit event in the world -- and a frame that is not in use drops its unit events entirely. nameplates registers per plate against the plate's own token, which is also the only workable shape: slots have no cap, so any nameplate1..N list would have been a guess that fails in exactly the crowded scenes where plates matter. marktracking names mark1 through mark8. A registration keeps its kind, so none of these can be plain-registered first. Both teardown paths PLAYER_LOGOUT guards -- the crash 132 -- now cover the per-frame subscriptions: plates tear down rather than dispatching through logout, and a unit frame takes itself off the visibility scan so it cannot re-register what it just dropped. marktracking also drops its once-a-second full rebuild, which ran for the whole session whether or not a marker existed anywhere. The ticker is created and cancelled with group membership. It is deliberately not keyed on a mark being visible -- a marker on an out-of-range unit shows no row, and that is the case the poll exists to catch. nameplates gates the per-plate update against the floor across all four throttle categories before classifying it, instead of running a GetAlpha, a castbar IsShown, a cast lookup and up to two libthrottle:Get resolutions on plates throttled to 10fps that were going to return anyway. Nothing that would have updated can be turned away by a floor. The four throttles resolve in CacheConfig, where config changes already land. energytick sweeps the clock the server actually runs. There is one regen timer for every power, re-armed every 2s by Player::RegenerateAll and never touched by casting; the five-second rule changes what a tick pays, not when it lands. The sweep is a free-running phase lock on that clock, so Illumination refunds, potions and a Mana Spring totem on its own phase no longer snap the spark mid-cycle, and an 80ms band keeps a correct tick from hitching it at the wrap. The FSR window shades rather than predicting a share of spirit the client cannot compute -- the Casting Regen item ladder is equip auras absent from the buff list. The energy period is summed from SPELL_AURA_MOD_ENERGY_REGEN_TIME across the spellbook and buffs, so Blade Rush is found without GetTalentInfo(2, 16), an ordinal that does not fail when the tree changes but reads another talent's rank. macrotweak is gone -- ClassicAPI 1.15 covers it -- with its config entry, its GUI block, its translations in all eight locales, and actionbar's ButtonMacroScan, the #showtooltip scanner that fed it.
262 lines
9.4 KiB
Lua
262 lines
9.4 KiB
Lua
-- One server clock drives every power: Player::RegenerateAll fires every
|
|
-- REGEN_TIME_FULL (2s), re-arms with `+=`, and is never reset by casting. The
|
|
-- five-second rule (SetLastManaUse on any mana-costing cast) changes what a tick
|
|
-- pays, never when it lands; mp5 and the player's MOD_MANA_REGEN_INTERRUPT share
|
|
-- still come in. That share can't be computed here -- item sources are equip
|
|
-- auras absent from the buff list and m_modManaRegenInterrupt is never sent --
|
|
-- so the spark shows it instead: dim through the window until a tick lands.
|
|
--
|
|
-- The sweep free-runs on that clock and phase-locks to observed gains. A gain
|
|
-- mid-sweep (Illumination, Judgement of Wisdom, potions, a Mana Spring totem on
|
|
-- its own phase) is not the tick and never moves it.
|
|
|
|
local FIVE_SECOND_RULE = 5
|
|
|
|
-- gains farther than this from the predicted boundary are not the tick
|
|
local TICK_TOLERANCE = .25
|
|
|
|
-- arrival jitter; a tick inside this band confirms the sweep rather than
|
|
-- re-anchoring it, or the spark hitches at every wrap
|
|
local TICK_JITTER = .08
|
|
|
|
-- Player::RegenerateAll:
|
|
-- mod = GetTotalAuraModifier(SPELL_AURA_MOD_ENERGY_REGEN_TIME)
|
|
-- if mod > 0 then mod = mod * agility / 10 end
|
|
-- m_regenTimer += max(1, REGEN_TIME_FULL - mod) -- milliseconds
|
|
local REGEN_TIME_FULL = 2
|
|
local ENERGY_REGEN_TIME_AURA = 217 -- SPELL_AURA_MOD_ENERGY_REGEN_TIME
|
|
|
|
-- fixed magnitude is basePoints + baseDice (stored 11 -> 12); a die above 1 is
|
|
-- a roll the client can't know, so it counts as nothing rather than a guess
|
|
local amountCache = {}
|
|
local function auraAmount(spellID)
|
|
local amount = amountCache[spellID]
|
|
if amount then return amount end
|
|
amount = 0
|
|
local effects = C_Spell.GetSpellEffectInfo(spellID) -- nil for an id with no record
|
|
if effects then
|
|
for i = 1, 3 do
|
|
local fx = effects[i]
|
|
if fx.auraName == ENERGY_REGEN_TIME_AURA and fx.dieSides <= 1 then
|
|
amount = fx.basePoints + fx.baseDice
|
|
break
|
|
end
|
|
end
|
|
end
|
|
amountCache[spellID] = amount
|
|
return amount
|
|
end
|
|
|
|
-- A passive is in effect exactly while known (current rank only, never in the
|
|
-- buff list); anything castable or cast on us counts only while it is up.
|
|
local function getEnergyRegenTimeMod()
|
|
local sum = 0
|
|
for _, spellID in ipairs(C_SpellBook.GetPlayerSpellsByAura(ENERGY_REGEN_TIME_AURA)) do
|
|
if C_Spell.IsSpellPassive(spellID) then
|
|
sum = sum + auraAmount(spellID)
|
|
end
|
|
end
|
|
for i = 1, 32 do
|
|
local spellID = select(10, C_UnitAuras.UnitAura("player", i, "HELPFUL"))
|
|
if not spellID then break end
|
|
sum = sum + auraAmount(spellID)
|
|
end
|
|
return sum
|
|
end
|
|
|
|
-- cleared on SPELLS_CHANGED (passives) and PLAYER_AURAS_CHANGED (buffs), and
|
|
-- recomputed by the next tick that asks. Agility stays live: it's one call.
|
|
local energyRegenTimeMod
|
|
|
|
local function getAdjustedTickTimer()
|
|
if not energyRegenTimeMod then
|
|
energyRegenTimeMod = getEnergyRegenTimeMod()
|
|
end
|
|
if energyRegenTimeMod == 0 then return REGEN_TIME_FULL end
|
|
|
|
-- ms on the server, seconds here; the 1ms floor is the server's and this is a divisor
|
|
local reduction = energyRegenTimeMod * UnitStat("player", 2) / 10000
|
|
return math.max(0.001, REGEN_TIME_FULL - reduction)
|
|
end
|
|
|
|
pfUI:RegisterModule("energytick", function()
|
|
if not pfUI.uf or not pfUI.uf.player then
|
|
return
|
|
end
|
|
|
|
-- inside the module body on purpose: C is on pfUI.env, not _G
|
|
local function getBarWidth()
|
|
return C.unitframes.player.pwidth ~= "-1" and C.unitframes.player.pwidth or C.unitframes.player.width
|
|
end
|
|
|
|
-- was this gain the regen tick? if so, re-anchor the sweep on it
|
|
local function lockTick(frame)
|
|
local now, period = GetTime(), getAdjustedTickTimer()
|
|
|
|
if frame.start then
|
|
-- signed distance to the nearest predicted boundary
|
|
local err = mod(now - frame.start, period)
|
|
if err > period / 2 then err = err - period end
|
|
|
|
if math.abs(err) <= TICK_TOLERANCE then
|
|
-- correct only what lies beyond normal jitter
|
|
if err > TICK_JITTER then
|
|
frame.start = frame.start + (err - TICK_JITTER)
|
|
elseif err < -TICK_JITTER then
|
|
frame.start = frame.start + (err + TICK_JITTER)
|
|
end
|
|
frame.max, frame.rejected = period, nil
|
|
return true
|
|
end
|
|
|
|
-- two rejected gains one period apart are the real clock: relock to it
|
|
local periodic = frame.rejected and math.abs(now - frame.rejected - period) <= TICK_TOLERANCE
|
|
if not periodic then
|
|
frame.rejected = now
|
|
return false
|
|
end
|
|
end
|
|
|
|
frame.start, frame.max, frame.rejected = now, period, nil
|
|
return true
|
|
end
|
|
|
|
local energytick = CreateFrame("Frame", nil, pfUI.uf.player.power.bar)
|
|
energytick:SetAllPoints(pfUI.uf.player.power.bar)
|
|
energytick:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
energytick:RegisterUnitEvent("UNIT_DISPLAYPOWER", "player")
|
|
energytick:RegisterUnitEvent("UNIT_ENERGY", "player")
|
|
energytick:RegisterUnitEvent("UNIT_MANA", "player")
|
|
energytick:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
|
|
energytick:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player")
|
|
energytick:RegisterEvent("SPELLS_CHANGED")
|
|
energytick:RegisterEvent("PLAYER_AURAS_CHANGED")
|
|
|
|
energytick:SetScript("OnEvent", function()
|
|
if UnitPowerType("player") == Enum.PowerType.Mana and C.unitframes.player.manatick == "1" then
|
|
this.mode = "MANA"
|
|
this:Show()
|
|
elseif UnitPowerType("player") == Enum.PowerType.Energy and C.unitframes.player.energy == "1" then
|
|
this.mode = "ENERGY"
|
|
this:Show()
|
|
else
|
|
this:Hide()
|
|
end
|
|
|
|
if event == "SPELLS_CHANGED" or event == "PLAYER_AURAS_CHANGED" then
|
|
energyRegenTimeMod = nil
|
|
return
|
|
end
|
|
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
this.lastPower = UnitPower("player")
|
|
end
|
|
|
|
-- the rule arms on the cast (Spell::TakePower: mana powerType, cost > 0),
|
|
-- not on a mana drop -- Mana Burn lowers mana without arming it
|
|
if event == "UNIT_SPELLCAST_SUCCEEDED" and arg1 == "player" then
|
|
local cost = C_Spell.GetSpellPowerCost(arg3)
|
|
cost = cost and cost[1]
|
|
if cost and cost.type == Enum.PowerType.Mana and cost.cost > 0 then
|
|
this.fsrSpell, this.fsrEnd = arg3, GetTime() + FIVE_SECOND_RULE
|
|
this.fsrGain = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Unit::Update won't expire the rule while the spending spell still channels
|
|
if event == "UNIT_SPELLCAST_CHANNEL_STOP" and arg1 == "player" then
|
|
if this.fsrSpell and this.fsrSpell == arg3 then
|
|
this.fsrEnd, this.fsrGain = GetTime() + FIVE_SECOND_RULE, nil
|
|
end
|
|
return
|
|
end
|
|
|
|
if (event == "UNIT_MANA" or event == "UNIT_ENERGY") and arg1 == "player" then
|
|
local power = UnitPower("player")
|
|
local diff = this.lastPower and (power - this.lastPower) or 0
|
|
this.lastPower = power
|
|
|
|
-- only a gain can be the tick; a spend never touches the phase
|
|
if diff > 0 and lockTick(this) then
|
|
-- a tick inside the window proves regen continues through it
|
|
if this.fsrEnd and this.fsrEnd > GetTime() then
|
|
this.fsrGain = true
|
|
end
|
|
end
|
|
|
|
-- phase is kept while hidden; OnUpdate catches up by whole periods
|
|
if this.mode == "MANA" and power >= UnitPowerMax("player") then
|
|
this:Hide()
|
|
end
|
|
end
|
|
end)
|
|
|
|
energytick:SetScript("OnUpdate", function()
|
|
-- Throttle for performance
|
|
if (this.tick or 0) > GetTime() then
|
|
return
|
|
end
|
|
this.tick = GetTime() + 0.020 -- ~50 FPS
|
|
|
|
-- five-second rule drains to nothing
|
|
local remaining = this.fsrEnd and (this.fsrEnd - GetTime()) or 0
|
|
if this.mode == "MANA" and remaining > 0 then
|
|
this.fsrbar:SetWidth(getBarWidth() * remaining / FIVE_SECOND_RULE)
|
|
this.fsrbar:Show()
|
|
else
|
|
this.fsrSpell, this.fsrEnd, this.fsrGain = nil, nil, nil
|
|
this.fsrbar:Hide()
|
|
end
|
|
|
|
if not this.start then
|
|
this.spark:SetAlpha(0)
|
|
return
|
|
end
|
|
|
|
if this.mode == "MANA" and UnitPower("player") >= UnitPowerMax("player") then
|
|
this.spark:SetAlpha(0)
|
|
return
|
|
end
|
|
|
|
this.current = GetTime() - this.start
|
|
|
|
-- roll over by whole periods, not from now: restarting bakes frame
|
|
-- overshoot into the phase as drift the lock then has to chase
|
|
if this.current > this.max then
|
|
this.start = this.start + this.max * math.floor(this.current / this.max)
|
|
this.max = getAdjustedTickTimer()
|
|
this.current = GetTime() - this.start
|
|
end
|
|
|
|
-- dim while the rule is up and nothing has ticked inside it yet
|
|
this.spark:SetAlpha((remaining > 0 and not this.fsrGain) and .4 or 1)
|
|
|
|
if not C.unitframes.player.pheight then
|
|
return
|
|
end
|
|
|
|
local pos = getBarWidth() * (this.current / this.max)
|
|
this.spark:SetPoint("LEFT", pos - ((C.unitframes.player.pheight + 5) / 2), 0)
|
|
end)
|
|
|
|
energytick.fsrbar = energytick:CreateTexture(nil, "ARTWORK")
|
|
energytick.fsrbar:SetTexture(1, 1, 1, .15)
|
|
energytick.fsrbar:SetPoint("TOPLEFT", 0, 0)
|
|
energytick.fsrbar:SetPoint("BOTTOMLEFT", 0, 0)
|
|
energytick.fsrbar:Hide()
|
|
|
|
energytick.spark = energytick:CreateTexture(nil, "OVERLAY")
|
|
energytick.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
|
|
energytick.spark:SetHeight(C.unitframes.player.pheight + 15)
|
|
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
|
|
energytick.spark:SetBlendMode("ADD")
|
|
|
|
local hookUpdateConfig = pfUI.uf.player.UpdateConfig
|
|
function pfUI.uf.player.UpdateConfig()
|
|
energytick.spark:SetHeight(C.unitframes.player.pheight + 15)
|
|
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
|
|
hookUpdateConfig(pfUI.uf.player)
|
|
end
|
|
end)
|