ClassicAPI carries notInterruptible on C_Spell.UnitCastingInfo (arg 8) and
UnitChannelInfo (arg 7); both call sites already stepped over it positionally.
The target, focus and nameplate bars now paint it.
There is no shield art to hang on the icon -- this client ships three
CastingBar textures (Border, Flash, Spark), and Blizzard's shield arrived in
3.x -- so the state is carried by color instead: the bar and the icon's
border both take a new appearance.castbar.shieldcolor, which keeps it legible
on a bar configured without an icon. One api helper paints both so the two
modules cannot drift apart on it.
Dusty rose (.8,.45,.45) rather than a steel grey. Grey is the retail
convention, but pfUI's casting color is already a near-grey pale blue, so the
two separated on brightness alone and washed out against the bar texture. The
rose sits above failcolor in luminance and well below it in saturation, and
the two are further told apart by motion: a failed cast flashes the bar full
and fades, a shielded one fills normally and tints the icon border, which
failcolor never touches.
Not on the player's own bar. The flag is measured against the reader's own
interrupts and silences, so on your own cast it answers a question nobody
asked.
Stamped once at cast start, with the rest of the cast data, rather than
re-polled. ClassicAPI evaluates lazily from the caster's live auras but does
not yet fire the 3.3.5 UNIT_SPELLCAST_INTERRUPTIBLE / _NOT_INTERRUPTIBLE
change events, and polling per frame would undo the event-driven rework these
bars just got -- so a caster who bubbles mid-cast keeps the bar it started
with until the next one.
Two limits worth knowing when it stays dark. The value is relative to your own
kit -- with no interrupt and no silence known it is never reported, whatever
the cast -- and a creature's built-in interrupt immunity lives in
creature_template, which 1.12's SMSG_SPELL_START does not carry, so a boss
with that immunity reads interruptible while your kick still fails on it.
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.
* 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
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.
Add "Show Spell IDs" and "Show Unit IDs" options alongside the existing
item-ID line. Spell IDs come from GameTooltip:GetSpell() on spell
tooltips and from the aura's spellId on unit-aura tooltips; unit IDs
come from C_CreatureInfo.GetCreatureID on the mouseover unit's GUID
(skipped for players and guarded when the GUID carries no entry).
Register GreedMeter's primary window on the meter dock's damage slot,
mirroring the ShaguDPS integration. GreedMeter builds its windows lazily
and supports several, so frames are resolved at call time.
A second GreedMeter window docks into the panel's other half: pfUI's
dock decides fill-vs-split by whether the threat slot is filled, so hook
GreedMeter's window add/remove to reconcile that slot and re-run the
resize. Only the first two windows fit the panel; any beyond that float.
Add a "Show Unit Buffs" option that draws a row of buff icons above the
tooltip for mouseover units. Icons come from a ClassicAPI object pool
and are re-anchored left-to-right on each refresh; a single throttled
ticker counts down the remaining time on the visible icons.
The mouseover-scripts migration dropped the unit frame OnEnter/OnLeave
handlers that read C.unitframes[unit].showtooltip, so nothing consulted the
setting anymore. The engine mouseover (driven by the frame's unit attribute)
now shows the native tooltip unconditionally, so the option could not gate it
even if a reader remained.
Drop the config default, the "Enable Mouseover Tooltip" GUI checkbox, and the
now-orphaned locale string from all translation files.
The per-slot popout arrows previously appeared only while the equipment
manager sidecar was open. Add a "Always Show Equipment Slot Flyouts"
option under Character -> Inventory (character.inventory.equipflyout,
off by default) that instead ties them to the paperdoll, so gear can be
swapped without opening the equipment manager.
Centralize the show/hide in a single UpdatePopouts(): by default the
arrows follow the sidecar, with the option on they follow PaperDollFrame.
Both the sidecar OnShow/OnHide and new PaperDollFrame OnShow/OnHide hooks
route through it, and the flyout hides whenever the arrows go inactive.
Hook GameTooltip:SetUnitAura to append the caster's name; every pfUI aura
tooltip (player buffs, buffwatch, unitframes) routes through it, so one hook
covers them all. Resolve the name from the live sourceUnit token, falling
back to the sourceGUID name cache (players only) when the token is gone.
Gated behind a new tooltip.aurasource option (off by default) with a matching
"Show Aura Caster" checkbox. The toggle reloads the UI, so the hook is
installed conditionally at load rather than re-checking the flag per tooltip.
libbagsort:Sort now accepts an opts table:
- reverse: place the first-ranked item into the last slot of the last
bag (junk fills from the opposite end)
- reversePrio: flip the category ranking (e.g. hearthstone sorts last)
Wired to two new checkboxes under Bags & Bank, both defaulting off.
The druid secondary mana bar (shown while shapeshifted into a form that
uses energy/rage) lived in nampower.lua and read base mana through
nampower's GetUnitField. Extract it into the unit frame proper and drive
it with ClassicAPI instead:
- Create pfDruidMana_<unit> as f.druidmana in CreateUnitFrame (player and
target), lay it out in UpdateConfig from the existing C.unitframes.druidmana*
keys, and update it in a new pfUI.uf:UpdateDruidMana driven by the frame's
own base-refresh pass (UNIT_MANA / UNIT_DISPLAYPOWER). No separate event
frames, no nampower dependency.
- Read mana via UnitPower(unit, 0) / UnitPowerMax(unit, 0), the ClassicAPI
slot getters that return the mana pool regardless of the active power, so
it works while in Cat/Bear form.
- Add a "Show Druid Mana Bar Text" toggle (druidmanatext) so the current/max
readout can be hidden while keeping the bar; config default, GUI checkbox,
and locale stubs.
- Remove the now-dead block from nampower.lua.
- selfinraid now gates on `not IsInGroup()`, so "show self in raid frames"
applies only when truly solo (both party and raid suppress it), matching
the option's actual behavior.
- Hide the redundant group frames when a party is promoted to the raid grid
(raidforgroup + hide_in_raid), not just in an actual raid. A shared
hide_group local drives both the party-member and self branches; the
party-member branch is scoped to cache_raid == 0 so the raidforgroup-mapped
raid frames (which themselves carry label "party") aren't hidden too.
- Rename "Always Show Self In Raid Frames" to "Show Self In Raid Frames When
Solo" in gui.lua and all locale files; the four previously-translated
strings are reset to nil stubs since the meaning changed.
commit f19d7402810637fc32460864104cb792ac5af863
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date: Wed Jul 22 01:05:52 2026 -0500
remove comment
commit 60f4968f06d19d42bdc6347f98ff4d5a34785e97
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date: Wed Jul 22 01:05:06 2026 -0500
Bump ClassicAPI minimum version to 10705
Update the minimum required ClassicAPI version from 10704 (1.7.4) to 10705 (1.7.5).
commit 088dba4c23f4aa7ce98b9ce9d75bc5892cd40520
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date: Wed Jul 22 01:04:04 2026 -0500
Add raid-pet frames: an independent, roster-driven pet grid
New "raidpet" unitframe type that shows raid members' pets (raidpet1..40)
in their own movable block (pfRaidPetCluster), off by default. It's a
flat pool of frames laid out by pfUI.uf.raid:LayoutPets straight from the
raid roster -- cell N shows raidpet<N> -- so it's fully decoupled from how
the raid grid arranges its own slots.
Layout is independent of the raid grid: raidpet carries its own
width/height plus a Layout section (raidlayout / raidpadding / raidfill),
and its own Collapse Empty Slots toggle that packs only the pets that
exist into the leading cells. The raid grid gets the same collapse option
(sequential slot assignment in AddUnitToGroup instead of by subgroup).
Collapsed pets re-pack on roster changes and on UNIT_PET so summons and
dismisses track live without polling.
Also:
- unitframes: a "raidpet" branch in UpdateVisibility (hide when the pet is
out of range or its raid<N> owner is gone), and fix cache_raid so the
"pfRaid" prefix check doesn't misread pfRaidPet<n> frames (char 7 is
non-numeric -> nil compare crash).
- New "Owner Name" text option: on any pet frame (raidpet/partypet/pet) it
shows the owner's class-colored name so you can tell whose pet it is.
- unlock: a pfRaidPet drag cluster, a numeric-suffix guard so pfRaid no
longer matches (and crashes on) pet frames, and RaidPet config mappings.
- config/gui/translations for all of the above.
commit 52f02c8963fe7ec90f36100ebe069c7807529301
Author: Brues <5278969+brues-code@users.noreply.github.com>
Date: Mon Jul 20 23:09:06 2026 -0500
Enumerate chat bubbles via ClassicAPI instead of scanning WorldFrame
ClassicAPI's C_ChatBubbles.GetAllChatBubbles() walks the engine's own
bubble list and returns the exact set of live bubble frames, with real
GetRegions(), so the decoration idiom works unchanged. Replace the
WorldFrame:GetChildren() sweep and drop the IsBubble heuristic (unnamed
frame whose first region is the ChatBubble-Background texture) -- the API
only ever hands back bubbles, so that guess is both redundant and more
fragile than the engine list. Cheaper too: it iterates only live bubbles
rather than every world child on each chat event.
librange was a per-frame position scanner: it swept party/raid unit
tokens, cached each one's distance via UnitPosition, and answered range
queries from that cache. All of it existed only because 1.12 had no cheap
way to check an arbitrary unit's distance. ClassicAPI's UnitInRange does
exactly that C-side (fixed 40y healing range, position miss reported via
the second return), so the whole library collapses to a direct call.
Wins from dropping the cache:
- No staleness. The scanner's zone-death and roster-reindex bugs simply
can't exist without a cache to go stale, so this supersedes the
keep-alive fixes from 756e8840.
- All classes get target-frame range fading. The old target path faked a
40y check via IsActionInRange on a healing spell found on the action
bar, so classes without such a spell (GetRangeSlot returned nil) never
had a working target range check.
The rangecheck == "0" master switch used to be enforced by hiding the
scanner; with no scanner, move that gate into pfUI.api.UnitInRange so
disabling the check still means nothing fades. Threshold is now 40y (the
ClassicAPI constant) rather than the old 45y. Drop the now-dead
rangechecki (Range Check Interval) setting, its GUI row, and migration.
Mirror the druid-stealth page switch for priests: swap to the auto page
(8) while in Shadowform, back to the default page when it drops. Driven
by UPDATE_SHAPESHIFT_FORM (form ID 28), which fully covers shadowform on
its own -- no prowl-style stealth substate to chase.
Fold the new state into the existing prowl machinery instead of
duplicating it: prowl/shadowform are mutually exclusive by class, so one
shared `formpaging` flag and one page constant serve both, and the
OnUpdate switch collapses to a single class-gated block.
New module built on ClassicAPI's C_NewItems + BAG_NEW_ITEMS_UPDATED.
Glows bag slots (bags 0-4) holding items acquired since login, keyed on
item GUID so the flag survives rearranging. Hovering an item
acknowledges it (RemoveNewItem); closing the bags clears the rest via
ClearAll. Glow is UI-ActionButton-Border, sized off the slot width so it
tracks the icon_size config.
To stay decoupled from the bag frames, the bag module now broadcasts a
reusable "bag:closed" event through pfUI.events from its OnHide (carrying
the container so subscribers can tell backpack from bank), guarded so the
initial setup Hide() doesn't fire a phantom close.
Config: appearance.bags.newitem + newitem_color, with GUI toggles.
The castbar options were one long list. Break them into four subentries
under the Castbar parent (matching the Settings/Actionbar layout):
General (fonts, colors, texture, disable-blizzard) plus one page each for
Player, Target, and Focus. Drops the now-redundant per-unit header rows.
Adds per-unit (player/target/focus) dropdowns to align the castbar
spell name (left text) and cast timer (right text) Left/Center/Right.
Both share a castbaralign dropdown; defaults preserve current behavior
(name LEFT, timer RIGHT). Applied at castbar creation, so takes effect
on /reload like the other castbar options.
Drop the manual C_Item.GetItemStatDelta rendering (inline annotations
and bottom-block summary) in favor of driving native shopping tooltips
via SetHyperlinkCompareItem — the 3.3.5 flow now available through
ClassicAPI. Way less code, and Blizzard's own comparison rendering
handles all stat types uniformly.
Also revert the mode dropdown back to a single basestats checkbox — the
new implementation doesn't distinguish base vs extended (Blizzard's
tooltip shows everything the item exposes), so the two-level control
was redundant. Existing basestats configs pass through unchanged.
AtlasLootTooltip now goes through the shared HookTooltip helper instead
of a bespoke OnShow shim.
Replace the two-checkbox arrangement (Compare Base Stats + Compare
Extended Stats, with the latter gated on the former) with a single
"Item Comparison" dropdown offering Off / Base / Extended. Migrate
existing configs.
Also route CreateConfig's value-change sites through pfUI.events
("config:changed", category, config) so callers can react to arbitrary
setting changes without frame-specific plumbing. Use it here to grey
out "Always Show Item Comparison" when the mode is Off.
Refactor delta rendering:
- extendedstats on -> all stats (base + extended + DPS + block value)
appended at the bottom via AddDoubleLine
- extendedstats off -> base stats annotated inline; nothing at the bottom
Requires ClassicAPI's 60-line tooltip fix so the bottom block isn't
truncated. DPS and block value were previously extended-only inline
matches — moved into BASE_STAT_KEYS so they also show at the bottom.
Also extract MakeDependent(child, parent) helper from the local
gate-lambda in gui.lua so the base/extended checkbox pairing generalizes.
Replaces the twin-tooltip text extraction/comparison pair
(ExtractAttributes + CompareAttributes) with a single delta pull from
ClassicAPI's C_Item.GetItemStatDelta(equippedLink, newLink).
Base stats (Str/Agi/Sta/Int/Spi/Mana/Health + Armor + resistances) stay
annotated inline on their existing tooltip line: iterate the tooltip's
FontString regions (no more _G["...TextLeft"..i] name lookup), match the
"+N Foo" prefix, look up the trailing noun in a label→key map, append
(+delta)/(-delta) from the ClassicAPI delta table.
Extended stats (attack power / ranged AP / spell damage/healing / crit
ratings / hit ratings / mana regen / defense / DPS) don't emit their own
line — vanilla mixes them into equip-spell descriptions ("Equip:
Increases your critical strike chance by 1%") — so aggregate them into
a "Compared to equipped:" block at the bottom via AddDoubleLine. New
`tooltip.compare.extendedstats` config knob (default 1) gates that
block; it depends on `basestats` being on (its GUI checkbox grays out
otherwise). DPS rounded to one decimal — ClassicAPI derives it from
damage/delay so it lands as a raw float.
Random-suffix bonuses ("of the Bear" etc.) now count correctly since
GetItemStatDelta walks item-record + equip-spell auras + suffix
enchants server-side.
New tooltip.movespeed config knob (default off, checkbox in the GUI's
tooltip page). When on, the unit tooltip gains a "Speed: N%" line where
N is the unit's run speed normalized to vanilla's 7.0 yd/s base — 100
unmounted, 160 on a 60% mount, 200 on epic, less under snares.
Uses runSpeed (return 2 of GetUnitSpeed), not currentSpeed, so the
number reflects what the unit *would* be running at — visible even
while they're standing still. runSpeed is 0 for out-of-range units, so
the line is skipped in that case.
The player frame's "Effective Haste" mode was hardcoded talent-position
scrapes: GetTalentInfo(1, 16) for the Mage "Accelerated Arcana"
(flat 5%) and GetTalentInfo(1, 14) for the Warlock "Rapid Deterioration"
(3% per rank), folded into the displayed haste % via
`(1 / (modCastSpeed * modCastingTime) - 1) * 100`.
That's two problems in one:
- Hardcoded talent indices and effect percentages — brittle to any
Turtle tree reshuffle or retune.
- Conceptually muddled: it folds gear-haste and talent-cast-reduction
into one number that's hard to read as anything specific. The actual
effective cast time is already shown on the cast bar via
C_Spell.UnitCastingInfo (engine helper accounts for SpellMod op 10).
Drop modCastingTime, the LEARNED_SPELL_IN_TAB watcher frame that
maintained it, the per-class talent scrape, and the hasteMode == "2"
display branch. Collapse the now-binary "display_haste" config from a
3-option dropdown to a checkbox. Users on legacy "2" will see the
checkbox unchecked once and can re-enable with a single click.
Hook DoTradeSkill to capture the requested count, then on the first
SPELLCAST_START of an isTradeskill cast stretch endTime to span all
crafts so the player bar fills continuously across the chain. Mid-chain
SPELLCAST_START / SPELL_START_SELF events refresh the "(N)" remaining
label and reset a per-craft spark that crosses the bar once per craft.
SPELL_GO_SELF counts completions; SPELLCAST_STOP no-ops while merged.
Gated by a new C.castbar.player.mergetradeskill knob (default on).
With every external caller of libdebuff:UnitDebuff / :UnitOwnDebuff
now on C_UnitAuras, the two public per-aura readers and the
nameplate-side cache they were feeding have no consumers.
- libs/libdebuff.lua: removes libdebuff:UnitDebuff (~120 lines),
libdebuff:UnitOwnDebuff (~75 lines), the _ownDebuffSortFunc helper,
and the local cache table. The slotOwnership / ownDebuffs /
allAuraCasts / pendingCasts bookkeeping stays — GetBestAuraCast
(libpredict) and GetEnhancedDebuffs (CleveRoids) still read it, and
the event handlers maintain it. GetSlotCaster / GetDebuffSlotMap
stay too; the DEBUFF_ADDED_OTHER handler and the debug printer use
them. File goes 2010 → 1870 lines.
- modules/nameplates.lua: deletes PlateCacheDebuffs (was already
rewritten on C_UnitAuras and unused once the display loop bypassed
the cache), PlateUnitDebuff, the cachedVerify scaffolding, and the
nameplate.UnitDebuff / nameplate.CacheDebuffs registrations.
- api/config.lua + modules/gui.lua: drops the now-defunct
"guessdebuffs" knob — its only effect was gating the dead cache.
Splits the ClassicAPI version check into a hard floor (MIN, manual)
and a soft target (LATEST, pinned by the release workflow). Below MIN
pfUI disables itself entirely as before; between MIN and LATEST it
runs normally but fires a delayed chat nudge after PLAYER_LOGIN with
the available update. PLAYER_LOGIN handlers swap to
EventUtil.ContinueOnPlayerLogin so we don't have to spin a frame for
each branch.
URLs centralize on toc X-Website fields. pfUI.lua factors out
ClassicAPIReleaseUrl(version) sourcing from !!!ClassicAPI's metadata;
modules/gui.lua and libs/libdebuff.lua replace hardcoded
me0wg4ming/pfUI links with GetAddOnMetadata(pfUI.name, "X-Website")
lookups.
release.yml gains a pre-package step that queries ClassicAPI's latest
release tag, packs it (X*10000 + Y*100 + Z), and seds only the LATEST
line in pfUI.lua so the published zip ships with the correct soft
target.
- Switch the four GUID reads in OnDataChanged from plate.parent:GetName(1)
(SuperWoW idiom) to plate.cachedGuid (set by NAME_PLATE_UNIT_ADDED via
UnitGUID(token)). Move the initial OnDataChanged call out of
OnConfigChange's CREATE path so it runs after UNIT_ADDED has populated
cachedGuid; re-add it explicitly in the user-config-change loop.
- Reject GetUnitField's health/maxHealth when maxHealth == 100 — the
engine writes (hp_percent, 100) into UnitFields for non-detailed units
(UPDATE_PARTIAL packets carry percent only). Without this guard,
Nampower's raw field read returns the percent and the nameplate displays
"5 / 100" as if it were real HP. Mirrors libhealth's heuristic so we
fall through to its estimator instead.
- New config: nametextpos (LEFT/CENTER/RIGHT, defaults to CENTER).
Decouple the bar's anchor from the name so the name's JustifyH can
shift left/right without dragging the bar with it.
Adds a Fizzle-style durability label to each character pane slot.
Renders at the bottom of the slot icon as "N%" colored by the existing
DURABILITY_THRESHOLD_COLORS table (red < orange < yellow < invasion <
green), or hidden when the item has no durability stat (necks, rings,
trinkets) or the slot is empty.
api/api.lua: new pfUI.api.CreateFontString(f, key, layer, size, flags,
font) helper following the CreateBackdrop pattern — idempotent
get-or-create that attaches the FontString to the parent frame as the
named field. Defaults to pfUI.font_default at C.global.font_size with
OUTLINE on the OVERLAY layer.
skins/blizzard/character.lua: scoreText migrated from the inline
"if not frame.scoreText then ..." block to the new helper as a working
example; new durabilityText created the same way at BOTTOM/size 10.
Durability render added to RefreshCharacterSlot — runs on every
PaperDollItemSlotButton_Update so equip/unequip/repair/damage all
refresh automatically.
api/config.lua + modules/gui.lua: new C.character.inventory.durability
toggle (default "1"). GUI lives under a new top-level "Character" tab
→ "Inventory" sub-tab, leaving room for future "Reputation" / "Skills"
sub-tabs at the same level.
Drop the now-vestigial expansion plumbing.
- Delete modules/thirdparty-tbc.lua + its xml Include
- Strip 10 tbc-tagged CreateConfig calls in modules/gui.lua
- Drop the expansion arg from CreateConfig() signature + the disabled-
entry rendering path that depended on it
- Drop the showdisabled GUI toggle + its default
- Simplify pfUI:RegisterModule / pfUI:RegisterSkin to (name, func) only
- Strip the leading version arg ("vanilla:tbc", etc.) from all 114
Register call sites
- Delete the pfUI.expansion variable
- Decouple OnValueChanged from OnDataChanged to prevent expensive
full updates on every HP tick
- Replace UnitAffectingCombat(guid) with GetUnitField(flags) bitcheck
and add 0.2s per-GUID throttle cache for GetCombatStateColor
- Gate HP bar SetMinMaxValues/SetValue and text formatting behind
hp/hpmax change detection
- Inline castbar update into per-plate OnUpdate loop, remove dedicated
castbarFrame overhead
- Switch debuff slot cache from name-keyed to slot-index-keyed to fix
timer reset bug when debuffs shift after expiry
- Add raidGuidCache (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
for O(1) offtank target-name lookup
- Add UNIT_FLAGS_GUID event support for instant combat flag notification
(Nampower)
- Extract RebuildOfftanks() to ensure offtanks table is populated at
startup, not only on config change
- Reuse childs table across scan ticks to reduce GC pressure
- Add zoominstant config option to skip zoom animation
- Add combatColorCache cleanup on plate hide and combat leave
Please report any bugs that appear after this change, since it is a huge change.