Commit Graph

4071 Commits

Author SHA1 Message Date
Brues cc8b8b476b Removed macrotweak 2026-09-09 23:05:10 -05:00
Brues bd2b6c3105 no need to load macrotweak if ClassicAPI 1.15 is found 2026-09-09 21:48:13 -05:00
Brues 5bede33f00 energytick: sweep the real regen clock, and stop it stuttering
The mana spark modelled a tick clock that does not exist. Spending mana
restarted the sweep with a five-second span, then it cycled at two seconds
until the next spend -- so every cast reset the phase, and the spark drew a
period the server never runs. There is one regen timer for every power,
REGEN_TIME_FULL (2s), re-armed with `+=` in Player::RegenerateAll and never
touched by casting. The five-second rule is a separate countdown that
SetLastManaUse arms on any mana-costing cast; it changes what a tick pays,
never when ticks land. Inside the window the tick still fires and still pays
mp5 plus whatever share of spirit the player keeps.

That share is common and not computable here. The amounts are readable now
(C_Spell.GetSpellEffectInfo), and the talent, racial and buff sources could be
enumerated -- but the Casting Regen item ladder is equip auras that never
appear in the buff list, and m_modManaRegenInterrupt is never sent. A floor
that is wrong whenever the player wears such an item is worse than showing
what happens: the spark starts an FSR window dim and goes solid on the first
tick that lands inside it.

The sweep is a free-running phase lock on that one clock. Every positive mana
delta used to re-anchor it, and most are not the tick -- Illumination refunds,
Judgement of Wisdom, potions, a Mana Spring totem on its own 2s clock at
another phase -- so the spark snapped back to the edge mid-sweep several times
a cycle. A gain re-anchors only if it lands within 250ms of the predicted
boundary. Two rejected gains exactly one period apart are the real clock and
move the lock, which is how a wrong first lock heals. Rollover advances by
whole periods from the anchor instead of restarting from now, so frame timing
no longer accumulates into drift the lock then has to chase.

Even a correct tick used to hitch the spark: re-anchoring to arrival time
moved it by the jitter, 20-80ms, right at the wrap. Inside an 80ms band the
tick now confirms the sweep and leaves the anchor alone; only the excess is
pulled in. With no self-drift left there is nothing systematic in that band.

The rule is armed from UNIT_SPELLCAST_SUCCEEDED filtered through
C_Spell.GetSpellPowerCost, mirroring Spell::TakePower's condition, rather than
from a mana decrease -- Mana Burn lowers mana without arming the rule.
UNIT_SPELLCAST_CHANNEL_STOP re-arms it, since Unit::Update will not expire it
while the spending spell is still channeling.

The energy period no longer names a spell. Player::RegenerateAll sums
SPELL_AURA_MOD_ENERGY_REGEN_TIME and takes amount * agility / 10 off the timer
in milliseconds; everything in that sum is readable here. The player's own
spells carrying aura 217 come from C_SpellBook.GetPlayerSpellsByAura, passives
count while known (current rank only), anything else while it is up in the
buff list, and each amount is basePoints + baseDice from the effect data. So
Blade Rush is found without GetTalentInfo(2, 16) -- an ordinal into the talent
grid that does not fail when the tree changes but reads another talent's rank
(index 16 is Vigor one tab over) -- and a retune or a second source needs no
edit. The sum is cached and dropped on SPELLS_CHANGED and
PLAYER_AURAS_CHANGED, the two events its two halves move on, so a tick costs
no spellbook walk; agility stays live, it is one call.

The CHAT_MSG_SPELL_SELF_BUFF "You gain ... Energy from" filter is gone. The
phase lock rejects Relentless Strikes and Thistle Tea the same way it rejects
a paladin's refunds, it was English-only, and its ignore-the-next-gain logic
could eat a real tick if events reordered.
2026-09-09 13:24:17 -05:00
Brues a286843b18 nameplates: gate the per-plate update before classifying it
The central loop calls the per-plate update for every visible plate ~100 times
a second, and the plate's own throttle sat at the bottom of the function. Above
it ran the whole classification: a GetAlpha call, a castbar IsShown, a cast
lookup, and one or two libthrottle:Get resolutions -- all to work out which
throttle applied, on plates throttled to 10fps that were going to return
anyway. Forty plates makes that four thousand times a second.

Add a cheap gate first, against the floor across all four throttle categories.
Nothing that would have updated can be turned away by it -- a plate past its
real throttle is necessarily past the minimum -- and the category-specific gate
still runs after the classification, unchanged. Event flags are read before
both and bypass both, as they did before.

libthrottle:Get is not a lookup: it walks the saved variables, a defaults
fallback and a preset table, and can build a "<category>_custom" key. Resolve
the four categories in CacheConfig instead, where config changes already land,
and read them from cfg on the hot path. That covers the two calls in the
castbar branch as well.

The castbar hide moves below the cheap gate, which is safe rather than merely
tolerable: a plate showing a castbar sets isCastingNonTarget, which selects the
castbar throttle -- one of the four the floor is derived from -- so it always
clears the first gate and reaches the hide at its normal rate.
2026-09-09 13:24:17 -05:00
Brues 39840c10fd unitframes: let UpdateVisibility own the event subscriptions
Every unit frame registered fifteen unit events globally and then sorted them
out per event:

  arg1 == this.label .. this.id or (UnitGUID and arg1 == UnitGUID(this.label .. this.id))

A string concat -- and on a miss a second concat plus a UnitGUID call -- for
every frame, for every unit event fired by anything in the world. Forty-five
frames in a raid, on every health tick of every raider, to conclude "not mine".

UpdateVisibility is the single place a frame's label and id are ever assigned,
and it already pushed the result to SetAttribute("unit", ...). It now caches
the unitstr and points the subscriptions at it, so there is one hook rather
than a set of transitions to catch, and visibilityscan re-running it five
times a second makes it self-healing. A frame that is not in use drops its
unit events entirely.

EnableEvents no longer registers them: a registration keeps its kind, so
plain-registering first would make every later RegisterUnitEvent a no-op.

UNIT_PET and UNIT_HAPPINESS stay plain deliberately. Their branches key on the
frame's label, and UNIT_PET's arg1 is the pet's OWNER -- "player" for a "pet"
frame -- so filtering them by the frame's own unit would drop them.

The GUID alternative is gone. These events fire once per token that resolves
to the unit AND once with the raw GUID, so the token form always arrives and
the GUID clause was only ever a duplicate wake. It dates from b7a0912e, before
ClassicAPI's TokenObserver made synthetic tokens first-class event sources.

Two teardown paths needed help. UnregisterAllEvents drops the filters with the
registrations, so the cached unit has to be cleared or the next
UpdateVisibility believes they are still in place. And PLAYER_LOGOUT silences
the frame while visibilityscan keeps ticking, which would re-register
everything it just dropped -- the crash 132 that branch exists to prevent -- so
the frame now takes itself off the scan list.
2026-09-09 13:24:17 -05:00
Brues ab98bb7ab5 nameplates: let each plate watch its own unit
The central handler registered UNIT_AURA, UNIT_FLAGS and the four
UNIT_SPELLCAST_* globally, so every unit event in the world woke it for a
"^nameplate" prefix test, and each of the four branches that survived the test
then resolved the plate back out of arg1 -- UnitGUID + plateByGuid, or
GetNamePlateForUnit.

Register them per plate instead, on the plate's own frame against its own
token. The token is the subscription, so the event only reaches the plate it
concerns and `this` is already that plate: the aura branch is now one
assignment where it used to be a prefix test, an API call and a table lookup.

This is also what makes the approach viable at all. A central listener would
have had to name every token it might care about, and nameplate slots have no
cap -- Events.cpp keeps them in a vector that grows by push_back, with only a
"<80 even in AV-scale scenes" comment for sizing. Any nameplate1..N list would
have been a guess that fails silently in exactly the crowded scenes where
plates matter.

Lifecycle: NAME_PLATE_UNIT_ADDED points the registrations at the new token,
replacing the previous unit rather than stacking, since RegisterUnitEvent over
an already-filtered registration swaps the units. NAME_PLATE_UNIT_REMOVED
unregisters, which is required -- freed slots are reused, and a stale token
would feed the frame another unit's events.

PLAYER_LOGOUT now tears down the plates too. It silenced only the central
frame before; leaving forty plate frames dispatching through logout is the
crash 132 that branch exists to prevent.
2026-09-09 13:24:17 -05:00
Brues 73f4999004 marktracking: filter to the mark tokens and idle when solo
UNIT_HEALTH/UNIT_MAXHEALTH register through RegisterUnitEvent for mark1
through mark8, so the scanner stops waking for every unit in the world. Eight
tokens is past the four other clients accept; ClassicAPI takes as many as it
is given, so this file is fork-only.

That makes arg1 always one of eight known strings, so the handler looks the
row index up in a table built alongside markerTokens, instead of running
string.match + tonumber -- which allocated a capture and parsed it on every
health tick of every marked unit.

The fallback poll was the larger cost and the filtering did not touch it: a
full eight-row rebuild once a second for the whole session, whether or not a
marker existed anywhere. Raid markers are a group feature, so the scanner now
watches PARTY_MEMBERS_CHANGED / RAID_ROSTER_UPDATE and creates or cancels the
ticker with group membership -- outside a group no timer is queued at all,
rather than one waking every second to return early. It is deliberately not
keyed on a mark being visible: a marker set on a unit that is out of range
shows no row, and that is precisely the case the poll exists to catch.

The mark-token capability probe at the top goes too. ClassicAPI is a hard
dependency, so the tokens are always there to resolve.
2026-09-09 13:24:17 -05:00
Brues b7c3fe5333 Filter unit events with RegisterUnitEvent
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. Convert the 26 registrations whose unit set
is fixed and known at registration time.

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.
castbar is the case that matters -- the player's own casts only ever fire
arg1=="player", never "target"/"focus", so the target and focus bars must
register "player" too, and their UnitIsUnit test is what still rejects a
"player" event while you are targeting a mob.

Two that are not one-line swaps:

  actionbar's two unit events are keys in tables that also drive dispatch, so
  they cannot leave the tables -- and they cannot be plain-registered first,
  because a registration keeps its kind and RegisterUnitEvent over a plain
  one stays plain. They route through a small event_units map instead.

  swingtimer's UNIT_DIED carries a GUID, not a token. The filter is a plain
  case-insensitive string compare with no GUID resolution, so the player's
  own GUID -- fixed for the session -- filters it exactly.

Guards that are now unreachable stay put: the filter applies only when arg1
is a string, so an event that ever fires with a number or no argument is
delivered as if plainly registered, and the handler's own test is what still
rejects it.

Left alone: api/unitframes.lua (the frame's unit changes at runtime and the
handler also matches on GUID), nameplates' six nameplateN events, libdebuff
and libpredict's UNIT_HEALTH (genuinely any unit), and raid.lua's UNIT_PET
(any raid member can own the pet).
2026-09-09 13:24:17 -05:00
Brues 8e109a585a eqcompare: place the compare after the tooltip settles when Guda is loaded
Guda anchors its item tooltips with ANCHOR_NONE and its own SetPoint, and
the tooltip module then relocates every ANCHOR_NONE tooltip to its
configured spot. eqcompare picked left or right from GetLeft/GetRight
inside the Set* call -- against the position the tooltip was about to
leave -- so for an item in the left half of the screen the shopping
tooltips anchored to the right edge and rode the relocation off-screen
(vatichild/guda#41). Guda's own inventory block also widens the tooltip
after our hook has run.

With Guda present, resolve the link in the hook but hand the placement to
RunNextFrame, skipping it if the tooltip has hidden since. Guda lists
pfUI as a dependency, so it loads after us: gate on DoesAddOnExist and
flip the flag from ContinueOnAddOnLoaded. Without Guda the hook is
unchanged.
2026-09-09 13:23:53 -05:00
Brues 83f89be14a unitframes: compare indicator auras without case folding
The indicator scan lowercased the aura's name and icon on every aura of every
unit, and SetupBuffIndicators lowercased the other side to match. Both sides
come out of the same DBC records byte for byte, so the comparison already held
without any of it: the aura's name and icon are Spell.dbc's localized name and
SpellIcon.dbc's path (aura/Data.cpp fills one struct and emits it as either the
AuraData table or the positional UnitAura tuple), and the indicator record
reads those same two fields through C_Spell.GetSpellName / GetSpellTexture.

So this was two string allocations per aura per scan to reach a result it
already had -- and on Lua 5.0 that is an allocate-and-hash each time, since the
VM interns even when the string exists. RefreshUnit runs this for every unit
frame, which in a raid is 40 frames against up to 32 auras each.

Checked every rank of all 65 indicator spells in Spell.dbc: name and icon are
byte-identical across a spell's whole ladder, no case or spelling drift, so
nothing depended on the fold.

It also drops a crash path. aura/Data.cpp yields nil for the icon when a
spell's SpellIconID is 0, and icon:lower() would have errored on that; name was
guarded on the line above but icon never was. A raw compare is just false.

The equality is now load-bearing, so AddIndicator's comment says to feed it
spell ids and never a hand-written name or icon path -- a literal
"interface\icons\foo" in that table would silently match nothing.
2026-09-08 16:35:31 -05:00
Brues e59158764c unitframes: identify buff indicators by spell id, not icon path
Spell.dbc reuses icons freely across unrelated spells, so matching an aura on
its icon alone lit the indicator for the wrong buff. Blessing of Sanctuary
fired on Lightning Shield and Shadowguard, Blessing of Kings on Mage Armor and
Commanding Shout, and Totemic Power on the Blessed Sunfruit food buff -- the
last of which the code already carried a comment about. Fixes #55.

Each entry is now a spell id, resolved once into the aura's name plus its icon,
and a match needs both. Name alone is no better than icon alone: creature and
item auras reuse player spell names, so a mob's "Renew" or a trinket's
"Rejuvenation" would light a HoT indicator. Every rank of a spell carries the
same name and icon, so one id covers the whole rank ladder without listing it.

HOT_INDICATORS folds away with this. It existed to name-check exactly the three
HoTs whose icons were known to collide; that check is now what every entry
does, and the table's only remaining job -- naming a HoT's libpredict key -- is
an argument on the Renew, Regrowth and Rejuvenation entries.

Deriving the name from an id also picks up this client's renames for free. The
paladin blessings are "Hand of Freedom", "Hand of Protection" and "Greater
Blessing of Sacrifice" here, which hardcoded English names would have missed.

All 65 ids were checked against the client's Spell.dbc, SpellIcon.dbc and
SkillLineAbility.dbc: each resolves to the intended aura, with its full rank
ladder sitting on the icon the old list matched.

That check also turned up five entries matching no spell at all on this
client. ability_hunter_misdirection, spell_holy_prayerofmendingtga and
inv_misc_herb_felblossom are TBC leftovers and are gone.
spell_nature_giftofthewild was never right -- Gift of the Wild uses Mark of the
Wild's icon -- and is now covered by id 21849. ability_warrior_rallyingcry
hosts no Commanding Shout either, so that entry now points at the real spell
(45580) and works for the first time.

Unrelated to the above, this file's string.fn(s, ...) calls become s:fn(...).
2026-09-08 16:28:51 -05:00
Brues 863d99647d Draw the loot history and map overlays from real pools
Both modules had already grown a pool by hand. loothistory kept usedPlayers /
freePlayers tables with a recycle loop and a table.remove acquire; mapreveal
kept pfOverlays keyed by index, a pfOverlayMax high-water mark, and a
hide-the-tail loop. Replace both with ClassicAPI's Pools.lua backport, which
is what modules/tooltip.lua already uses for its buff icons.

The map explore icons change shape rather than just plumbing. Everything
constant -- size, scripts, mouse, frame level, the magnifying glass texture --
moves into the pool's creator instead of being re-set on every overlay on
every map update, and the acquire moves inside the visibility test. The old
loop built and configured an icon for every overlay in the zone and then hid
most of them again; with "mapexploration" off it built the whole set and hid
all of them. Now nothing is acquired for an icon that will not be shown.

The tile textures lose their pfRevealN names, since pools create anonymous
regions. They were already unique, so nothing was being clobbered -- this
only costs their labels in /fstack.
v9.0.25
2026-09-07 18:41:54 -05:00
Brues 06b3ae8d22 cooldown: stop shadowing the global time()
`local time = time()` bound the timestamp over the top of the time function
inside that branch, so any later call in the same scope would have indexed a
number. Nothing did, but the name is a trap. Call it currentTime.
2026-09-07 18:39:28 -05:00
Brues 879d7afe83 cooldown: throttle before the work, not after
The 0.1s gate sat below the hidden-cooldown check, so every frame, for every
ticking cooldown, the update built "<parent>Cooldown" twice and did two _G
lookups with it. Lua 5.0 interns on every concat -- it allocates and hashes
even when the string already exists -- so this was allocating garbage at the
frame rate times the number of live cooldowns. A profiler run had it at 24s
of accumulated CPU.

Move the gate to the top so a non-tick frame costs one GetTime() and a
compare. The name lookup goes away entirely: pfCreateCoolDown already has the
cooldown frame, so it stashes the reference instead. That is also more
correct than deriving it from the parent's name, which silently skipped the
check for any cooldown not named "<parent>Cooldown".

The text frame and its fontstring were both created with a fixed name, so
every one of the hundreds in a UI clobbered _G.pfCooldownFrame and left it
pointing at whichever was made last. Name them after the cooldown they render
for, falling back to a counter for anonymous ones.

One behavior change: the hidden-cooldown check is now throttled too, so text
can linger up to 100ms after its cooldown frame hides. It only refreshes at
0.1s anyway, and expiry still runs through the remaining < 0 branch.
2026-09-07 18:14:33 -05:00
Brues 4eab494a01 Name the installed ClassicAPI version in the outdated popup
"cannot run on this ClassicAPI" named no version, so the player had nothing
to compare against the requirement, and it overstated the failure: pfUI does
load on an old DLL, it just throws wherever it reaches for an API that isn't
there. Put the installed version in the headline and soften it to "will not
work correctly"; the detail line then only has to state the requirement.
v9.0.24
2026-09-07 17:55:56 -05:00
Brues bb8ce8fd63 apparently some addons can't figure out -1 for themselves 2026-09-05 07:19:58 -05:00
brues-code 75c4657ab9 ClassicAPI flavor TOCs, and drop the multi-client scaffolding (#53)
* nameplates: source totem icons from UnitCreatedBySpell

Read the totem's icon from the totem-drop spell (UnitCreatedBySpell +
GetSpellTexture) instead of the self-aura at index 1 plus a
UNIT_SPELLCAST_SUCCEEDED capture for active totems. The drop spell is a
broadcast descriptor field present for every summoned unit in range, so
it resolves immediately for passive and active totems alike and gives the
totem's own icon rather than the attack-spell proxy. Drops the
UNIT_SPELLCAST_SUCCEEDED registration and handler.

Re-read the spell each render and key the cached texture on the spell id
so an in-place totem swap (same unit, new drop spell -- no plate re-add)
refreshes the icon without needing the plate to leave and re-enter view.

* CAPI min bumped to 1.13.1

* Gracefully disable pfUI when ClassicAPI is missing

Add API_Check.lua as the first TOC entry. When the ClassicAPI DLL is
absent or below the minimum version it sets pfUI_disabled and stands up
an inert stub so the rest of the addon no-ops instead of flooding load
errors: modules and skins register their bodies into no-ops, and the
setfenv'd api/lib files run inside an environment where CreateFrame and
any missing global resolve to a null object -- so no real frames or live
handlers are created and missing API calls just return null. pfUI.lua
bails early on pfUI_disabled.

* auras: scan through GetAuraSlots instead of by-index loops

Every aura scan loop (unit frame buffs/debuffs, dispel indicators, buff and
custom indicators, player buff frame, buffwatch bars, tooltip buff row) now
enumerates a unit's auras once with C_UnitAuras.GetAuraSlots and reads each
aura by slot id via the positional C_UnitAuras.UnitAuraBySlot. The by-index
getters re-walk the aura array from slot 0 on every call, so a per-button
loop over them was quadratic in the aura count; one enumeration plus a
by-slot read per aura is linear.

pfUI.api.ScanAuraSlots(unit, filter, buf, max) wraps GetAuraSlots' fill-a-
table form (table as the 5th argument) so no vararg Lua frame is involved:
Lua 5.0 allocates an `arg` table for every vararg call, which showed up as
nameplate OnUpdate/OnEvent memory growth in the first cut of this change.

Single by-index reads in tooltip and click handlers are unchanged (one call
each, and SetUnitAura takes the same index).

Requires the ClassicAPI build that adds GetAuraSlots' fill form; on an older
DLL the 5th argument is ignored and ScanAuraSlots would read the first slot
id as the count.

* Show Faction/Race icons in chat

* Load pfUI through ClassicAPI's flavor TOCs

ClassicAPI redirects the read of pfUI\pfUI.toc to a flavored file whenever
the DLL is installed, so which TOC the client opens already answers whether
ClassicAPI is there. Split the manifest three ways and let that do the work:

  pfUI.toc             fallback, reached only when ClassicAPI is missing;
                       loads API_Check.lua and nothing else
  pfUI_ClassicAPI.toc  full addon, every non-Turtle client
  pfUI_Turtle.toc      full addon plus init\turtle.xml, on Turtle

The fallback TOC declares no SavedVariables. It used to, while API_Check.lua
reset pfUI_profiles to an empty table on the disabled path -- which truncated
the player's profiles on logout.

With the missing-DLL case handled by TOC selection, API_Check.lua drops the
null object stub that kept the other ~140 files quiet, along with
pfUI_disabled and the now unreachable early return in pfUI.lua. It keeps the
version gate, which still matters: the flavor redirect landed in ClassicAPI
v1.11.0, below the v1.13.1 pfUI needs, so an old DLL still gets served a
flavor TOC. pfUI.lua also loses a verbatim duplicate of the whole check.

Turtle-only files move to init\turtle.xml: modules\turtle-wow.lua (its
TURTLE_WOW_VERSION guard is now redundant) and the lft, turtle_shop,
barbershop, transmog and ebc skins. turtle-wow registers last instead of
75th of 84; the only ordering it relies on is pfUI.chat, registered 8th.

pfSellData moves to env\selldata.lua, listed only in pfUI_ClassicAPI.toc,
since turtle-wow.lua replaces the table wholesale on Turtle. env\tables.lua
keeps an empty declaration so sellvalue.lua has something to index when the
turtle-wow module is disabled.

The release workflow pinned PFUI_CLASSIC_API_LATEST in pfUI.lua, which has
not held that constant since it moved to API_Check.lua, so the pin was
silently doing nothing. It also switches to brues-code/packager@vCAPI, which
recognizes the _ClassicAPI and _Turtle suffixes and applies the TOC build
type filters to them.

* Split the vendor price tables into their own manifests

Turtle's pfSellData moves out of modules\turtle-wow.lua into
env\selldata_turtle.lua, matching env\selldata.lua for the stock list, and
each is pulled in by the manifest for its client: init\stock.xml from
pfUI_ClassicAPI.toc, init\turtle.xml from pfUI_Turtle.toc. Either way it
loads after init\env.xml and replaces the empty pfSellData declared there.

Turtle's prices used to be assigned inside the turtle-wow module body, which
put them on pfUI.env and skipped them entirely when that module was
disabled. At file scope they land on _G and apply either way.

* Drop the vanilla compat layer

compat\vanilla.lua named the handful of things that differed between clients
back when pfUI targeted several. Only one client remains, so every constant
had exactly one value. Inline each at its use site and delete the file,
init\compat.xml, and both TOC entries.

  COOLDOWN_FRAME_TYPE                    -> "Model"
  LOOT_BUTTON_FRAME_TYPE                 -> "LootButton"
  MINIMAP_TRACKING_FRAME                 -> _G.MiniMapTrackingFrame
  FRIENDS_NAME_LOCATION                  -> "ButtonTextNameLocation"
  EVENTS_MINIMAP_ZONE_UPDATE             -> the event list, in panel.lua
  MICRO_BUTTONS                          -> a local in panel.lua
  NAMEPLATE_OBJECTORDER                  -> a local in nameplates.lua
  ACTIONBAR_SECURE_TEMPLATE_BAR/_BUTTON  -> nil, so the argument goes away

NAMEPLATE_FRAMETYPE and PLAYER_BUFF_START_ID had no readers left.

RunMacroText moves to pfUI.lua. compat\vanilla.lua was setfenv'd into the
pfUI environment, so the function only ever existed on pfUI.env; at file
scope it lands on _G as a real export instead. Nothing in pfUI calls it, and
ClassicAPI neither defines nor looks for a RunMacroText global -- it does
the same throwaway edit box natively in src/macro/Execute.cpp and only
defers to a global RunMacro.

* bump CAPI min to 11303

* auras: uncap the self-debuff tooltip lookup

With selfdebuff on, the displayed debuff list is PLAYER-filtered while
GameTooltip:SetUnitAura indexes the unfiltered HARMFUL list, so both
handlers map one to the other by matching name + sourceGUID. That mapping
scanned slots 1..16 only.

The unfiltered harmful list is not capped at 16. Once a unit's 16 debuff
slots are full the server parks further debuffs in buff slots, and
C_UnitAuras classifies by the aura's polarity flag rather than its slot
range, so it reports those as harmful too -- verified live at 18 harmful on
a 20-aura target. Past the sixteenth the lookup found nothing and fell
through to the raw filtered index, opening the wrong tooltip or none.

Both now enumerate however many harmful auras the unit actually has, via
ScanAuraSlots, which also drops the per-index rescan the by-index accessor
was doing. Each handler gets its own slot buffer: OnEnter can fire while a
refresh is showing/hiding frames under the cursor, so sharing the refresh
buffer could clobber a scan mid-walk.

The nameplate module still collects at most 16 debuffs per plate. That one
is a display cap matching its 16 configured icon frames, not an aura-count
assumption, so it is left alone.

* bump CAPI min to 11304
2026-09-04 19:57:57 -05:00
Brues 5815b9e81e chat: let pfUI's chat colors own their alpha and apply live
Two problems with routing the native transparency slider into the pfUI panel.

RefreshBackgroundAlpha overwrote the alpha of C.chat.global.background on every
refresh, so anyone with custom colors enabled saw their configured opacity
revert on reload, tab switch and dock change. That value carries its own alpha,
is set by the shipped profiles and is shared with the meter skins, so the slider
must not own it. Skip the alpha mirror entirely when custom colors are on; the
slider still drives the panel on the default theme, which is what issue #48 was
actually about.

The colors were also only applied at module load, so the pickers needed a
/reload to show anything. Extract that into ApplyPanelColors and expose it as
pfUI.chat:UpdateConfig, which the gui resolves as U["chat"], so the three chat
color settings take effect on the spot. CreateBackdrop is re-run first to
restore the appearance theme, which is what lets toggling custom colors back
off return the panel to the global theme without a reload.
v9.0.23
2026-09-02 11:40:20 -05:00
Brues 60f953178f nameplates: validate unitstr before filling the per-unit cache
cache.player and cache.minion were filled from unitstr before the two guards
that validate it, so a stale identifier poisoned the cache -- and because the
fill is gated on `== nil`, the wrong answer was never recomputed. Players read
back as cache.player == false until the plate was hidden and shown again, which
only appeared to fix it because pool reuse tripped the name/guid wipe.

Two paths produce a stale unitstr: OnUpdate dispatches a targetUpdate to
OnDataChanged before it refreshes plate.istarget, and frameState.mouseoverGuid
is only updated on gaining mouseover, never on losing it.

Hoist the PLAYER_TARGET_CHANGED distrust and the UnitName mismatch check above
the cache fill so an unverified unitstr leaves the cache nil for the next tick
instead of locking in a wrong answer.
v9.0.22
2026-09-02 10:56:46 -05:00
Brues 1dc10964a2 luarc: use nested config form so diagnostics settings apply 2026-09-02 10:31:03 -05:00
Brues 0fc08e77c0 Create .pkgmeta 2026-09-02 10:26:28 -05:00
Brues 7f3795b45b Add .luarc.json for Lua 5.1 language server diagnostics 2026-09-02 10:23:48 -05:00
Brues 8eee766652 chat: apply Blizzard per-window transparency to docked backdrops
The native chat background transparency slider drives FCF_SetWindowAlpha,
which only touches the ChatFrame*Background textures pfUI hides on docked
frames, so the visible pfUI backdrop never responded to it.

Mirror the selected window's stored alpha (GetChatWindowInfo) onto the pfUI
backdrop and refresh it from RefreshChat, FCF_SetWindowAlpha and
FCF_SelectDockFrame so the native slider drives it live and persists across
/reload. Default the SetupPositions window alpha to 0.8 to keep the historical
look, and migrate existing installs whose windows still carry the old hard-0
alpha so their chat background is not suddenly transparent.

Fixes #48
2026-09-02 01:05:47 -05:00
Brues 2b5288a688 nameplates: don't run GetUnitInfo for minions 2026-08-31 16:43:38 -05:00
Brues daba4e202e Default saved variables with 'or {}' instead of overwriting
Seed the SavedVariables globals only when absent (X = X or {}) rather
than unconditionally assigning {}. Behaviorally identical under the
current load order -- a returning character's data is restored over these
between file load and ADDON_LOADED -- but expresses intent as a default
and won't clobber data if that timing ever changed.
v9.0.21
2026-08-31 12:04:00 -05:00
Brues 98c7751416 Utilize CAPI Tracking functions
commit 8ea5c81789
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date:   Mon Aug 24 23:40:01 2026 -0500

    bump CAPI min to 1.12.4

commit 39a46563a1
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date:   Mon Aug 24 23:35:49 2026 -0500

    tracking: use native ClassicAPI tracking API

    Replace the hardcoded knownTrackingSpells class table and the
    icon-substring spellbook scan with ClassicAPI's native tracking
    functions:

    - RefreshSpells now enumerates GetNumTrackingTypes() / GetTrackingInfo(),
      which the DLL derives from the spellbook by tracking-aura effect. This
      drops the per-class spell/icon table and picks up server-custom
      trackers (e.g. Turtle's Find Trees) automatically.
    - Selecting a tracker in the menu uses SetTracking(index) instead of
      CastSpell(slot, BOOKTYPE_SPELL).
    - Keep the Druid Cat Form gate on Track Humanoids (5225) and the
      existing invalidSpells filter. Falls back to no entries if the tracking
      API is absent.

    Active icon (GetTrackingTexture), cancel (CancelTrackingBuff), and the
    hover tooltip (GameTooltip:SetTrackingSpell) were already native and are
    unchanged.

Fixes #42
2026-08-30 17:19:17 -05:00
Brues 8796fab6d8 libpredict: predict incoming heals for built-in click casting (#44)
Built-in click casting drives the cast through secure frame attributes,
bypassing the CastSpell/CastSpellByName/UseAction hooks that populate
spell_queue -- so the heal-prediction lookup in the SPELL_START_SELF
handler missed and no incoming heal showed.

Populate spell_queue from the cast's own rank-specific spellId (via
C_Spell.GetSpellName/GetSpellSubtext) right before the cache lookup, so
prediction works regardless of how the cast was initiated. Deriving the
rank from the actual spellId also fixes stale-rank lookups when a prior
keybind cast left a different rank of the same spell queued.
2026-08-24 21:37:41 -05:00
brues-code 7ca0e40d8b CAPI needs 1.12.1 for UnitAura changes 2026-08-24 12:05:18 -05:00
brues-code 2f93b246a1 Accidentally bumped ClassicAPI to the wrong version 2026-08-24 11:48:54 -05:00
Brues c647698f8f bump CAPI min 2026-08-24 11:07:38 -05:00
Brues 48026008b6 buffwatch: use official dispel-type colours for debuff bars
The debuff-bar auto colour read the vanilla DebuffTypeColor global,
diverging from unitframes/buff which use C_UnitAuras.GetAuraDispelTypeColor
(the BC GlobalColor.dbc palette ClassicAPI ships). Switch buffwatch to the
same accessor so all aura displays share one palette; nil/unknown types
fall back to DEBUFF_TYPE_NONE instead of the old ad-hoc (1, .2, .2).
2026-08-24 11:07:38 -05:00
Brues c1d96bae58 buff/buffwatch: positional aura reads in hot refresh loops
buff.lua RefreshBuffButton runs 50x per PLAYER_AURAS_CHANGED and
buffwatch.lua GetBuffData runs 32x per RefreshBuffBarFrame; both now use
the zero-allocation positional C_UnitAuras.UnitAura instead of the
table-allocating GetAuraDataByIndex.

buffwatch also threads the aura's dispelType out of GetBuffData onto the
frame.buffs row, so the debuff-bar colour reads it from the row instead
of re-fetching the aura with GetDebuffDataByIndex on every name change.

Cold single-lookups (hover/click tooltips) keep the readable table API.
2026-08-24 11:07:38 -05:00
Brues 1d44c5ec6f unitframes: scan debuff slots once for dispel indicators
The dispellable-indicator block re-scanned all 16 harmful slots for
every dispellable type (O(types x 16) UnitDebuff calls per refresh).
Scan the 16 slots once into a reusable set, then have the per-type loop
check the set. Zero-allocation (the set is cleared and reused within the
single RefreshUnit call).
2026-08-24 11:07:38 -05:00
Brues 8f88dfd852 Use positional C_UnitAuras accessors in hot aura scans
Swap the per-frame aura-refresh loops from table-allocating
GetAuraDataByIndex/GetBuffDataByIndex/GetDebuffDataByIndex/GetUnitAuras
to the zero-allocation positional C_UnitAuras.UnitAura/UnitBuff/UnitDebuff.

unitframes: buff/debuff icon loops, the dispellable-indicator 16-slot
scan, the two GetUnitAuras indicator passes, and the custom-debuff scan.
nameplates: the per-plate debuff scan now writes positional results
straight into the reusable display buffer, dropping the per-aura tables
and the result array.

Filters change from HARMFUL|PLAYER/HARMFUL to PLAYER/nil since UnitBuff/
UnitDebuff lock the range and still honor the PLAYER predicate. Cold
single-lookups (hover/click, tooltip, totem icon, libdebuff) keep the
readable table API.
2026-08-24 11:07:37 -05:00
Brues a57214aeb0 Bump ClassicAPI minimum version 2026-08-24 11:07:37 -05:00
Brues 9bde843e33 utlize inline icons and CreateGoldString in repair module 2026-08-24 11:07:37 -05:00
Brues ad13e6ace3 Utilize SetFormattedText
This change updates several text setters to use `SetFormattedText` instead of passing `string.format(...)` into `SetText`. It also switches a few regex captures from `string.find` to `string.match` for cleaner capture handling, and replaces explicit width/height setters with `SetSize` in the first-run dialog for consistency.
2026-08-24 11:07:37 -05:00
Brues 82c74c2db5 Skip declaring pfSellData on turtle client
This giant table is just going to be immediately replaced so there's no point in declaring it.
2026-08-21 14:59:19 -05:00
roby-brok a3cfd711fe nameplates: read friendly name colours from the class table
The class-colour write sourced r, g, b from the health bar rather than
from PFUI_CLASS_COLORS[class], which the condition tested for but never
read. So "class colours on friendly names" only produced a class colour
when friendclassc - a bar option - happened to be on as well; otherwise
the name took the generic friendly colour. In every case it also
inherited the tapped-grey and barcombatstate overrides meant for the bar,
neither of which belongs on the name.

The name now reads the class table directly.

The discarded "and PFUI_CLASS_COLORS[class]" term was dead weight: the
table carries an __index metamethod returning a grey ColorMixin for any
missing key, so it was always truthy and merely allocated a throwaway
table per evaluation. "and class" is the real guard. One behaviour change
falls out - a friendly player of an unrecognised class now takes that
grey fallback rather than the bar colour, which is what the option means.

(cherry picked from commit f880dc3528db2ba0d0936b00a4dff8deec48e6d2)
(cherry picked from commit 111d70004d4fb172f168aa48a4cb6177a7f0d393)
v9.0.20
2026-08-21 13:31:07 -05:00
roby-brok a7ad1f073f nameplates: give the name colour a single owner
cache.namecolor was written by two writers holding unrelated quantities:
the computed health-bar colour in OnDataChanged, which class-colours
friendly player names when friendclassnamec is on, and Blizzard's own
name FontString in the OnUpdate sync. Sharing one key let either suppress
the other, so the option silently lost - names came out white with
namefightcolor off, or the plain reaction colour with it on.

It also forced a full data pass every throttled tick, because the sync
writer sets update = true whenever it writes and the two kept flipping
the key. That defeats the half-second tick gate entirely.

Compounded by nameplate.cache surviving pool reuse: OnShow re-runs the
data pass but never clears the table, which is built once at plate
creation, so a recycled plate could inherit the previous unit's name
colour.

Ownership is now explicit through cache.ownname, each writer keeps its
own key, and both are invalidated when ownership flips.

(cherry picked from commit 0a4b3a7d6ae523cf70584adbb694a8740024b7cd)
(cherry picked from commit 0df9bb9fa907349182fb03217cab6aa1777aa7d9)
2026-08-21 13:31:07 -05:00
roby-brok fe5b3b135b nameplates: use true difficulty colours for level text
The level string was brightened by +0.3 on all three channels before
display, in the levelFromDB path and again in the OnUpdate colour sync.
A flat offset desaturates toward white and costs the high tiers most,
because their green/blue channels start near zero: verydifficult
1.00/0.50/0.25 becomes 1.00/0.80/0.55 and reads as yellow, impossible
1.00/0.10/0.10 becomes 1.00/0.40/0.40 and reads as orange. Orange and
yellow ended up 0.2 apart on a single channel, so a mob 3-4 levels above
the player showed yellow and a skull-range mob showed orange.

Both offsets dropped. Ownership of the colour is now explicit through
cache.levelfromdb so the two writers no longer race: the sync block
stands down on ?? plates, and the DB path clears cache.levelcolor so the
sync re-asserts cleanly once the level resolves. That also fixes a stale
cache - nameplate.cache is built once at plate creation and survives pool
reuse, so a recycled plate could keep the previous unit's colour.

Deleting the sync block instead would be wrong: it is the only thing that
colours the level on non-?? plates, and plate.level is created with no
colour at all.

Reported by Iden via Discord.

(cherry picked from commit eaa9beac51953d2a959c67d5ff5bc50273a919c3)
(cherry picked from commit 3fdb3b23136b260d111509e24e6d7b0d0b9b1b33)
2026-08-21 13:31:07 -05:00
Brues 9c29f0c7d4 Add handlesHookScript capability flag
Introduce pfUI.handlesHookScript boolean in pfUI.lua to signal that this fork's actionbar buttons correctly support the modern HookScript widget method. This prevents ClassicAPI's AddOnCompat shim from shadowing HookScript during actionbar load. The flag documents behavior (not identity): forks that maintain HookScript-correct actionbar code should keep the flag; forks that do not should clear it to opt into the safe compatibility fallback.
2026-08-21 13:26:09 -05:00
Brues 685ecb4a3e Removed !!!ClassicAPI from toc dependencies
Addon is now baked into the DLL and !!!ClassicAPI will always load before pfUI so this only served to confuse users
2026-08-18 10:35:31 -05:00
Brues 8cdaf1fc00 Prefer GetCoinTextureString in CreateGoldString
If the global GetCoinTextureString exists, use it to format money (wrapped in white color codes) for CreateGoldString. Keeps the existing numeric fallback formatting for environments without GetCoinTextureString.
2026-08-18 04:38:09 -05:00
Brues bb39c9d94e Add localized race info to pfUI environment
Initialize pfUI.env.L["race"] by iterating C_CreatureInfo.GetRaceInfo and C_CreatureInfo.GetFactionInfo. Builds a table keyed by clientFileString containing raceName, raceID and faction (groupTag).
2026-08-18 04:34:34 -05:00
Brues 18059ffbb7 Class-color aura caster name in tooltips 2026-08-18 04:29:33 -05:00
Brues d75e280238 Show party pets in raidpet frames under raidforgroup
When "Use Raid Frames To Display Group Members" is on and in a party,
raid pet frames tried to show raidpet1..40 (nonexistent in a party)
instead of the player pet and partypet1..4.

Remap raidpet grid slots to pet / partypet<N> in UpdateVisibility,
keyed on a stable pfRaidPet<N> slot, mirroring the raid player frames.
LayoutPets now mirror-places the party pet slots, and the raid updater
runs LayoutPets when in a party (not just a raid).

Also exclude raid pet frames (label "partypet", cache_raidpet set) from
the hide_in_raid group-hiding clause so they are not hidden.
2026-08-13 18:03:56 -05:00
Brues 9eb34b8f0a Revert "40-y rangecheck on by default"
This reverts commit af6893c10c.
2026-08-13 17:43:13 -05:00
Brues f8dde49f09 Add IsPlayerGuid API wrapper
Expose the built-in IsPlayerGuid helper through pfUI.api so callers can check whether a GUID or unit token matches the local player. This keeps the pfUI API surface consistent with the underlying WoW API while making the check available through the addon namespace.
2026-08-13 17:32:47 -05:00
Brues 74fbbb6154 Use PFUI_CLASS_COLORS in loothistory v9.0.19 2026-08-12 14:33:05 -05:00