enhanced merge into master

Final release!
This commit is contained in:
Meow
2026-02-04 22:48:30 +01:00
parent df550cf9e2
commit 54355c9019
22 changed files with 4660 additions and 1238 deletions
+942 -2
View File
@@ -12,6 +12,946 @@ This version includes significant performance improvements, DLL-enhanced feature
> **Looking for TBC support?** Visit the original pfUI by Shagu: [https://github.com/shagu/pfUI](https://github.com/shagu/pfUI)
## 🎯 What's New in Version 7.6.0 (February 3, 2026)
### 🚀 Centralized Cast-Bar Tracking System (libdebuff.lua + nameplates.lua)
**Major architectural change: Cast tracking moved from nameplates to libdebuff for single source of truth!**
Previously, both `nameplates.lua` and `libdebuff.lua` independently tracked cast events, creating code duplication and maintenance overhead. Now all cast tracking is centralized in `libdebuff.lua` with nameplates consuming shared data.
**libdebuff.lua - NEW Cast Tracking:**
- ✅ `SPELL_START_SELF/OTHER` → Cast-Start Tracking
- ✅ `SPELL_GO_SELF/OTHER` → Cast-Completion Tracking
- ✅ `SPELL_FAILED_OTHER` → Cast-Cancel Detection (movement, interrupts, OOM)
- ✅ `pfUI.libdebuff_casts` → Shared cast data structure `[casterGuid] = {spellID, spellName, icon, startTime, duration, endTime, event}`
- ✅ `pfUI.libdebuff_GetSpellIcon()` → Icon cache export function
**nameplates.lua - Simplified Cast Consumption:**
- ✅ `GetCastInfo(guid)` → Reads `pfUI.libdebuff_casts`
- ✅ `pfUI.libdebuff_GetSpellIcon` → Uses shared icon cache
- ❌ `UNIT_CASTEVENT` → **REMOVED** (replaced by Nampower SPELL_* events)
- ❌ Local cast tracking code → **REMOVED** (~56 lines saved)
**Benefits:**
- **100% Nampower, 0% SuperWOW** - No longer depends on UNIT_CASTEVENT
- **Single Source of Truth** - Cast data only tracked once
- **Icon Cache 100-400x faster** - First lookup via Nampower's GetSpellIconTexture, then cached
- **Easier Maintenance** - Changes only in one place
- **Code Reduction** - 56 lines removed from nameplates.lua
### ⚠️ Nampower Version Requirement Update (libdebuff.lua)
**Now requires Nampower 2.27.2+ (SPELL_FAILED_OTHER bug fix):**
Version 2.27.1 had a bug where `SPELL_FAILED_OTHER` didn't fire for movement-cancelled casts. This is now fixed in 2.27.2.
**User Warnings:**
- **2.27.2+**: ✅ Success message + auto-enable CVars
- **2.27.1**: ⚠️ Yellow warning + popup (cast-bar cancel broken)
- **< 2.27.1**: ❌ Red error + popup (debuff tracking disabled)
- **No Nampower**: ❌ Red error + popup (addon disabled)
**NEW StaticPopup Dialogs:**
Popups appear center-screen on login to ensure users don't miss the version requirement!
### 🌿 libpredict HoT Tracking Integration (libpredict.lua)
**Major enhancement: libdebuff integration for server-accurate HoT tracking!**
Previously, libpredict relied purely on prediction (UNIT_CASTEVENT + timing calculations). Now it uses libdebuff's AURA_CAST events for server-side accurate buff/debuff data when available.
**NEW Hybrid System:**
```
GetHotDuration(unit, spell):
1. Try libdebuff first (Nampower AURA_CAST events)
↓
if available: return server-accurate data
2. Fallback to prediction (legacy system)
↓
Use hots[] table with UNIT_CASTEVENT prediction
```
**Benefits:**
- ✅ **Server-accurate durations** - No prediction needed with Nampower
- ✅ **Automatic rank protection** - Built into libdebuff's system
- ✅ **Multi-caster support** - Multiple druids = multiple rejuvs tracked separately
- ✅ **Zero overhead** - libdebuff already tracks all auras
- ✅ **Backwards compatible** - Falls back to prediction without Nampower
**NEW Rank Support for HoTs:**
Extended `Hot()` function signature to include rank parameter:
```lua
function libpredict:Hot(sender, target, spell, duration, startTime, source, rank)
```
**Rank Protection Logic:**
- Don't overwrite Rank 10 HoT with Rank 8!
- Active higher-rank HoTs block lower-rank applications
- Works with multiple casters simultaneously
**HealComm Protocol Extended (Backwards Compatible):**
- OLD: `"Reju/TargetName/15/"`
- NEW: `"Reju/TargetName/15/10/"` (rank added)
- `"0"` = unknown rank (for non-rank-aware clients)
**Example Scenario:**
```
Druid A casts Rejuvenation Rank 10 (15s duration)
Druid B casts Rejuvenation Rank 8 (12s duration)
With rank protection:
→ Rank 8 is BLOCKED while Rank 10 is active!
→ No more accidental overwrites of better HoTs!
```
### 📊 Code Statistics
**libdebuff.lua:**
- Lines: 2743 → 2835 (+92 lines)
- Events: 12 → 15 (+3: SPELL_START_SELF/OTHER, SPELL_FAILED_OTHER)
- Exports: 14 → 16 (+2: pfUI.libdebuff_casts, pfUI.libdebuff_GetSpellIcon)
**nameplates.lua:**
- Lines: 1826 → 1770 (-56 lines)
- Events: 7 → 6 (-1: UNIT_CASTEVENT removed)
- Code removed: ~74 lines (UNIT_CASTEVENT handler, local cast tracking)
**libpredict.lua:**
- Lines: 935 → 1095 (+160 lines)
- New: libdebuff integration, rank support, rank protection logic
- Backwards compatible: Works with/without Nampower
---
## 🎯 What's New in Version 7.5.1 (February 02, 2026)
- Added icon cache system - Icons are now cached in pfUI.libdebuff_icon_cache for instant lookups after first access
- Replaced SpellInfo texture lookups with GetSpellIconTexture - Direct DBC queries via Nampower (~100-400x faster than tooltip parsing)
- Optimized UnitDebuff() function - Now uses GetUnitField("aura") to retrieve spell IDs directly from unit data, then fetches icons via GetSpellIconTexture instead of vanilla UnitDebuff API
- Changed fallback icons - Unknown spell icons now display QuestionMark instead of class-specific icons
- Performance impact - Icon lookups reduced from ~5-20ms to ~0.05ms (first) / ~0.001ms (cached) per debuff, resulting in 600-2600x speedup for full debuff bars
- Replaced in libdebuff.lua the UNIT_CASTEVENT of Superwow with Nampowers SPELL_GO and SPELL_START events (slowly trying to get away from superwow, not maintained anymore and outdated)
---
## 🎯 What's New in Version 7.5.0 (January 31, 2026)
### 🔧 Player Buff Bar Timer Fix (buffwatch.lua)
**Fixed buff timers resetting on Player Buff/Debuff Bars when other buffs expire:**
Previously, buff bar timers would reset or jump when other buffs expired because the UUID (unique identifier) included the slot number. Since slots shift when buffs expire, the same buff would get a new UUID, causing the timer bar to think it's a new buff.
**The Problem:**
- Old UUID: `texture + name + slot` (e.g., "PowerWordFortitude_tex_PWF_3")
- Buff in slot 3 expires → slots 4,5,6 shift down to 3,4,5
- UUID changes from `..._4` to `..._3` → timer resets!
**The Solution:**
- Player buffs now use: `texture + name` only (no slot)
- Target debuffs still use: `texture + name + slot` (needed for multi-caster scenarios)
```lua
-- For player: no slot in uuid (slots shift when other buffs expire)
-- For target: include slot (multiple players can have same debuff)
local uuid
if frame.unit == "player" then
uuid = data[4] .. data[3] -- texture + name only
else
uuid = data[4] .. data[3] .. data[2] -- texture + name + slot
end
```
### 🛡️ Immunity Check for Debuff Timers (libdebuff.lua)
**No more phantom timers for immune targets:**
When a target is immune to your debuff (e.g., Rake bleed on a bleed-immune mob), the `AURA_CAST` event fires but `DEBUFF_ADDED` never comes. Previously this could create a timer with icon for a debuff that was never actually applied.
**The Fix:**
- Debuff data now requires `slot` to be set (confirmed by `DEBUFF_ADDED_OTHER` event)
- If `AURA_CAST` fires but `DEBUFF_ADDED` never comes → `slot` stays `nil` → no timer/icon shown
```lua
-- IMMUNITY CHECK: Only show if slot is set (confirmed by DEBUFF_ADDED_OTHER)
-- This prevents showing timers for spells like Rake where the bleed is immune
if data.slot and timeleft > -1 then
-- Show the debuff
end
```
### 🎯 UnitDebuff() Now Returns Caster Information (libdebuff.lua)
**Enhanced UnitDebuff() API - 8th return value is now `caster`:**
```lua
local name, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitDebuff(unit, id)
-- caster = "player" (your debuff), "other" (someone else's), or nil (unknown)
```
**Use Cases:**
- Buff bar tooltip can now find correct slot for "only own debuffs" mode
- UI can differentiate between your debuffs and others' debuffs
- Enables future features like "show only my debuffs" filters
### 🔄 Buff Bar Tooltip Fix for "Only Own Debuffs" Mode (buffwatch.lua)
**Fixed tooltip showing wrong debuff in "only own debuffs" mode:**
When using the "Show only own debuffs" option on Target Debuff Bars, hovering over a debuff could show the wrong tooltip because the displayed slot didn't match the actual game slot. Now searches through all game slots to find the correct one by matching spell name AND caster.
### 🔧 Lua 5.0 Local Variable Limit Workaround (libdebuff.lua)
**Fixed addon failing to load due to Lua 5.0's 32 local variable limit:**
Lua 5.0 (used by WoW 1.12) has a hard limit of 32 local variables per function scope. As libdebuff grew, it hit this limit and stopped loading entirely.
**The Solution:** Moved 11 tables from local scope to `pfUI.` namespace:
| Old (local) | New (pfUI. namespace) |
|-------------|----------------------|
| `ownDebuffs` | `pfUI.libdebuff_own` |
| `ownSlots` | `pfUI.libdebuff_own_slots` |
| `allSlots` | `pfUI.libdebuff_all_slots` |
| `allAuraCasts` | `pfUI.libdebuff_all_auras` |
| `pendingCasts` | `pfUI.libdebuff_pending` |
| `objectsByGuid` | `pfUI.libdebuff_objects_guid` |
| `debugStats` | `pfUI.libdebuff_debugstats` |
| `lastCastRanks` | `pfUI.libdebuff_lastranks` |
| `lastFailedSpells` | `pfUI.libdebuff_lastfailed` |
| `lastUnitDebuffLog` | `pfUI.libdebuff_lastlog` |
| `cache` | `pfUI.libdebuff_cache` |
### 🛑 Crash 132 Fix (Credits: jrc13245)
**Fixed client crash (Error 132) when logging out:**
WoW crashes with Error 132 when addons make API calls like `UnitExists()` during shutdown, especially with UnitXP DLL installed.
**The Fix:** Register `PLAYER_LOGOUT` event and immediately disable all event handling:
```lua
frame:RegisterEvent("PLAYER_LOGOUT")
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
-- ... rest of event handling
end)
```
**Applied to:**
- `libdebuff.lua` - All event frames
- `nameplates.lua` - nameplates + nameplates.combat frames
- `nampower.lua` - Spell queue indicator frame
- `superwow.lua` - Secondary mana bar frames
- `actionbar.lua` - Page switch frame
### ⚡ Performance Micro-Optimizations
**Various small performance improvements across the codebase:**
| Optimization | Location | Benefit |
|-------------|----------|---------|
| `childs` table reuse | nameplates.lua | Avoids creating new table every scan cycle |
| Indexed access instead of `pairs()` | nameplates.lua | Faster debuff timeout scanning |
| Quick exit if not in combat | nameplates.lua | Skips threatcolor calculation when unnecessary |
| Player GUID caching | libdebuff.lua | Avoids repeated `UnitExists("player")` calls |
| Consistent DoNothing() pattern | unitframes.lua, nameplates.lua | Lightweight frames when animation disabled |
### 📋 Complete libdebuff.lua Feature Summary
For reference, here's everything the enhanced libdebuff system now provides:
**Debuff Detection:**
- ✅ Checks for dodges, misses, resists, parries, immunes, reflects, and evades
- ✅ Immunity check - no timer if debuff wasn't actually applied
- ✅ Tracks if debuff is from YOU or from OTHERS (including their GUID)
**Rank & Duration:**
- ✅ Rank protection - lower rank spells can't refresh higher rank timers
- ✅ Shared debuff logic (`uniqueDebuffs` and `debuffOverwritePairs`)
- ✅ Faerie Fire ↔ Faerie Fire (Feral), Demo Shout ↔ Demo Roar overwrites (with rank check!)
- ✅ Combo point finisher duration (Rip, Rupture, Kidney Shot)
- ✅ Talent-based duration modifiers (Booming Voice, Improved SW:P, etc.)
**Tracking:**
- ✅ Multi-target debuff tracking via GUID
- ✅ Debuff stack tracking for stackable debuffs
- ✅ Handles dispels and removals via events
**API:**
- ✅ `UnitDebuff()` returns: name, rank, texture, stacks, dtype, duration, timeleft, caster
- ✅ `UnitOwnDebuff()` for filtering only your own debuffs
- ✅ Cleveroids API compatibility via `objectsByGuid`
**Debug Commands:**
- `/shifttest start/stop/stats/slots` - Debug debuff slot tracking
- `/memcheck` - Show memory usage statistics
---
## 🎯 What's New in Version 7.4.3 (January 29, 2026)
### ⚡ Massive Performance Optimization - Cooldown Frame Overhaul
**Revolutionary frame creation system that eliminates unnecessary Model frames:**
Previously, pfUI created expensive Model frames for every single buff/debuff cooldown timer, even when the animation was disabled. This caused significant performance overhead, especially in 40-man raids where hundreds of frames were created but never actually used.
**The Problem:**
- Old system: **ALWAYS** created Model frames with `CooldownFrameTemplate`
- When animation was disabled, frames were just hidden with `SetAlpha(0)`
- The frames still existed and consumed CPU resources in the background
- In raids: 40 players × 32 buffs/debuffs = **1,280 Model frames** running even when animations were off!
**The Solution (Nameplates.lua + Unitframes.lua):**
- New system: Creates **Frame type based on config setting**
- Animation ON → Model frame with `CooldownFrameTemplate` (expensive but animated)
- Animation OFF → Regular Frame with dummy functions (lightweight, no animation)
- Dummy functions (`DoNothing()`) prevent crashes when `CooldownFrame_SetTimer()` is called
**Performance Impact:**
| Scenario | Before 7.4.3 | After 7.4.3 | Improvement |
|----------|--------------|-------------|-------------|
| Player frame (animation ON) | 32 Model frames | 32 Model frames | No change ✅ |
| 40 raid frames (animation OFF) | 1,280 Model frames | 1,280 Light frames | **100% lighter!** 🚀 |
| Mixed (player ON, raid OFF) | 1,312 Model frames | 32 Model + 1,280 Light | **98% less Model frames!** 🎯 |
**Real-World Example:**
```
Before: ALL frames = 1,312 expensive Model frames
After: 32 Model (player) + 1,280 Light (raid) = 98% reduction in overhead
```
**Technical Implementation:**
```lua
-- Nameplates.lua & Unitframes.lua
if cooldown_anim == 1 then
-- Create expensive Model frame
cd = CreateFrame("Model", ...)
else
-- Create lightweight Frame
cd = CreateFrame("Frame", ...)
cd.AdvanceTime = DoNothing
cd.SetSequence = DoNothing
cd.SetSequenceTime = DoNothing
end
```
**User Experience:**
- ✅ **GUI Integration:** Toggling "Show Timer Animation" now prompts for `/reload`
- ✅ **Per-Frame Control:** Each unitframe type (player/target/raid/party) has independent settings
- ✅ **Immediate Effect:** Reload applies the correct frame type based on your config
- ✅ **No Visual Change:** When animation is ON, everything looks identical (just way more efficient!)
**Why This Matters:**
- **40-man raids:** Dramatically reduced frame update overhead
- **Low-end PCs:** Smoother gameplay with animations disabled
- **Battery life:** Less CPU usage = longer laptop battery
- **Future-proof:** Foundation for more performance optimizations
**Compatibility:**
- Works with existing timer text display (independent of animation)
- Fully backward compatible with all existing configs
- No changes needed to user settings (automatic on reload)
---
## 🎯 What's New in Version 7.4.2 (January 28, 2026)
### Major Performance Improvements
- **Much faster debuff tracking** - No more lag in 40-man raids
- **10x less memory usage** - Runs cleaner over long raid sessions
- **Instant cleanup** - Dead mobs cleaned up immediately (was: 2-5 minutes)
### Better Debuff Tracking
- **Multi-player debuffs** - See debuffs from all raid members with accurate timers
- **Rank protection** - Lower rank spells can't overwrite higher ranks anymore
- **100% accurate positioning** - Debuff icons always in the correct slot
- **Better combo points** - Rip, Rupture, Kidney Shot show correct duration
---
## 🎯 What's New in Version 7.4.1 (January 27, 2026)
### 🎯 Nameplate Debuff Timer Improvements
- ✅ **New Option: Enable Debuff Timers** - Toggle for debuff timer display on nameplates
- Moved from hidden location (Appearance → Cooldown → "Display Debuff Durations") to Nameplates → Debuffs
- All timer-related options are now grouped together for better discoverability
- ✅ **New Option: Show Timer Text** - Toggle the countdown text (e.g., "12s") on debuff icons
- Previously always shown, now configurable
- ✅ **Show Timer Animation** - Existing pie-chart animation option, now properly grouped with other timer options
### 🖼️ Unitframe Timer Config Fix (unitframes.lua)
- ✅ **Live Config Updates** - "Show Timer Animation" and "Show Timer Text" now update immediately
- Previously: Changes only applied after buffs/debuffs were refreshed
- Now: Toggling the option instantly shows/hides the animation and text on existing buffs/debuffs
### 🔧 Slot Shifting Fix Attempt (libdebuff.lua)
- ✅ **DEBUFF_REMOVED now uses slotData.spellName** - Previously used spellName from scan, which could be wrong after slot shifting
- When debuffs shift slots (e.g., slot 3 removed, slots 4+ shift down), the scan might read a different spell
- Now uses `removedSpellName = slotData.spellName` from stored slot data for consistency
- ✅ **Cleanup empty spell tables** - After removing a caster from allAuraCasts, checks if no other casters remain and removes the empty spell table
- ✅ **Defensive casterGuid validation** - Checks for empty string and "0x0000000000000000" before looking up timer data
- ✅ **Invalid timer detection** - Warns when remaining > duration (impossible state)
- ✅ **ValidateSlotConsistency function** - Debug function to verify allSlots and allAuraCasts consistency after shifting
- ✅ **Enhanced debug logging** - All debug messages now include target= for easier filtering
---
## 🎯 What's New in Version 7.4.0 (January 26, 2026)
### 🗡️ Rogue Combo Point Fix
**PLAYER_COMBO_POINTS event now works for Rogues:**
The combo point tracking was previously only enabled for Druids. Rogues were completely ignored, causing abilities like Kidney Shot to always show base duration (1 sec) instead of the correct CP-scaled duration.
**Technical Details:**
- Nampower sends `durationMs=1000` (base duration) for Kidney Shot
- Code checked `if duration == 0` before calling `GetDuration()`
- Since duration was 1 (not 0), the CP calculation was skipped
- Fix: Always call `GetDuration()` for CP-based abilities, regardless of event duration
### ⚙️ New Settings: Number & Timer Formatting
**Abbreviate Numbers (Settings → General):**
| Option | Example |
|--------|---------|
| Full Numbers | 4250 |
| 2 Decimals | 4.25k |
| 1 Decimal | 4.2k (always rounds DOWN) |
**Castbar Timer Decimals (Settings → General):**
| Option | Example |
|--------|---------|
| 1 Decimal | 2.1 |
| 2 Decimals | 2.14 |
### 🎬 Nameplate Castbar Improvements
**Smooth Castbar Animation:**
- Fixed stuttering castbar caused by incorrect throttle placement
- Scanner throttle (0.05s) now only affects nameplate detection
- Castbar updates run at full 50 FPS for smooth animation
**Countdown Timer:**
- Castbar timer now counts DOWN (3.0 → 0.0) instead of up
- Shows remaining cast time, not elapsed time
**Intelligent Throttling (unchanged):**
- Target OR casting nameplates: 0.02s (50 FPS)
- All other nameplates: 0.1s (10 FPS)
- Event updates bypass throttle entirely
### 🧹 Memory Management
**Cache cleanup for hidden nameplates:**
- `guidRegistry` cleared when plate hides
- `CastEvents` cleared when plate hides
- `debuffCache` cleared when plate hides
- `threatMemory` cleared when plate hides
Prevents memory leaks when mobs die or go out of range.
---
## 🎯 What's New in Version 7.3.0 (January 25, 2026)
### ⚡ O(1) Performance Optimizations for Unitframes
**Complete rewrite of health/mana lookups using Nampower's `GetUnitField` API:**
The unitframes now use direct memory access via `GetUnitField(guid, "health")` instead of the slower `UnitHealth()` API calls. This provides significant performance improvements especially in raids.
**Key Changes:**
| Component | Before (7.2.0) | After (7.3.0) |
|-----------|----------------|---------------|
| HealPredict Health | `UnitHealth()` API calls | `GetUnitField(guid, "health")` O(1) |
| Health Bar Colors | 4x redundant API calls per update | Uses cached `hp_orig`/`hpmax_orig` values |
| GetColor Function | `UnitHealth()` API calls | `GetUnitField(guid, "health")` O(1) |
**Fallback Support:**
- Automatic fallback to `UnitHealth()` when Nampower not available
- Automatic fallback for units >180 yards (out of Nampower range)
- Automatic fallback when GUID unavailable
### 🚀 Smart Roster Updates (No More Freeze!)
**GUID-based tracking eliminates screen freezes when swapping raid groups:**
Previously, any raid roster change would trigger a full update of ALL 40 raid frames, causing noticeable freezes. Now, only frames where the actual player changed get updated.
**How it works:**
```lua
-- OLD: RAID_ROSTER_UPDATE → ALL 40 frames update_full = true → FREEZE
-- NEW: RAID_ROSTER_UPDATE → Check GUID per frame → Only changed frames update
```
| Scenario | Before (7.2.0) | After (7.3.0) |
|----------|----------------|---------------|
| Swap 2 players | 40 frame updates | 2 frame updates |
| Player joins | 40 frame updates | 1 frame update |
| Player leaves | 40 frame updates | 1 frame update |
| No changes | 40 frame updates | 0 frame updates |
**Technical Implementation:**
- `pfUI.uf.guidTracker` tracks GUID per frame
- On roster change, compares old GUID vs new GUID
- Only sets `update_full = true` if GUID actually changed
- Also forces `update_aura = true` to refresh buffs/debuffs
### 🔧 libpredict.lua Optimizations
**Eliminated redundant `UnitName()` calls:**
- `UnitGetIncomingHeals()`: Removed double `UnitName()` call
- `UnitHasIncomingResurrection()`: Removed double `UnitName()` call
- `UNIT_HEALTH` event handler: Reuses cached name variable
---
## 🎯 What's New in Version 7.2.0 (January 24, 2026)
### 🐱 Druid Secondary Mana Bar Overhaul
**Complete rewrite using Nampower's `GetUnitField` API:**
The Druid Mana Bar feature (showing base mana while in shapeshift form) has been completely rewritten to use Nampower's native `GetUnitField` instead of the deprecated `UnitMana()` extended return values.
**Key Changes:**
| Component | Before (7.1.0) | After (7.2.0) |
|-----------|----------------|---------------|
| Data Source | `UnitMana()` second return value | `GetUnitField(guid, "power1")` |
| Player Support | ✅ Druids only | ✅ Druids only |
| Target Support | ❌ Limited/broken | ✅ All classes can see Druid mana in all forms |
| Text Settings | Hardcoded format | Respects Power Bar text config |
<img width="704" height="210" alt="grafik" src="https://i.ibb.co/bgfC04Gk/grafik.png" />
**New Features:**
- ✅ **Target Secondary Mana:** See enemy/friendly Druid's base mana while they're in Cat/Bear form
- ✅ **Respects Power Text Settings:** Uses same format as your Power Bar configuration (`powerdyn`, `power`, `powerperc`, `none`, etc.)
- ✅ **Available for ALL Classes:** Any class can now see Druid mana bars (controlled by "Show Druid Mana Bar" setting)
**Technical Implementation:**
```lua
-- OLD: Extended UnitMana (unreliable for other units)
local _, baseMana = UnitMana("target") -- Often returns nil for non-player
-- NEW: Direct field access via Nampower
local _, guid = UnitExists("target")
local baseMana = GetUnitField(guid, "power1") -- Base mana
local baseMaxMana = GetUnitField(guid, "maxPower1") -- Max base mana
```
### 🧹 Major Code Cleanup
**superwow.lua:**
- ❌ Removed legacy `pfDruidMana` bar (old SuperWoW-style implementation)
- ❌ Removed `UnitMana()` fallback code
- ✅ Unified all secondary mana bars to use `GetUnitField`
- ✅ Fixed text centering issue (was using `SetJustifyH("RIGHT")`)
**nampower.lua - Massive Cleanup:**
Removed significant amounts of dead/unused code:
| Removed Feature | Reason |
|-----------------|--------|
| Buff tracking system | Data collected but never displayed |
| HoT Detection (AURA_CAST events) | `OnHotApplied` callback never implemented |
| Swing Timer (`GetSwingTimers()`) | Never called anywhere in codebase |
| UNIT_DIED buff/debuff cleanup | Now handled by libdebuff |
**Result:** Cleaner, more maintainable code with reduced memory footprint.
---
## 🎯 What's New in Version 7.1.0 (January 24, 2026)
### ⚡ Cooldown Timer Animation Support
**Nameplate Debuff Animations:**
- ✅ Added "Show Timer Animation" option for nameplate debuffs
- ✅ Uses proper `Model` frame with `CooldownFrameTemplate` for Vanilla client
- ✅ Pie/swipe animation now works on nameplate debuff icons
- ✅ Configurable via GUI: Nameplates → Show Timer Animation
**Target Frame Debuff Animations:**
- ✅ Timer animations now properly visible on target/player frame debuffs
- ✅ Fixed CD frame scaling and positioning for correct display
- ✅ `SetScale(size/32)`, `SetAllPoints()`, `SetFrameLevel(14)` for proper rendering
**cooldown.lua Fix:**
- ✅ Added `elseif pfCooldownStyleAnimation == 1 then SetAlpha(1)` to make animations visible
- ✅ Previously animations were created but never shown (alpha stayed 0)
### 🧹 Memory Leak Fixes
**libdebuff.lua:**
- ✅ `lastCastRanks` table now cleaned up (entries older than 3 seconds removed)
- ✅ `lastFailedSpells` table now cleaned up (entries older than 2 seconds removed)
- ✅ Previously these tables grew indefinitely over long play sessions
**unitframes.lua:**
- ✅ Cache cleanup now uses in-place `= nil` instead of creating new table every 30 seconds
- ✅ Reduces garbage collector pressure
**nameplates.lua:**
- ✅ Reusable `debuffSeen` table instead of creating `local seen = {}` on every DEBUFF_UPDATE event
- ✅ Significant reduction in table allocations during combat
---
## 🎯 What's New in Version 7.0.0 (January 21, 2026)
### 🔥 Complete libdebuff.lua Rewrite (464 → 1594 lines)
**Event-Driven Architecture:**
Replaced tooltip scanning with a pure event-based system using Nampower/SuperWoW:
**OLD (Master):**
```lua
-- Every UI update:
for slot = 1, 16 do
scanner:SetUnitDebuff("target", slot) -- Tooltip scan
local name = scanner:Line(1)
end
```
**NEW (Experiment):**
```lua
-- Events fire when changes happen:
RegisterEvent("AURA_CAST_ON_SELF") -- You cast a debuff
RegisterEvent("DEBUFF_ADDED_OTHER") -- Debuff lands in slot
RegisterEvent("DEBUFF_REMOVED_OTHER") -- Debuff removed
-- UI reads from pre-computed tables:
local data = ownDebuffs[guid][spell] -- Direct lookup
```
---
### 🐱 Combo Point Finisher Support
**Dynamic Duration Calculation:**
| Ability | Formula | Durations (1-5 CP) |
|---------|---------|-------------------|
| Rip | 8s + CP × 2s | 10s / 12s / 14s / 16s / 18s |
| Rupture | 10s + CP × 2s | 12s / 14s / 16s / 18s / 20s |
| Kidney Shot | 2s + CP × 1s | 3s / 4s / 5s / 6s / 7s |
**Before:** All Rips showed 18s (wrong for 1-4 CP)
**After:** Shows actual duration based on combo points used
---
### 🎭 Carnage Talent Detection
**Ferocious Bite Refresh Mechanics:**
- Tracks Carnage talent (Rank 2) which makes Ferocious Bite refresh Rip & Rake
- Only refreshes when Ferocious Bite HITS (not on miss/dodge/parry)
- Preserves original duration (doesn't reset to new CP count)
- Uses `DidSpellFail()` API for miss detection
---
### 🔄 Additional Features
- **Debuff Overwrite Pairs:** Faerie Fire ↔ Faerie Fire (Feral), Demoralizing Shout ↔ Demoralizing Roar
- **Slot Shifting Algorithm:** Accurate icon placement when debuffs expire
- **Multi-Caster Tracking:** Multiple players' debuffs tracked separately
- **Rank Protection:** Lower rank can't overwrite higher rank timer
- **Unique Debuff System:** Hunter's Mark, Scorpid Sting, etc. handled correctly
---
## 📊 Performance Comparison
### The Core Difference: Data Access Architecture
**Master uses Blizzard API + Tooltip Scanning:**
```lua
-- Every UnitDebuff call requires tooltip scan
function libdebuff:UnitDebuff(unit, id)
local texture, stacks, dtype = UnitDebuff(unit, id)
if texture then
scanner:SetUnitDebuff(unit, id) -- Tooltip scan to get spell name
effect = scanner:Line(1)
end
-- Duration comes from hardcoded lookup tables
end
-- UnitOwnDebuff iterates all 16 slots
function libdebuff:UnitOwnDebuff(unit, id)
for i = 1, 16 do
local effect = libdebuff:UnitDebuff(unit, i) -- 16 tooltip scans!
if caster == "player" then ...
end
end
```
**Experiment uses Nampower Events + GetUnitField:**
```lua
-- Single call returns ALL 48 aura slots (32 buffs + 16 debuffs)
local auras = GetUnitField(guid, "aura") -- Returns array[48] of spell IDs
local stacks = GetUnitField(guid, "auraApplications") -- Returns array[48] of stack counts
-- Events fire with full data including duration
-- AURA_CAST_ON_OTHER: spellId, casterGuid, targetGuid, effect, effectAuraName,
-- effectAmplitude, effectMiscValue, durationMs, auraCapStatus
-- BUFF_REMOVED_OTHER: guid, slot, spellId, stackCount, auraLevel
-- UnitOwnDebuff is just a table lookup
function libdebuff:UnitOwnDebuff(unit, id)
local _, guid = UnitExists(unit)
local data = ownDebuffs[guid][spellName] -- Pre-computed by events
return data.duration, data.timeleft, ...
end
```
### Nampower Features Used (Experiment Only)
| Feature | Purpose | Data Provided |
|---------|---------|---------------|
| `GetUnitField(guid, "aura")` | Single call returns all 48 aura spell IDs | `array[48]` of spell IDs |
| `GetUnitField(guid, "auraApplications")` | Stack counts for all auras | `array[48]` of stack counts |
| `GetUnitField(guid, "power1")` | Base mana for shapeshifted Druids | Mana value (7.2.0) |
| `GetUnitField(guid, "maxPower1")` | Max base mana | Max mana value (7.2.0) |
| `AURA_CAST_ON_OTHER` | Instant debuff cast detection | spellId, casterGuid, targetGuid, **durationMs** |
| `AURA_CAST_ON_SELF` | Instant self-buff detection | Same as above |
| `BUFF_REMOVED_OTHER` | Instant aura removal detection | guid, **slot**, spellId, stackCount |
| `DEBUFF_ADDED_OTHER` | Debuff slot assignment | guid, slot, spellId, stacks |
| `DEBUFF_REMOVED_OTHER` | Debuff removal with slot info | guid, slot, spellId |
Master uses **none** of these - it relies on:
- `UnitDebuff()` API (no caster info, no duration)
- Tooltip scanning via `GameTooltip:SetUnitDebuff()` to get spell names
- Chat message parsing (`CHAT_MSG_SPELL_PERIODIC_*`) for duration detection
- Hardcoded duration lookup tables
### Performance Comparison
| Operation | Master | Experiment | Improvement |
|-----------|--------|------------|-------------|
| Initial target scan | 16 tooltip scans | 1 GetUnitField call (48 slots) | **16x fewer calls** |
| Get YOUR debuffs | Loop 16 slots + tooltip each | Direct table lookup | **~50-100x faster** |
| Debuff duration | Hardcoded tables / chat parsing | Event provides `durationMs` | **Accurate to ms** |
| Detect debuff removal | Polling / timeout | `BUFF_REMOVED_OTHER` event | **Instant** |
| Detect new debuff | Chat message delay | `AURA_CAST_ON_OTHER` event | **Instant** |
| Caster identification | Not available | Event provides `casterGuid` | **New capability** |
| Druid mana (other units) | Not available | `GetUnitField(guid, "power1")` | **New in 7.2.0** |
| Memory usage | ~50KB | ~200KB | 4x more (negligible) |
### Memory Management (7.1.0+ Fixes)
| Table | Before 7.1.0 | After 7.1.0 |
|-------|--------------|-------------|
| `lastCastRanks` | Grew indefinitely | Cleaned every 30s (>3s old) |
| `lastFailedSpells` | Grew indefinitely | Cleaned every 30s (>2s old) |
| `debuffSeen` (nameplates) | New table per DEBUFF_UPDATE | Reused single table |
| `cleanedCache` (unitframes) | New table every 30s | In-place cleanup |
---
## 📋 File Changes Summary
### Version 7.5.0
| File | Location | Changes |
|------|----------|---------|
| `buffwatch.lua` | `modules/` | Player buff bar timer fix (UUID without slot), tooltip fix for "only own debuffs" mode |
| `libdebuff.lua` | `libs/` | Immunity check, UnitDebuff() 8th return value `caster`, Lua 5.0 table limit workaround (11 tables to pfUI. namespace), Crash 132 fix, Player GUID caching |
| `unitframes.lua` | `api/` | Consistent DoNothing() pattern for lightweight cooldown frames |
| `nameplates.lua` | `modules/` | Consistent DoNothing() pattern, `childs` table reuse, indexed debuff timeout scan, quick exit optimization, Crash 132 fix |
| `nampower.lua` | `modules/` | Crash 132 fix |
| `superwow.lua` | `modules/` | Crash 132 fix |
| `actionbar.lua` | `modules/` | Crash 132 fix |
### Version 7.4.3 (January 29, 2026)
| File | Location | Changes |
|------|----------|---------|
| `libdebuff.lua` | `libs/` | Rogue PLAYER_COMBO_POINTS fix, always use GetDuration() for CP-abilities |
| `api.lua` | `api/` | Abbreviate() now supports 3 modes (off/2dec/1dec), 1dec always floors |
| `config.lua` | `api/` | Added `castbardecimals` option |
| `gui.lua` | `modules/` | Abbreviate Numbers dropdown, Castbar Timer Decimals dropdown |
| `nameplates.lua` | `modules/` | Smooth castbar (throttle fix), countdown timer, cache cleanup |
| `castbar.lua` | `modules/` | FormatCastbarTime() helper, respects castbardecimals config |
### Version 7.2.0
| File | Location | Changes |
|------|----------|---------|
| `superwow.lua` | `modules/` | Removed legacy pfDruidMana, added Target/ToT secondary mana bars, GetUnitField for all mana queries, respect Power Bar text settings |
| `nampower.lua` | `modules/` | Major cleanup: removed dead buff tracking, HoT detection, swing timer code |
### Version 7.1.0
| File | Location | Changes |
|------|----------|---------|
| `libdebuff.lua` | `libs/` | Memory leak fixes for lastCastRanks, lastFailedSpells |
| `unitframes.lua` | `api/` | In-place cache cleanup, CD frame scaling/positioning |
| `nameplates.lua` | `modules/` | Reusable debuffSeen table, Model+CooldownFrameTemplate |
| `cooldown.lua` | `modules/` | SetAlpha(1) for pfCooldownStyleAnimation == 1 |
| `config.lua` | `api/` | Added nameplates.debuffanim option |
| `gui.lua` | `modules/` | Added "Show Timer Animation" checkbox for nameplates |
---
## 📋 Installation
### Requirements
**REQUIRED:**
- SuperWoW DLL
- Nampower DLL
**Optional but Recommended:**
- UnitXP_SP3 DLL (for accurate XP tracking)
### Steps
1. Install SuperWoW + Nampower
2. Download pfUI Experiment build
3. Extract to `Interface/AddOns/pfUI`
4. `/reload`
5. Check for errors in console
### Verification
Type `/run print(GetNampowerVersion())` - should show version number.
If `nil`, Nampower is not installed correctly!
---
## 🐛 Known Issues
### Untested Scenarios
- ❌ 40-man raids with 5+ druids (slot shifting stress test)
- ❌ Rapid target swapping with Ferocious Bite spam
- ⚠️ Multi-caster tracking in AQ40/Naxx
### Edge Cases
1. **DEBUFF_ADDED race condition:** Sometimes fires before AURA_CAST_ON_SELF processes
2. **Slot shifting bugs:** Complex logic for removing/adding debuffs
3. **Combo point detection:** Relies on PLAYER_COMBO_POINTS event timing
---
## 📜 Changelog
### 7.5.0 (January 31, 2026)
**Added:**
- ✅ UnitDebuff() 8th return value: `caster` ("player"/"other"/nil)
- ✅ Immunity check - no timer/icon shown if debuff wasn't actually applied
- ✅ Buff bar tooltip correctly identifies debuff slot in "only own debuffs" mode
- ✅ `/shifttest` and `/memcheck` debug commands for libdebuff troubleshooting
**Fixed:**
- 🔧 Player Buff Bar timer reset bug (UUID no longer includes slot for player buffs)
- 🔧 Lua 5.0 local variable limit (moved 11 tables to pfUI. namespace)
- 🔧 Crash 132 on logout (Credits: jrc13245) - affects libdebuff, nameplates, nampower, superwow, actionbar
- 🔧 Consistent DoNothing() pattern across all cooldown frame creation
**Performance:**
- ⚡ `childs` table reuse in nameplate scanner (avoids GC churn)
- ⚡ Indexed access instead of `pairs()` for debuff timeout scanning
- ⚡ Quick exit if not in combat for threatcolor calculation
- ⚡ Player GUID caching in libdebuff
### 7.4.3 (January 29, 2026)
**Added:**
- ✅ Castbar Timer Decimals setting (1 or 2 decimals)
- ✅ Abbreviate Numbers dropdown (Full / 2 Decimals / 1 Decimal)
- ✅ Nameplate castbar countdown (shows remaining time)
- ✅ Cache cleanup for hidden nameplates (prevents memory leaks)
**Fixed:**
- 🔧 Rogue combo point tracking (PLAYER_COMBO_POINTS was Druid-only)
- 🔧 Kidney Shot/Rupture duration (now always uses GetDuration() for CP-abilities)
- 🔧 Nameplate castbar stuttering (throttle only affects scanner, not updates)
**Changed:**
- 🔧 Abbreviate Numbers: 1 Decimal mode always rounds DOWN (4180 → 4.1k)
- 🔧 Nameplate castbar: counts down instead of up
### 7.2.0 (January 24, 2026)
**Added:**
- ✅ Target Secondary Mana Bar (see Druid mana while in shapeshift form)
- ✅ Target-of-Target Secondary Mana Bar
- ✅ Secondary Mana Bars now respect Power Bar text settings
**Changed:**
- 🔧 Secondary Mana Bars now use `GetUnitField(guid, "power1")` instead of `UnitMana()`
- 🔧 "Show Druid Mana Bar" setting now available for ALL classes (not just Druids)
**Removed:**
- ❌ Legacy `pfDruidMana` bar (replaced by `pfPlayerSecondaryMana`)
- ❌ `UnitMana()` extended return value fallback
- ❌ Dead code in nampower.lua: buff tracking, HoT detection, swing timer
### 7.1.0 (January 24, 2026)
**Added:**
- ✅ Nameplate debuff timer animation support (pie/swipe effect)
- ✅ Target frame debuff animation improvements
- ✅ GUI option: Nameplates → Show Timer Animation
**Fixed:**
- 🔧 Memory leak: `lastCastRanks` now cleaned up (>3s old entries)
- 🔧 Memory leak: `lastFailedSpells` now cleaned up (>2s old entries)
- 🔧 Memory churn: Reusable `debuffSeen` table in nameplates
- 🔧 Memory churn: In-place cache cleanup in unitframes
- 🔧 cooldown.lua: Animation now visible when pfCooldownStyleAnimation == 1
### 7.0.0 (January 21, 2026)
**Added:**
- ✅ Event-driven debuff tracking (AURA_CAST, DEBUFF_ADDED, etc.)
- ✅ Combo point finisher support (Rip, Rupture, Kidney Shot)
- ✅ Carnage talent detection (Ferocious Bite refresh)
- ✅ Debuff overwrite pairs (Faerie Fire ↔ Faerie Fire Feral)
- ✅ Slot shifting algorithm (accurate icon placement)
- ✅ Multi-caster tracking (multiple Moonfires)
- ✅ Rank protection (Rank 1 can't overwrite Rank 10)
- ✅ Unique debuff system (Hunter's Mark, Scorpid Sting)
- ✅ Nampower GetUnitField() initial scan
- ✅ Combat indicator fix (works on player frame now)
**Changed:**
- 🔧 libdebuff.lua completely rewritten (464 → 1594 lines)
- 🔧 UnitOwnDebuff() uses table lookup instead of tooltip scan
---
## What's New in Version 6.2.6 (January 27, 2026)
@@ -476,7 +1416,7 @@ Same as original pfUI - free to use and modify.
---
**Version:** 6.2.6
**Release Date:** January 27, 2026
**Version:** 7.6.0
**Release Date:** February 3, 2026
**Compatibility:** Turtle WoW 1.18.0
**Status:** Stable
+19 -4
View File
@@ -445,18 +445,33 @@ end
-- 'number' [number] the number that should be abbreviated
-- 'returns: [string] the abbreviated value
function pfUI.api.Abbreviate(number)
if pfUI_config.unitframes.abbrevnum == "1" then
local mode = pfUI_config.unitframes.abbrevnum
-- mode "0" = disabled (full numbers)
-- mode "1" = 2 decimals (4250 -> 4.25k) [legacy/default]
-- mode "2" = 1 decimal (4250 -> 4.2k) - always rounds DOWN
if mode == "1" or mode == "2" then
local sign = number < 0 and -1 or 1
number = math.abs(number)
if number > 1000000 then
return pfUI.api.round(number/1000000*sign,2) .. "m"
if mode == "2" then
-- 1 decimal, round DOWN: 4.18m -> 4.1m
return (floor(number/100000) / 10 * sign) .. "m"
else
return pfUI.api.round(number/1000000*sign, 2) .. "m"
end
elseif number > 1000 then
return pfUI.api.round(number/1000*sign,2) .. "k"
if mode == "2" then
-- 1 decimal, round DOWN: 4180 -> 4.1k (not 4.2k)
return (floor(number/100) / 10 * sign) .. "k"
else
return pfUI.api.round(number/1000*sign, 2) .. "k"
end
end
end
return number
return math.floor(number)
end
-- [ SendChatMessageWide ]
+1 -1
View File
@@ -224,6 +224,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("unitframes", nil, "combowidth", "6")
pfUI:UpdateConfig("unitframes", nil, "comboheight", "6")
pfUI:UpdateConfig("unitframes", nil, "abbrevnum", "1")
pfUI:UpdateConfig("unitframes", nil, "castbardecimals", "2")
pfUI:UpdateConfig("unitframes", nil, "abbrevname", "1")
-- Nampower Settings
@@ -231,7 +232,6 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("unitframes", nil, "spellqueuesize", "24")
pfUI:UpdateConfig("unitframes", nil, "gcd_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "gcd_size", "4")
pfUI:UpdateConfig("unitframes", nil, "nampower_buffs", "1")
pfUI:UpdateConfig("unitframes", nil, "reactive_indicator", "0")
pfUI:UpdateConfig("unitframes", nil, "reactive_size", "28")
pfUI:UpdateConfig("unitframes", nil, "damage_tracking", "0")
+801 -212
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -700,6 +700,7 @@ pfUI_translation["zhCN"] = {
["Show Empty Buttons"] = "显示空按钮",
["Show FPS and Latency Colors"] = "显示帧数以及延迟颜色",
["Show Guild Name"] = "显示公会名称",
["Show Player Levels"] ="显示玩家等级",
["Show Happiness Icon"] = "显示高兴值图标",
["Show Health Points"] = "显示生命值",
["Show/Hide TimeManager"] = "显示/隐藏时间管理器",
+1545 -316
View File
File diff suppressed because it is too large Load Diff
+297 -53
View File
@@ -12,10 +12,27 @@ setfenv(1, pfUI:GetEnvironment())
-- The library is able to receive and send compatible messages to HealComm
-- including resurrections. It has an option to disable the sending of those
-- messages in case HealComm is already active.
--
-- HOT TRACKING INTEGRATION (NEW):
-- With Nampower enabled, HoT tracking now primarily uses libdebuff's AURA_CAST
-- event system for accurate server-side buff/debuff tracking with full rank
-- protection. GetHotDuration() first checks libdebuff, then falls back to the
-- legacy prediction system for backwards compatibility with non-Nampower clients.
-- This provides:
-- - Accurate duration from server (no prediction needed)
-- - Automatic rank protection (lower ranks won't overwrite higher ranks)
-- - Support for multiple casters of same HoT on one target
-- - Zero event overhead (libdebuff already tracks all auras)
-- return instantly when another libpredict is already active
if pfUI.api.libpredict then return end
-- Check if libdebuff integration is available
local libdebuff_available = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
-- Check if Nampower is available for SPELL_FAILED events
local hasNampower = GetNampowerVersion ~= nil
local senttarget
local heals, ress, events, hots = {}, {}, {}, {}
@@ -98,6 +115,7 @@ local libpredict = CreateFrame("Frame")
libpredict:RegisterEvent("UNIT_HEALTH")
libpredict:RegisterEvent("CHAT_MSG_ADDON")
libpredict:RegisterEvent("PLAYER_TARGET_CHANGED")
libpredict:RegisterEvent("PLAYER_LOGOUT")
-- SuperWoW: Registriere UNIT_CASTEVENT für akkurate Instant-HoT Detection
if superwow_active then
@@ -105,12 +123,23 @@ if superwow_active then
end
libpredict:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then
-- Ignore own messages (sender receives own addon messages)
local playerName = UnitName("player")
if arg4 == playerName then return end
this:ParseChatMessage(arg4, arg2, arg1)
elseif event == "UNIT_HEALTH" then
local name = UnitName(arg1)
if ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[UnitName(arg1)] = nil
if name and ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[name] = nil -- Reuse 'name' variable instead of calling UnitName again
end
elseif event == "UNIT_CASTEVENT" and superwow_active then
-- arg1 = casterGUID, arg2 = targetGUID, arg3 = event type, arg4 = spellId, arg5 = castTime
@@ -174,23 +203,38 @@ libpredict:SetScript("OnEvent", function()
duration = renewDuration or 15
end
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[UNIT_CASTEVENT]|r spell=%s target=%s dur=%s", hotType, targetName, tostring(duration)))
-- Extract rank from spellId (if SpellInfo available)
local rank = nil
if SpellInfo then
local _, rankString = SpellInfo(spellId)
if rankString and rankString ~= "" then
rank = tonumber((string.gsub(rankString, "Rank ", ""))) or nil
end
end
-- Sende HoT
local playerName = UnitName("player")
libpredict:Hot(playerName, targetName, hotType, duration, nil, "UNIT_CASTEVENT")
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[UNIT_CASTEVENT]|r spell=%s target=%s dur=%s rank=%s",
hotType, targetName, tostring(duration), tostring(rank or "?")))
end
-- Sende HealComm Nachricht (sender könnte noch nicht existieren bei sehr frühem Event)
-- Sende HoT mit Rank
local playerName = UnitName("player")
libpredict:Hot(playerName, targetName, hotType, duration, nil, "UNIT_CASTEVENT", rank)
-- Sende HealComm Nachricht mit Rank (backwards compatible: rank optional)
-- Use "0" for unknown rank instead of empty string to avoid parsing issues
local rankStr = rank and tostring(rank) or "0"
if libpredict.sender and libpredict.sender.SendHealCommMsg then
libpredict.sender:SendHealCommMsg(hotType .. "/" .. targetName .. "/" .. duration .. "/")
libpredict.sender:SendHealCommMsg(hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/")
else
-- Fallback: direkt senden
local msg = hotType .. "/" .. targetName .. "/" .. duration .. "/"
SendAddonMessage("HealComm", msg, "PARTY")
SendAddonMessage("HealComm", msg, "RAID")
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
-- Fallback: direkt senden (smart channel selection)
local msg = hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/"
if GetNumRaidMembers() > 0 then
SendAddonMessage("HealComm", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
SendAddonMessage("HealComm", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
end
end
end)
@@ -210,10 +254,14 @@ libpredict:SetScript("OnUpdate", function()
end)
function libpredict:ParseComm(sender, msg)
local msgtype, target, heal, time
local msgtype, target, heal, time, rank
if msg == "Healstop" or msg == "GrpHealstop" then
if msg == "HealStop" or msg == "GrpHealstop" then
msgtype = "Stop"
-- DEBUG: Log when HealStop received
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[libpredict RX]|r HealStop from " .. tostring(sender))
end
elseif msg == "Resurrection/stop/" then
msgtype = "RessStop"
elseif msg then
@@ -242,6 +290,13 @@ function libpredict:ParseComm(sender, msg)
if msgobj[1] == "Reju" or msgobj[1] == "Renew" or msgobj[1] == "Regr" then --hots
msgtype, target, heal, time = "Hot", msgobj[2], msgobj[1], msgobj[3]
-- NEW: Parse rank (optional, backwards compatible)
-- Format: "Reju/Target/12/10/" where msgobj[3]=duration, msgobj[4]=rank
-- "0" = unknown rank (for clients without rank extraction)
local rankStr = msgobj[4]
if rankStr and rankStr ~= "" and rankStr ~= "/" and rankStr ~= "0" then
rank = tonumber(rankStr)
end
end
elseif select and UnitCastingInfo then
-- latest healcomm
@@ -271,7 +326,7 @@ function libpredict:ParseComm(sender, msg)
end
end
return msgtype, target, heal, time
return msgtype, target, heal, time, rank
end
-- Duplikat-Erkennung für HoT Nachrichten
@@ -279,10 +334,10 @@ local recentHots = {}
local DUPLICATE_WINDOW = 0.5 -- Ignoriere gleiche Nachricht innerhalb 0.5s
function libpredict:ParseChatMessage(sender, msg, comm)
local msgtype, target, heal, time
local msgtype, target, heal, time, rank
if comm == "HealComm" then
msgtype, target, heal, time = libpredict:ParseComm(sender, msg)
msgtype, target, heal, time, rank = libpredict:ParseComm(sender, msg)
elseif comm == "CTRA" then
local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)")
if cmd and ctratarget and cmd == "RES" and ctratarget ~= "" and ctratarget ~= UNKNOWN then
@@ -344,10 +399,10 @@ function libpredict:ParseChatMessage(sender, msg, comm)
local delay = (heal == "Regr") and 0.3 or 0
local correctedStart = now - delay
libpredict:Hot(sender, target, heal, time, correctedStart, "ParseComm-Self")
libpredict:Hot(sender, target, heal, time, correctedStart, "ParseComm-Self", rank)
return
end
libpredict:Hot(sender, target, heal, time, nil, "ParseComm")
libpredict:Hot(sender, target, heal, time, nil, "ParseComm", rank)
end
end
@@ -371,7 +426,7 @@ end
-- Debug flag
libpredict.debug = false
function libpredict:Hot(sender, target, spell, duration, startTime, source)
function libpredict:Hot(sender, target, spell, duration, startTime, source, rank)
hots[target] = hots[target] or {}
hots[target][spell] = hots[target][spell] or {}
@@ -382,10 +437,30 @@ function libpredict:Hot(sender, target, spell, duration, startTime, source)
-- Sicherstellen dass duration eine Zahl ist
duration = tonumber(duration) or duration
-- Rank protection: Don't overwrite higher rank HoT with lower rank
local existing = hots[target][spell]
if existing and existing.rank and rank then
local existingRank = tonumber(existing.rank) or 0
local newRank = tonumber(rank) or 0
local now = pfUI.uf.now or GetTime()
local existingTimeleft = (existing.start + existing.duration) - now
-- If existing HoT is still active and has higher rank, don't overwrite
if existingTimeleft > 0 and newRank > 0 and newRank < existingRank then
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[Hot RANK BLOCK]|r %s Rank %d cannot overwrite Rank %d on %s",
spell, newRank, existingRank, target))
end
return -- Don't overwrite!
end
end
local now = pfUI.uf.now or GetTime()
hots[target][spell].duration = duration
hots[target][spell].start = startTime or now
hots[target][spell].rank = rank -- Store rank for protection
-- Debug
if libpredict.debug then
@@ -393,7 +468,8 @@ function libpredict:Hot(sender, target, spell, duration, startTime, source)
" | sender=" .. (sender or "nil") ..
" | target=" .. (target or "nil") ..
" | spell=" .. (spell or "nil") ..
" | dur=" .. tostring(duration) .. " (" .. type(duration) .. ")")
" | dur=" .. tostring(duration) .. " (" .. type(duration) .. ")" ..
" | rank=" .. tostring(rank or "?"))
end
-- update aura events of relevant unitframes
@@ -447,9 +523,10 @@ function libpredict:RessStop(sender)
end
function libpredict:UnitGetIncomingHeals(unit)
if not unit or not UnitName(unit) then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
if not unit then return 0 end
local name = UnitName(unit)
if not name then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
local sumheal = 0
if not heals[name] then
@@ -468,8 +545,9 @@ function libpredict:UnitGetIncomingHeals(unit)
end
function libpredict:UnitHasIncomingResurrection(unit)
if not unit or not UnitName(unit) then return nil end
if not unit then return nil end
local name = UnitName(unit)
if not name then return nil end
if not ress[name] then
return nil
@@ -587,6 +665,12 @@ hooksecurefunc("CastSpell", function(id, bookType)
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant-HoTs: Mit SuperWoW nutzen wir UNIT_CASTEVENT (akkurater)
-- Ohne SuperWoW: Fallback auf Hook-Methode mit Cooldown
if superwow_active then return end
@@ -603,10 +687,11 @@ hooksecurefunc("CastSpell", function(id, bookType)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell REJU INSTANT]|r target=%s (Fallback)", target))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell REJU INSTANT]|r target=%s rank=%s (Fallback)", target, tostring(rankNum or "?")))
end
libpredict:Hot(player, target, "Reju", rejuvDuration, nil, "CastSpell-Instant")
libpredict.sender:SendHealCommMsg("Reju/"..target.."/"..rejuvDuration.."/")
libpredict:Hot(player, target, "Reju", rejuvDuration, nil, "CastSpell-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..target.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local target = spell_queue[3]
local now = pfUI.uf.now or GetTime()
@@ -619,10 +704,11 @@ hooksecurefunc("CastSpell", function(id, bookType)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell RENEW INSTANT]|r target=%s (Fallback)", target))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpell RENEW INSTANT]|r target=%s rank=%s (Fallback)", target, tostring(rankNum or "?")))
end
libpredict:Hot(player, target, "Renew", renewDuration, nil, "CastSpell-Instant")
libpredict.sender:SendHealCommMsg("Renew/"..target.."/"..renewDuration.."/")
libpredict:Hot(player, target, "Renew", renewDuration, nil, "CastSpell-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..target.."/"..renewDuration.."/"..rankStr.."/")
end
end)
@@ -639,6 +725,12 @@ hooksecurefunc("CastSpellByName", function(effect, target)
target = target and target == true and UnitName("player") or target
target = target and target == 1 and UnitName("player") or target
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Nur spell_queue überschreiben wenn kein Cast läuft
-- (verhindert dass Instant-Spam während Regrowth-Cast die Queue zerstört)
if not libpredict.sender.current_cast then
@@ -663,10 +755,11 @@ hooksecurefunc("CastSpellByName", function(effect, target)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName REJU INSTANT]|r target=%s (Fallback)", hotTarget))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName REJU INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "CastSpellByName-Instant")
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/")
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "CastSpellByName-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local hotTarget = target or mouseover or default
local now = pfUI.uf.now or GetTime()
@@ -679,10 +772,11 @@ hooksecurefunc("CastSpellByName", function(effect, target)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName RENEW INSTANT]|r target=%s (Fallback)", hotTarget))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CastSpellByName RENEW INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "CastSpellByName-Instant")
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/")
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "CastSpellByName-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/"..rankStr.."/")
end
end)
@@ -697,6 +791,12 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
-- Extract rank number
local rankNum = nil
if rank and rank ~= "" then
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant-HoTs: Mit SuperWoW nutzen wir UNIT_CASTEVENT (akkurater)
-- Ohne SuperWoW: Fallback auf Hook-Methode mit Cooldown
if superwow_active then return end
@@ -713,10 +813,11 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction REJU INSTANT]|r target=%s (Fallback)", hotTarget))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction REJU INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "UseAction-Instant")
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/")
libpredict:Hot(player, hotTarget, "Reju", rejuvDuration, nil, "UseAction-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Reju/"..hotTarget.."/"..rejuvDuration.."/"..rankStr.."/")
elseif effect == RENEW then
local hotTarget = spell_queue[3]
local now = pfUI.uf.now or GetTime()
@@ -729,24 +830,39 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
instantHotCooldown[key] = now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction RENEW INSTANT]|r target=%s (Fallback)", hotTarget))
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UseAction RENEW INSTANT]|r target=%s rank=%s (Fallback)", hotTarget, tostring(rankNum or "?")))
end
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "UseAction-Instant")
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/")
libpredict:Hot(player, hotTarget, "Renew", renewDuration, nil, "UseAction-Instant", rankNum)
local rankStr = rankNum and tostring(rankNum) or "0"
libpredict.sender:SendHealCommMsg("Renew/"..hotTarget.."/"..renewDuration.."/"..rankStr.."/")
end
end)
libpredict.sender = CreateFrame("Frame", "pfPredictionSender", UIParent)
libpredict.sender.enabled = true
libpredict.sender.SendHealCommMsg = function(self, msg)
SendAddonMessage("HealComm", msg, "PARTY")
SendAddonMessage("HealComm", msg, "RAID")
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
-- Smart channel selection: Only send to relevant channel to avoid duplicates
if GetNumRaidMembers() > 0 then
-- In raid: Only send to RAID (includes all raid members)
SendAddonMessage("HealComm", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
-- In party: Only send to PARTY
SendAddonMessage("HealComm", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
-- BG groups are handled by RAID channel
end
libpredict.sender.SendResCommMsg = function(self, msg)
SendAddonMessage("CTRA", msg, "PARTY")
SendAddonMessage("CTRA", msg, "RAID")
SendAddonMessage("CTRA", msg, "BATTLEGROUND")
-- Smart channel selection: Only send to relevant channel to avoid duplicates
if GetNumRaidMembers() > 0 then
-- In raid: Only send to RAID (includes all raid members)
SendAddonMessage("CTRA", msg, "RAID")
elseif GetNumPartyMembers() > 0 then
-- In party: Only send to PARTY
SendAddonMessage("CTRA", msg, "PARTY")
end
-- Note: BATTLEGROUND channel not used (no reliable way to detect BG in Vanilla)
-- BG groups are handled by RAID channel
end
libpredict.sender:SetScript("OnUpdate", function()
@@ -756,15 +872,19 @@ libpredict.sender:SetScript("OnUpdate", function()
local target = this.regrowth_target or player
local duration = 20
local startTime = this.regrowth_start
local rank = this.regrowth_rank
libpredict:Hot(player, target, "Regr", duration, startTime, "OnUpdate")
libpredict.sender:SendHealCommMsg("Regr/"..target.."/"..duration.."/")
libpredict:Hot(player, target, "Regr", duration, startTime, "OnUpdate", rank)
local rankStr = rank and tostring(rank) or "0"
libpredict.sender:SendHealCommMsg("Regr/"..target.."/"..duration.."/"..rankStr.."/")
-- Übernehme nächsten Regrowth falls vorhanden
this.regrowth_target = this.regrowth_target_next
this.regrowth_start = this.regrowth_start_next
this.regrowth_rank = this.regrowth_rank_next
this.regrowth_target_next = nil
this.regrowth_start_next = nil
this.regrowth_rank_next = nil
this.regrowth_timer = nil
end
end)
@@ -776,6 +896,18 @@ libpredict.sender:RegisterEvent("SPELLCAST_FAILED")
libpredict.sender:RegisterEvent("SPELLCAST_INTERRUPTED")
libpredict.sender:RegisterEvent("SPELLCAST_DELAYED")
-- Nampower: Register SPELL_FAILED_SELF for more reliable cast fail detection
if hasNampower then
libpredict.sender:RegisterEvent("SPELL_FAILED_SELF")
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[libpredict]|r Nampower detected - SPELL_FAILED_SELF registered")
end
else
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00[libpredict]|r Nampower NOT detected - using vanilla SPELLCAST_FAILED only")
end
end
-- force cache updates
libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED")
libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED")
@@ -819,10 +951,17 @@ libpredict.sender:SetScript("OnEvent", function()
local casttime = time
if spell == REGROWTH then
-- Extract rank from spell_queue[2] which contains "spell + rank"
local fullSpell = spell_queue[2]
local rankStr = fullSpell and string.match(fullSpell, "Rank (%d+)") or nil
local rankNum = rankStr and tonumber(rankStr) or nil
if this.regrowth_timer then
this.regrowth_target_next = spell_queue[3]
this.regrowth_rank_next = rankNum
else
this.regrowth_target = spell_queue[3]
this.regrowth_rank = rankNum
end
end
@@ -852,6 +991,11 @@ libpredict.sender:SetScript("OnEvent", function()
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
if libpredict.sender.healing then
libpredict:HealStop(player)
-- DEBUG: Log when sending HealStop
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[libpredict TX]|r Sending HealStop (via " .. event .. ") to group")
end
libpredict.sender:SendHealCommMsg("HealStop")
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
@@ -871,6 +1015,35 @@ libpredict.sender:SetScript("OnEvent", function()
-- Cleanup
this.current_cast = nil
this.current_cast_target = nil
elseif event == "SPELL_FAILED_SELF" then
-- Nampower SPELL_FAILED_SELF: More reliable than vanilla SPELLCAST_FAILED
-- Same cleanup as SPELLCAST_FAILED
if libpredict.sender.healing then
libpredict:HealStop(player)
-- DEBUG: Log when sending HealStop
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[libpredict TX]|r Sending HealStop to group (SPELL_FAILED_SELF)")
end
libpredict.sender:SendHealCommMsg("HealStop")
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
local target = senttarget or spell_queue[3]
libpredict:RessStop(player)
libpredict.sender:SendHealCommMsg("Resurrection/stop/")
libpredict.sender:SendResCommMsg("RESNO " .. target)
libpredict.sender.resurrecting = nil
end
-- Regrowth cleanup
if this.current_cast == REGROWTH then
this.regrowth_timer = nil
this.regrowth_start = nil
this.regrowth_target_next = nil
this.regrowth_start_next = nil
end
-- Cleanup
this.current_cast = nil
this.current_cast_target = nil
elseif event == "SPELLCAST_DELAYED" then
if libpredict.sender.healing then
libpredict:HealDelay(player, arg1)
@@ -900,7 +1073,37 @@ end)
function libpredict:GetHotDuration(unit, spell)
if unit == UNKNOWNOBJECT or unit == UNKOWNBEING then return end
-- NEW: Try libdebuff first (Nampower AURA_CAST events)
if pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast then
local _, guid = UnitExists(unit) -- FIX: Get GUID, not exists boolean!
if guid then
-- Get the best (highest rank) aura cast for this spell
local spellName = spell
-- Map short spell codes to full names
if spell == "Reju" then
spellName = REJUVENATION
elseif spell == "Regr" then
spellName = REGROWTH
elseif spell == "Renew" then
spellName = RENEW
end
local start, duration, timeleft, rank, casterGuid = pfUI.api.libdebuff:GetBestAuraCast(guid, spellName)
if start and duration and timeleft then
-- SUCCESS: libdebuff has accurate server-side data!
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[GetHotDuration]|r %s on %s via libdebuff: dur=%.1fs timeleft=%.1fs rank=%d",
spell, unit, duration, timeleft, rank or 0))
end
return start, duration, timeleft
end
end
end
-- FALLBACK: Use old prediction system (for non-Nampower clients or no AURA_CAST data)
local start, duration, timeleft
local now = pfUI.uf.now or GetTime()
@@ -915,6 +1118,11 @@ function libpredict:GetHotDuration(unit, spell)
start = spellData.start
duration = spellData.duration
timeleft = endTime - now
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff9900[GetHotDuration]|r %s on %s via prediction: dur=%.1fs timeleft=%.1fs",
spell, unit, duration, timeleft))
end
end
end
end
@@ -922,4 +1130,40 @@ function libpredict:GetHotDuration(unit, spell)
return start, duration, timeleft
end
-- Debug command: /hotdebug - Show HoT tracking status
_G.SLASH_HOTDEBUG1 = "/hotdebug"
_G.SlashCmdList.HOTDEBUG = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[HoT Tracking Debug]|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
-- Check libdebuff availability
local libdebuff_now = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
if libdebuff_now then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[PRIMARY]|r libdebuff integration: ACTIVE")
DEFAULT_CHAT_FRAME:AddMessage(" Using AURA_CAST events for server-side tracking")
DEFAULT_CHAT_FRAME:AddMessage(" Rank protection: ENABLED")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[PRIMARY]|r libdebuff integration: NOT AVAILABLE")
DEFAULT_CHAT_FRAME:AddMessage(" Reason: Nampower not enabled or libdebuff outdated")
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[FALLBACK]|r Legacy prediction system: ACTIVE")
DEFAULT_CHAT_FRAME:AddMessage(" Using UNIT_CASTEVENT + HealComm messages")
-- Show active HoTs in tracking
local hotCount = 0
for target, spells in pairs(hots) do
for spell, data in pairs(spells) do
hotCount = hotCount + 1
end
end
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ffff[TRACKED]|r %d HoTs in legacy system", hotCount))
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
DEFAULT_CHAT_FRAME:AddMessage("Tip: /libpredict.debug = true for verbose logging")
end
pfUI.api.libpredict = libpredict
+15 -3
View File
@@ -668,7 +668,10 @@ pfUI:RegisterModule("actionbar", "vanilla", function ()
start, duration, enable = GetActionCooldown(button.id)
end
CooldownFrame_SetTimer(button.cd, start, duration, enable)
-- Nil-protect: GetActionCooldown can return nil during macro parsing/indexing
if start and duration then
CooldownFrame_SetTimer(button.cd, start, duration, enable or 1)
end
end
local _, active
@@ -982,7 +985,16 @@ pfUI:RegisterModule("actionbar", "vanilla", function ()
pageswitch:RegisterEvent("PLAYER_AURAS_CHANGED")
pageswitch:RegisterEvent("PLAYER_ENTERING_WORLD")
pageswitch:RegisterEvent("UNIT_CASTEVENT")
pageswitch:RegisterEvent("PLAYER_LOGOUT")
pageswitch:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
this:SetScript("OnUpdate", nil)
return
end
if class ~= "DRUID" then return end
-- On login/reload: full scan
@@ -1230,8 +1242,8 @@ pfUI:RegisterModule("actionbar", "vanilla", function ()
f.count:SetJustifyH("RIGHT")
f.count:SetJustifyV("BOTTOM")
-- macro spell scan
if C.bars.macroscan == "0" then
-- macro spell scan (disabled when macro addons are loaded)
if C.bars.macroscan == "0" or pfUI:MacroAddonsLoaded() then
f.scanmacro, f.spellslot, f.booktype = nil, nil, nil
else
f.scanmacro = true
+116 -26
View File
@@ -76,8 +76,55 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
end
else
buff:Hide()
return
-- Fallback: try UnitBuff/UnitDebuff API which may be more reliable in some cases
local fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId
local maxSlots = buff.btype == "HELPFUL" and 32 or 16
if buff.id >= 1 and buff.id <= maxSlots then
if buff.btype == "HELPFUL" and C.buffs.buffs == "1" then
for i = 1, maxSlots do
local tex, stacks, dtype, spellId = UnitBuff("player", i)
if tex and i == buff.id then
fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId = tex, stacks, dtype, spellId
break
end
if not tex then break end
end
elseif buff.btype == "HARMFUL" and C.buffs.debuffs == "1" then
for i = 1, maxSlots do
local tex, stacks, dtype, spellId = UnitDebuff("player", i)
if tex and i == buff.id then
fallbackTexture, fallbackStacks, fallbackDispelType, fallbackSpellId = tex, stacks, dtype, spellId
break
end
if not tex then break end
end
end
end
if fallbackTexture then
buff.mode = buff.btype
buff.fallbackSpellId = fallbackSpellId
buff.texture:SetTexture(fallbackTexture)
if buff.btype == "HARMFUL" then
if fallbackDispelType == "Magic" then
buff.backdrop:SetBackdropBorderColor(0,1,1,1)
elseif fallbackDispelType == "Poison" then
buff.backdrop:SetBackdropBorderColor(0,1,0,1)
elseif fallbackDispelType == "Curse" then
buff.backdrop:SetBackdropBorderColor(1,0,1,1)
elseif fallbackDispelType == "Disease" then
buff.backdrop:SetBackdropBorderColor(1,1,0,1)
else
buff.backdrop:SetBackdropBorderColor(1,0,0,1)
end
else
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
end
else
buff:Hide()
return
end
end
buff:Show()
@@ -119,30 +166,8 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
buff.btype = btype
buff.gid = i
buff:SetScript("OnUpdate", function()
if not this.next then this.next = GetTime() + .1 end
if this.next > GetTime() then return end
this.next = GetTime() + .1
local timeleft = 0
local stacks = 0
if this.mode == this.btype then
timeleft = GetPlayerBuffTimeLeft(this.bid, this.btype)
stacks = GetPlayerBuffApplications(this.bid, this.btype)
elseif this.mode == "MAINHAND" then
local _, mhtime, mhcharge = GetWeaponEnchantInfo()
timeleft = mhtime/1000
stacks = mhcharge
elseif this.mode == "OFFHAND" then
local _, _, _, _, ohtime, ohcharge = GetWeaponEnchantInfo()
timeleft = ohtime/1000
stacks = ohcharge
end
this.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
this.stacks:SetText(stacks > 1 and stacks or "")
end)
-- PERF: OnUpdate moved to consolidated parent frame handler (see pfUI.buff:SetScript("OnUpdate"))
-- Individual buff frames no longer have their own OnUpdate
buff:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
@@ -251,6 +276,71 @@ pfUI:RegisterModule("buff", "vanilla:tbc", function ()
end
end)
-- PERF: Consolidated OnUpdate handler for all buff timers
-- This replaces 50 individual OnUpdate handlers with a single one
pfUI.buff:SetScript("OnUpdate", function()
local now = GetTime()
if not this.nextUpdate then this.nextUpdate = now + 0.1 end
if this.nextUpdate > now then return end
this.nextUpdate = now + 0.1
-- Cache weapon enchant info once per update cycle
local mh, mhtime, mhcharge, oh, ohtime, ohcharge = GetWeaponEnchantInfo()
-- Update all visible buff buttons
local buttons = pfUI.buff.buffs.buttons
for i = 1, 32 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == buff.btype then
timeleft = GetPlayerBuffTimeLeft(buff.bid, buff.btype)
stacks = GetPlayerBuffApplications(buff.bid, buff.btype)
elseif buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update all visible debuff buttons
buttons = pfUI.buff.debuffs.buttons
for i = 1, 16 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft = GetPlayerBuffTimeLeft(buff.bid, buff.btype)
local stacks = GetPlayerBuffApplications(buff.bid, buff.btype)
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
-- Update weapon buff buttons if separate
if C.buffs.separateweapons == "1" then
buttons = pfUI.buff.wepbuffs.buttons
for i = 1, 2 do
local buff = buttons[i]
if buff:IsShown() then
local timeleft, stacks = 0, 0
if buff.mode == "MAINHAND" then
timeleft = mhtime and mhtime / 1000 or 0
stacks = mhcharge or 0
elseif buff.mode == "OFFHAND" then
timeleft = ohtime and ohtime / 1000 or 0
stacks = ohcharge or 0
end
buff.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "")
buff.stacks:SetText(stacks > 1 and stacks or "")
end
end
end
end)
-- Weapon Buffs
pfUI.buff.wepbuffs = CreateFrame("Frame", "pfWepBuffFrame", UIParent)
pfUI.buff.wepbuffs.count = 0
+25 -2
View File
@@ -125,7 +125,23 @@ pfUI:RegisterModule("buffwatch", "vanilla:tbc", function ()
if this.unit == "player" then
GameTooltip:SetPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,this.type))
elseif this.type == "HARMFUL" then
GameTooltip:SetUnitDebuff(this.unit, this.id)
-- For "only own debuffs" mode: find the REAL slot by matching spell name AND caster
local config = this.parent and this.parent.config
if config and config.selfdebuff == "1" and libdebuff then
local ownDebuffName = libdebuff:UnitOwnDebuff(this.unit, this.id)
if ownDebuffName then
-- Search through all game slots to find OUR debuff with matching name
for gameSlot = 1, 16 do
local gameName, _, _, _, _, _, _, gameCaster = libdebuff:UnitDebuff(this.unit, gameSlot)
if gameName == ownDebuffName and gameCaster == "player" then
GameTooltip:SetUnitDebuff(this.unit, gameSlot)
break
end
end
end
else
GameTooltip:SetUnitDebuff(this.unit, this.id)
end
elseif this.type == "HELPFUL" then
GameTooltip:SetUnitBuff(this.unit, this.id)
end
@@ -261,7 +277,14 @@ pfUI:RegisterModule("buffwatch", "vanilla:tbc", function ()
and data[3] and data[3] ~= "" -- buff has a name
and data[4] and data[4] ~= "" -- buff has a texture
then
local uuid = data[4] .. data[3] -- we use that to cache some values for buffs
-- For player: no slot in uuid (slots shift when other buffs expire)
-- For target: include slot (multiple players can have same debuff, slot identifies who)
local uuid
if frame.unit == "player" then
uuid = data[4] .. data[3] -- texture + name only
else
uuid = data[4] .. data[3] .. data[2] -- texture + name + slot
end
-- update bar data
frame.bars[bar] = frame.bars[bar] or CreateStatusBar(bar, frame)
+14 -3
View File
@@ -6,6 +6,17 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
local rawborder, default_border = GetBorderSize("unitframes")
local cbtexture = pfUI.media[C.appearance.castbar.texture]
-- Helper function for castbar timer formatting
local function FormatCastbarTime(value)
if C.unitframes.castbardecimals == "1" then
-- 1 decimal, always floor
return string.format("%.1f", floor(value * 10) / 10)
else
-- 2 decimals (default)
return string.format("%.2f", value)
end
end
local function CreateCastbar(name, parent, unitstr, unitname)
local cb = CreateFrame("Frame", name, parent or UIParent)
@@ -165,10 +176,10 @@ pfUI:RegisterModule("castbar", "vanilla", function ()
if this.showtimer then
if this.delay and this.delay > 0 then
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. round(this.delay,2) .. " |r "
this.bar.right:SetText(delay .. string.format("%.2f",cur) .. " / " .. round(max,2))
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
this.bar.right:SetText(delay .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
else
this.bar.right:SetText(string.format("%.2f",cur) .. " / " .. round(max,2))
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
end
end
+23 -10
View File
@@ -891,6 +891,15 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
"7:" .. T["Small"],
"8:" .. T["Tiny (PixelPerfect)"],
},
["abbrevnum"] = {
"0:" .. T["Full Numbers (4250)"],
"1:" .. T["Abbreviate 2 Decimals (4.25k)"],
"2:" .. T["Abbreviate 1 Decimal (4.2k)"],
},
["castbardecimals"] = {
"1:" .. T["1 Decimal (2.1)"],
"2:" .. T["2 Decimals (2.14)"],
},
["orientation"] = {
"HORIZONTAL:" .. T["Horizontal"],
"VERTICAL:" .. T["Vertical"],
@@ -1378,7 +1387,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
donate:SetHeight(20)
donate:SetText(T["Donate"])
donate:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://ko-fi.com/shagu")
pfUI.chat.urlcopy.CopyText("https://github.com/me0wg4ming/pfUI")
end)
SkinButton(donate)
@@ -1388,7 +1397,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
github:SetHeight(20)
github:SetText(T["GitHub"])
github:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://github.com/shagu/pfUI")
pfUI.chat.urlcopy.CopyText("https://github.com/me0wg4ming/pfUI")
end)
SkinButton(github)
@@ -1398,7 +1407,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
website:SetHeight(20)
website:SetText(T["Website"])
website:SetScript("OnClick", function()
pfUI.chat.urlcopy.CopyText("https://shagu.org/pfUI")
pfUI.chat.urlcopy.CopyText("https://github.com/me0wg4ming/pfUI")
end)
SkinButton(website)
end)
@@ -1496,7 +1505,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Disable Errors in UIErrors Frame"], C.global, "errors_hide", "checkbox")
CreateConfig(nil, T["Highlight Settings That Require Reload"], C.gui, "reloadmarker", "checkbox")
CreateConfig(nil, T["Show Incompatible Config Entries"], C.gui, "showdisabled", "checkbox")
CreateConfig(nil, T["Abbreviate Numbers (4200 -> 4.2k)"], C.unitframes, "abbrevnum", "checkbox")
CreateConfig(nil, T["Abbreviate Numbers"], C.unitframes, "abbrevnum", "dropdown", pfUI.gui.dropdowns.abbrevnum)
CreateConfig(nil, T["Castbar Timer Decimals"], C.unitframes, "castbardecimals", "dropdown", pfUI.gui.dropdowns.castbardecimals)
CreateConfig(nil, T["Abbreviate Unit Names"], C.unitframes, "abbrevname", "checkbox")
CreateConfig(nil, T["Health Point Estimation"], nil, nil, "header")
CreateConfig(nil, T["Estimate Enemy Health Points"], C.global, "libhealth", "checkbox")
@@ -1669,7 +1679,6 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(nil, T["Spell Queue Icon Size"], C.unitframes, "spellqueuesize", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Show Reactive Spell Indicator"], C.unitframes, "reactive_indicator", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Reactive Indicator Size"], C.unitframes, "reactive_size", nil, nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["Enhanced Buff Tracking"], C.unitframes, "nampower_buffs", "checkbox", nil, nil, nil, nil, "vanilla" )
CreateConfig(nil, T["UnitXP Settings"], nil, nil, "header")
CreateConfig(nil, T["Show Line of Sight Indicator"], C.unitframes, "los_indicator", "checkbox", nil, nil, nil, nil, "vanilla" )
@@ -1831,7 +1840,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(U[c], T["Timer"], nil, nil, "header")
CreateConfig(U[c], T["Show Timer Text"], C.unitframes[c], "cooldown_text", "checkbox")
CreateConfig(U[c], T["Show Timer Animation"], C.unitframes[c], "cooldown_anim", "checkbox")
CreateConfig(Reload, T["Show Timer Animation"], C.unitframes[c], "cooldown_anim", "checkbox")
CreateConfig(U[c], T["Buffs"], nil, nil, "header")
CreateConfig(U[c], T["Buff Position"], C.unitframes[c], "buffs", "dropdown", pfUI.gui.dropdowns.uf_buff_position)
@@ -2032,7 +2041,9 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(U["bars"], T["Button Animation"], C.bars, "animation", "dropdown", pfUI.gui.dropdowns.actionbuttonanimations)
CreateConfig(U["bars"], T["Button Animation Trigger"], C.bars, "animmode", "dropdown", pfUI.gui.dropdowns.animationmode)
CreateConfig(U["bars"], T["Show Animation On Hidden Bars"], C.bars, "animalways", "checkbox")
CreateConfig(U["bars"], T["Scan Macros For Spells"], C.bars, "macroscan", "checkbox", nil, nil, nil, nil, "vanilla")
if not pfUI:MacroAddonsLoaded() then
CreateConfig(U["bars"], T["Scan Macros For Spells"], C.bars, "macroscan", "checkbox", nil, nil, nil, nil, "vanilla")
end
CreateConfig(U["bars"], T["Show Reagent Count"], C.bars, "reagents", "checkbox")
CreateConfig(U["bars"], T["Highlight Equipped Items"], C.bars, "showequipped", "checkbox")
CreateConfig(U["bars"], T["Equipped Item Color"], C.bars, "eqcolor", "color")
@@ -2357,7 +2368,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateConfig(U["nameplates"], T["Show Debuff Stacks"], C.nameplates.debuffs, "showstacks", "checkbox")
CreateConfig(U["nameplates"], T["Enable Debuff Timers"], C.nameplates, "debufftimers", "checkbox")
CreateConfig(U["nameplates"], T["Show Timer Text"], C.nameplates, "debufftext", "checkbox")
CreateConfig(nil, T["Show Timer Animation"], C.nameplates, "debuffanim", "checkbox")
CreateConfig(Reload, T["Show Timer Animation"], C.nameplates, "debuffanim", "checkbox")
CreateConfig(U["nameplates"], T["Only Show Own Debuffs (|cffffaaaaExperimental|r)"], C.nameplates, "selfdebuff", "checkbox")
CreateConfig(U["nameplates"], T["Filter Mode"], C.nameplates.debuffs, "filter", "dropdown", pfUI.gui.dropdowns.buffbarfilter)
CreateConfig(U["nameplates"], T["Blacklist"], C.nameplates.debuffs, "blacklist", "list")
@@ -2448,7 +2460,8 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
CreateGUIEntry(T["Components"], T["Modules"], function()
table.sort(pfUI.modules)
for i,m in pairs(pfUI.modules) do
if m ~= "gui" then
-- skip gui and macrotweak when macro addons are loaded
if m ~= "gui" and not (m == "macrotweak" and pfUI:MacroAddonsLoaded()) then
-- create disabled entry if not existing and display
pfUI:UpdateConfig("disabled", nil, m, "0")
CreateConfig(nil, T["Disable Module"] .. " " .. m, C.disabled, m, "checkbox")
@@ -2465,4 +2478,4 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function ()
end
end)
end
end)
end)
+2 -2
View File
@@ -1,6 +1,6 @@
pfUI:RegisterModule("macrotweak", "vanilla", function ()
-- disable macrotweak when SuperCleveRoidMacros is loaded
if IsAddOnLoaded("SuperCleveRoidMacros") then return end
-- disable macrotweak when macro addons are loaded
if IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros") then return end
-- do not write macro calls into chat input history
if ChatFrameEditBox._AddHistoryLine then
+469 -240
View File
@@ -4,7 +4,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
-- check for SuperWoW support (use SUPERWOW_VERSION global)
local superwow_active = SUPERWOW_VERSION ~= nil
--print("pfUI Nameplates: SuperWoW " .. (superwow_active and "ACTIVE" or "INACTIVE"))
-- Local function references for performance
local GetTime = GetTime
@@ -19,6 +18,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local UnitCanAssist = UnitCanAssist
local UnitCastingInfo = UnitCastingInfo
local UnitChannelInfo = UnitChannelInfo
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitMana = UnitMana
local UnitManaMax = UnitManaMax
local pairs = pairs
local tonumber = tonumber
local strlower = strlower
@@ -27,6 +30,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local floor = floor
local ceil = ceil
local abs = abs
local mathmod = math.mod
local unitcolors = {
["ENEMY_NPC"] = { .9, .2, .3, .8 },
@@ -56,7 +60,8 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
}
-- catch all nameplates
local childs, regions, plate
local childs = {} -- PERF: Reuse table instead of creating new one each scan
local regions, plate
local initialized = 0
-- Friendly zone nameplate disable state
@@ -66,31 +71,111 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local parentcount = 0
local platecount = 0
local registry = {}
-- NEW: Cast event cache like Overhead.lua
local CastEvents = {}
-- ============================================================================
-- OPTIMIZATION: GUID-based registries for O(1) lookups
-- ============================================================================
local guidRegistry = {} -- guid -> plate (for direct event routing)
-- Helper function to safely access libdebuff cast data
local function GetCastInfo(guid)
return pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid]
end
local debuffCache = {} -- guid -> { [spellID] = { start, duration } }
local threatMemory = {} -- guid -> true if mob had player targeted
local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
-- PERF: Track visible plate count for adaptive throttling
local visiblePlateCount = 0
local lastVisibleCheck = 0
-- wipe polyfill
local wipe = wipe or function(t) for k in pairs(t) do t[k] = nil end end
-- Player GUID for filtering
local _, PlayerGUID = UnitExists("player")
-- ============================================================================
-- OPTIMIZATION: Config caching
-- ============================================================================
local cfg = {}
local function CacheConfig()
cfg.showcastbar = C.nameplates["showcastbar"] == "1"
cfg.targetcastbar = C.nameplates["targetcastbar"] == "1"
cfg.notargalpha = tonumber(C.nameplates.notargalpha) or 0.5
if cfg.notargalpha > 1 then cfg.notargalpha = cfg.notargalpha / 100 end
cfg.namefightcolor = C.nameplates.namefightcolor == "1"
cfg.spellname = C.nameplates.spellname == "1"
cfg.showhp = C.nameplates.showhp == "1"
cfg.showdebuffs = C.nameplates["showdebuffs"] == "1"
cfg.targetzoom = C.nameplates.targetzoom == "1"
cfg.zoomval = (tonumber(C.nameplates.targetzoomval) or 0.4) + 1
cfg.width = tonumber(C.nameplates.width) or 120
cfg.heighthealth = tonumber(C.nameplates.heighthealth) or 8
cfg.targetglow = C.nameplates.targetglow == "1"
cfg.targethighlight = C.nameplates.targethighlight == "1"
cfg.outcombatstate = C.nameplates.outcombatstate == "1"
cfg.barcombatstate = C.nameplates.barcombatstate == "1"
cfg.ccombatcasting = C.nameplates.ccombatcasting == "1"
cfg.ccombatthreat = C.nameplates.ccombatthreat == "1"
cfg.ccombatnothreat = C.nameplates.ccombatnothreat == "1"
cfg.ccombatstun = C.nameplates.ccombatstun == "1"
cfg.ccombatofftank = C.nameplates.ccombatofftank == "1"
cfg.use_unitfonts = C.nameplates.use_unitfonts == "1"
cfg.font_size = cfg.use_unitfonts and C.global.font_unit_size or C.global.font_size
cfg.hptextformat = C.nameplates.hptextformat
-- NEW: Cache debuff config
cfg.debufftimers = C.nameplates.debufftimers == "1"
cfg.debuffanim = tonumber(C.nameplates.debuffanim) or 0
cfg.debufftext = tonumber(C.nameplates.debufftext) or 1
end
-- ============================================================================
-- OPTIMIZATION: Frame state cache
-- ============================================================================
local frameState = {
now = 0,
hasTarget = false,
targetGuid = nil,
hasMouseover = false,
}
-- cache default border color
local er, eg, eb, ea = GetStringColor(pfUI_config.appearance.border.color)
local function GetCombatStateColor(guid)
-- PERF: Quick exit if not in combat
if not UnitAffectingCombat("player") then return false end
if not UnitAffectingCombat(guid) then return false end
if UnitCanAssist("player", guid) then return false end
local target = guid.."target"
local color = false
if UnitAffectingCombat("player") and UnitAffectingCombat(guid) and not UnitCanAssist("player", guid) then
if C.nameplates.ccombatcasting == "1" and (UnitCastingInfo(guid) or UnitChannelInfo(guid)) then
color = combatstate.CASTING
elseif C.nameplates.ccombatthreat == "1" and UnitIsUnit(target, "player") then
color = combatstate.THREAT
elseif C.nameplates.ccombatofftank == "1" and UnitName(target) and offtanks[strlower(UnitName(target))] then
color = combatstate.OFFTANK
elseif C.nameplates.ccombatofftank == "1" and pfUI.uf and pfUI.uf.raid and pfUI.uf.raid.tankrole[UnitName(target)] then
color = combatstate.OFFTANK
elseif C.nameplates.ccombatnothreat == "1" and UnitExists(target) then
color = combatstate.NOTHREAT
elseif C.nameplates.ccombatstun == "1" and not UnitExists(target) and not UnitIsPlayer(guid) then
color = combatstate.STUN
end
local castInfo = GetCastInfo(guid)
local isCasting = castInfo and castInfo.endTime and frameState.now < castInfo.endTime
local targetingPlayer = UnitIsUnit(target, "player")
-- Remember if mob targets player, clear only when targeting someone else while NOT casting
if targetingPlayer then
threatMemory[guid] = true
elseif UnitExists(target) and not isCasting then
threatMemory[guid] = nil
end
if cfg.ccombatcasting and isCasting then
color = combatstate.CASTING
elseif cfg.ccombatthreat and (targetingPlayer or threatMemory[guid]) then
color = combatstate.THREAT
elseif cfg.ccombatofftank and UnitName(target) and offtanks[strlower(UnitName(target))] then
color = combatstate.OFFTANK
elseif cfg.ccombatofftank and pfUI.uf and pfUI.uf.raid and pfUI.uf.raid.tankrole[UnitName(target)] then
color = combatstate.OFFTANK
elseif cfg.ccombatnothreat and UnitExists(target) then
color = combatstate.NOTHREAT
elseif cfg.ccombatstun and not UnitExists(target) and not UnitIsPlayer(guid) then
color = combatstate.STUN
end
return color
@@ -298,31 +383,25 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
plate.debuffs[index].stacks:SetJustifyV("BOTTOM")
plate.debuffs[index].stacks:SetTextColor(1,1,0)
-- Read config for cooldown animation and text
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
if pfUI.client <= 11200 then
-- Animation enabled: Use Model frame with CooldownFrameTemplate
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
plate.debuffs[index].cd.pfCooldownStyleAnimation = 1
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
plate.debuffs[index].cd.pfCooldownType = "ALL"
else
-- Animation disabled: Create fake cooldown frame for performance (like ShaguPlates)
-- PERF: Use lightweight fake cooldown frame when animation disabled
-- The Model-based CooldownFrameTemplate causes major lag with many nameplates
if pfUI.client <= 11200 and cfg.debuffanim ~= 1 then
plate.debuffs[index].cd = CreateFrame("Frame", plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index])
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
-- Add dummy functions so CooldownFrame_SetTimer doesn't crash
plate.debuffs[index].cd:SetScript("OnUpdate", CooldownFrame_OnUpdateModel)
plate.debuffs[index].cd.AdvanceTime = DoNothing
plate.debuffs[index].cd.SetSequence = DoNothing
plate.debuffs[index].cd.SetSequenceTime = DoNothing
plate.debuffs[index].cd.pfCooldownStyleAnimation = 0
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
plate.debuffs[index].cd.pfCooldownType = "ALL"
else
-- Use CooldownFrameTemplate for animation or TBC+
plate.debuffs[index].cd = CreateFrame(COOLDOWN_FRAME_TYPE, plate.platename.."Debuff"..index.."Cooldown", plate.debuffs[index], "CooldownFrameTemplate")
plate.debuffs[index].cd:SetAllPoints(plate.debuffs[index])
end
-- Set initial config flags (will be cached per-cooldown later)
plate.debuffs[index].cd.pfCooldownStyleAnimation = cfg.debuffanim
plate.debuffs[index].cd.pfCooldownStyleText = cfg.debufftext
plate.debuffs[index].cd.pfCooldownType = "ALL"
end
local function UpdateDebuffConfig(nameplate, i)
@@ -356,25 +435,62 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplate.debuffs[i]:SetWidth(tonumber(C.nameplates.debuffsize))
nameplate.debuffs[i]:SetHeight(tonumber(C.nameplates.debuffsize))
-- Update cooldown display settings
if nameplate.debuffs[i].cd then
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
nameplate.debuffs[i].cd.pfCooldownStyleText = cooldown_text
nameplate.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim
-- Update scale for TBC+
if pfUI.client > 11200 then
local debuffsize = tonumber(C.nameplates.debuffsize)
local cdScale = debuffsize / 32
nameplate.debuffs[i].cd:SetScale(cdScale)
end
end
end
-- create nameplate core
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
nameplates:RegisterEvent("UNIT_AURA")
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
local nameplates = CreateFrame("Frame", "pfNameplates", UIParent)
nameplates:RegisterEvent("PLAYER_ENTERING_WORLD")
nameplates:RegisterEvent("PLAYER_TARGET_CHANGED")
nameplates:RegisterEvent("PLAYER_LOGOUT")
nameplates:RegisterEvent("UNIT_COMBO_POINTS")
nameplates:RegisterEvent("PLAYER_COMBO_POINTS")
nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA")
-- NEW: Register cast events like Overhead.lua
if superwow_active then
nameplates:RegisterEvent("UNIT_CASTEVENT")
-- Cast tracking handled by libdebuff (SPELL_START/GO/FAILED events)
-- No local event registration needed
-- Callback from libdebuff when auras change (GUID-based, event-driven)
nameplates.OnAuraUpdate = function(self, guid)
if not guid then return end
-- GUID is actual GUID (0xF13000...) from SuperWoW/Nampower events
local plate = guidRegistry[guid]
if plate and plate.nameplate then
-- Mark nameplate for aura update in next OnUpdate cycle
plate.nameplate.auraUpdate = true
end
end
nameplates:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
-- Stop event handling during logout to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
this:SetScript("OnUpdate", nil)
if nameplates.mouselook then
nameplates.mouselook:SetScript("OnUpdate", nil)
end
return
elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
if event == "PLAYER_ENTERING_WORLD" then
_, PlayerGUID = UnitExists("player")
CacheConfig()
this:SetGameVariables()
end
@@ -419,47 +535,27 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
savedFriendlyState = nil
end
end
elseif event == "UNIT_CASTEVENT" then
-- NEW: Event-based cast handling from Overhead.lua
local casterGUID = arg1
if casterGUID == PlayerGUID then return end
local eventType = arg3 -- "START", "CAST", "FAIL", "CHANNEL"
local spellID = arg4
local castDuration = arg5
local spellName, _, icon = SpellInfo(spellID)
if eventType == "MAINHAND" or eventType == "OFFHAND" then
return
end
if eventType == "CAST" then
if not CastEvents[casterGUID] or spellID ~= CastEvents[casterGUID].spell then
return
elseif event == "PLAYER_TARGET_CHANGED" then
-- Flag target plate for update via GUID registry
local _, targetGuid = UnitExists("target")
if targetGuid then
local plate = guidRegistry[targetGuid]
if plate and plate.nameplate then
plate.nameplate.targetUpdate = true
end
end
if eventType == "START" or eventType == "CHANNEL" then
if not CastEvents[casterGUID] then
CastEvents[casterGUID] = {}
-- Also propagate to all plates for alpha/strata updates
this.eventcache = true
elseif event == "PLAYER_COMBO_POINTS" or event == "UNIT_COMBO_POINTS" then
-- Only flag the target plate for combo point update
local _, targetGuid = UnitExists("target")
if targetGuid then
local plate = guidRegistry[targetGuid]
if plate and plate.nameplate then
plate.nameplate.comboUpdate = true
end
wipe(CastEvents[casterGUID])
CastEvents[casterGUID].event = eventType
CastEvents[casterGUID].spellID = spellID
CastEvents[casterGUID].spellName = spellName
CastEvents[casterGUID].icon = icon
CastEvents[casterGUID].startTime = GetTime()
CastEvents[casterGUID].endTime = castDuration and GetTime() + castDuration / 1000
CastEvents[casterGUID].duration = castDuration and castDuration / 1000 or nil
elseif eventType == "FAIL" then
if CastEvents[casterGUID] then
wipe(CastEvents[casterGUID])
end
end
-- Propagate cast event to all nameplates
for plate in pairs(registry) do
plate.eventcache = true
end
else
this.eventcache = true
@@ -467,8 +563,10 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end)
nameplates:SetScript("OnUpdate", function()
-- Throttle main scanner to reasonable rate
if (this.tick or 0) > GetTime() then return else this.tick = GetTime() + 0.05 end
-- PERF: Cache GetTime() once per frame
frameState.now = GetTime()
frameState.hasTarget, frameState.targetGuid = UnitExists("target")
frameState.hasMouseover = UnitExists("mouseover")
-- propagate events to all nameplates
if this.eventcache then
@@ -478,19 +576,76 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end
end
-- detect new nameplates
parentcount = WorldFrame:GetNumChildren()
if initialized < parentcount then
childs = { WorldFrame:GetChildren() }
for i = initialized + 1, parentcount do
plate = childs[i]
if IsNamePlate(plate) and not registry[plate] then
nameplates.OnCreate(plate)
registry[plate] = plate
-- PERF: Update visible plate count periodically for adaptive throttling
if frameState.now - lastVisibleCheck > 0.5 then
lastVisibleCheck = frameState.now
local count = 0
for plate in pairs(registry) do
if plate:IsVisible() then count = count + 1 end
end
visiblePlateCount = count
end
-- Throttle ONLY the nameplate scanner
local scanThrottle = nameplates.combat and nameplates.combat.inCombat and 0.1 or 0.05
local shouldScan = (this.tick or 0) <= frameState.now
if shouldScan then
this.tick = frameState.now + scanThrottle
-- detect new nameplates
parentcount = WorldFrame:GetNumChildren()
if initialized < parentcount then
-- PERF: Reuse childs table instead of creating new one
local newchilds = { WorldFrame:GetChildren() }
for i = 1, parentcount do
childs[i] = newchilds[i]
end
for i = initialized + 1, parentcount do
plate = childs[i]
if IsNamePlate(plate) and not registry[plate] then
nameplates.OnCreate(plate)
registry[plate] = plate
end
end
initialized = parentcount
end
end
-- Central OnUpdate for all visible plates
for plate in pairs(registry) do
if plate:IsVisible() then
nameplates.OnUpdate(plate, frameState)
else
-- PERF: Clean up ALL caches for hidden plates to prevent memory leak
local guid = plate.nameplate and plate.nameplate.cachedGuid
if guid then
-- Remove from guidRegistry
if guidRegistry[guid] == plate then
guidRegistry[guid] = nil
end
-- Clean cast cache ONLY if cast has expired
-- (Don't delete active casts just because plate was hidden briefly)
local castInfo = GetCastInfo(guid)
if castInfo and castInfo.endTime and castInfo.endTime < frameState.now then
if pfUI.libdebuff_casts then
pfUI.libdebuff_casts[guid] = nil
end
end
-- Clean debuffCache
if debuffCache[guid] then
debuffCache[guid] = nil
end
-- Clean threatMemory
if threatMemory[guid] then
threatMemory[guid] = nil
end
end
end
initialized = parentcount
end
end)
@@ -498,13 +653,22 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplates.combat = CreateFrame("Frame")
nameplates.combat:RegisterEvent("PLAYER_ENTER_COMBAT")
nameplates.combat:RegisterEvent("PLAYER_LEAVE_COMBAT")
nameplates.combat:RegisterEvent("PLAYER_LOGOUT")
nameplates.combat:SetScript("OnEvent", function()
if event == "PLAYER_ENTER_COMBAT" then
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
elseif event == "PLAYER_ENTER_COMBAT" then
this.inCombat = 1
if PlayerFrame then PlayerFrame.inCombat = 1 end
elseif event == "PLAYER_LEAVE_COMBAT" then
this.inCombat = nil
if PlayerFrame then PlayerFrame.inCombat = nil end
-- Clear threat memory when leaving combat
for k in pairs(threatMemory) do
threatMemory[k] = nil
end
end
end)
@@ -635,9 +799,13 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplate.castbar = castbar
end
-- Stagger tick to spread updates across frames (0.05s apart per plate)
nameplate.tick = GetTime() + mathmod(platecount, 10) * 0.05
parent.nameplate = nameplate
HookScript(parent, "OnShow", nameplates.OnShow)
HookScript(parent, "OnUpdate", nameplates.OnUpdate)
-- NOTE: OnUpdate is now handled centrally, not per-plate/
parent:SetScript("OnUpdate", nil) -- Disable Blizzard's OnUpdate
nameplates.OnConfigChange(parent)
nameplates.OnShow(parent)
@@ -808,12 +976,12 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
-- always make sure to keep plate visible
plate:Show()
if target and C.nameplates.targetglow == "1" then
if target and cfg.targetglow then
plate.glow:Show() else plate.glow:Hide()
end
-- target indicator
if superwow_active and C.nameplates.outcombatstate == "1" then
if superwow_active and cfg.outcombatstate then
local guid = plate.parent:GetName(1) or ""
-- determine color based on combat state
@@ -822,7 +990,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
-- set border color
plate.health.backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a)
elseif target and C.nameplates.targethighlight == "1" then
elseif target and cfg.targethighlight then
plate.health.backdrop:SetBackdropBorderColor(plate.health.hlr, plate.health.hlg, plate.health.hlb, plate.health.hla)
elseif C.nameplates.outfriendlynpc == "1" and unittype == "FRIENDLY_NPC" then
plate.health.backdrop:SetBackdropBorderColor(unpack(unitcolors[unittype]))
@@ -899,30 +1067,44 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
plate.health:SetMinMaxValues(hpmin, hpmax)
plate.health:SetValue(hp)
if C.nameplates.showhp == "1" then
if cfg.showhp then
local rhp, rhpmax, estimated
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
rhp, rhpmax = hp, hpmax
elseif pfUI.libhealth and pfUI.libhealth.enabled then
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
-- Try Nampower first for real HP values via GUID
local guid = superwow_active and plate.parent:GetName(1) or nil
if guid and GetUnitField then
local npHp = GetUnitField(guid, "health")
local npMaxHp = GetUnitField(guid, "maxHealth")
if npHp and npHp > 0 and npMaxHp and npMaxHp > 0 then
rhp, rhpmax = npHp, npMaxHp
end
end
-- Fallback to existing methods
if not rhp then
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
rhp, rhpmax = hp, hpmax
elseif pfUI.libhealth and pfUI.libhealth.enabled then
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(name,level,tonumber(hp),tonumber(hpmax))
end
end
local setting = C.nameplates.hptextformat
local hasdata = ( estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp) )
local setting = cfg.hptextformat
local hasdata = ( rhp and rhpmax ) or estimated or hpmax > 100 or (round(hpmax/100*hp) ~= hp)
if setting == "curperc" and hasdata then
if setting == "curperc" and hasdata and rhp then
plate.health.text:SetText(string.format("%s | %s%%", Abbreviate(rhp), ceil(hp/hpmax*100)))
elseif setting == "cur" and hasdata then
elseif setting == "cur" and hasdata and rhp then
plate.health.text:SetText(string.format("%s", Abbreviate(rhp)))
elseif setting == "curmax" and hasdata then
elseif setting == "curmax" and hasdata and rhp then
plate.health.text:SetText(string.format("%s - %s", Abbreviate(rhp), Abbreviate(rhpmax)))
elseif setting == "curmaxs" and hasdata then
elseif setting == "curmaxs" and hasdata and rhp then
plate.health.text:SetText(string.format("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax)))
elseif setting == "curmaxperc" and hasdata then
elseif setting == "curmaxperc" and hasdata and rhp then
plate.health.text:SetText(string.format("%s - %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
elseif setting == "curmaxpercs" and hasdata then
elseif setting == "curmaxpercs" and hasdata and rhp then
plate.health.text:SetText(string.format("%s / %s | %s%%", Abbreviate(rhp), Abbreviate(rhpmax), ceil(hp/hpmax*100)))
elseif setting == "deficit" then
elseif setting == "deficit" and rhp then
plate.health.text:SetText(string.format("-%s" .. (hasdata and "" or "%%"), Abbreviate(rhpmax - rhp)))
else -- "percent" as fallback
plate.health.text:SetText(string.format("%s%%", ceil(hp/hpmax*100)))
@@ -943,7 +1125,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
r, g, b, a = .5, .5, .5, .8
end
if superwow_active and C.nameplates.barcombatstate == "1" then
if superwow_active and cfg.barcombatstate then
local guid = plate.parent:GetName(1) or ""
local color = GetCombatStateColor(guid)
@@ -971,7 +1153,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
-- update debuffs
local index = 1
if C.nameplates["showdebuffs"] == "1" then
if cfg.showdebuffs then
local verify = string.format("%s:%s", (name or ""), (level or ""))
-- update cached debuffs
@@ -1008,18 +1190,26 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
plate.debuffs[index].stacks:Hide()
end
if duration and timeleft and C.nameplates.debufftimers == "1" then
-- Read config values for cooldown display
local cooldown_anim = tonumber(C.nameplates.debuffanim) or 0
local cooldown_text = tonumber(C.nameplates.debufftext) or 1
if duration and timeleft and cfg.debufftimers then
-- PERF: Only update cooldown if start time changed significantly
local cd = plate.debuffs[index].cd
local newStart = GetTime() + timeleft - duration
-- Ensure cooldown flags are set
plate.debuffs[index].cd.pfCooldownStyleText = cooldown_text
plate.debuffs[index].cd.pfCooldownStyleAnimation = cooldown_anim
plate.debuffs[index].cd.pfCooldownType = "ALL"
plate.debuffs[index].cd:Show()
CooldownFrame_SetTimer(plate.debuffs[index].cd, GetTime() + timeleft - duration, duration, 1)
if not cd.cachedStart or abs(cd.cachedStart - newStart) > 0.5 then
-- Update config flags only on first run or config change
if not cd.configCached or cd.cachedAnim ~= cfg.debuffanim or cd.cachedText ~= cfg.debufftext then
cd.pfCooldownStyleAnimation = cfg.debuffanim
cd.pfCooldownStyleText = cfg.debufftext
cd:SetAlpha(cfg.debuffanim == 1 and 1 or 0)
cd.cachedAnim = cfg.debuffanim
cd.cachedText = cfg.debufftext
cd.configCached = true
end
cd:Show()
CooldownFrame_SetTimer(cd, newStart, duration, 1)
cd.cachedStart = newStart
end
end
index = index + 1
@@ -1039,45 +1229,111 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local frame = frame or this
local nameplate = frame.nameplate
-- Register GUID when plate becomes visible
if superwow_active then
local guid = frame:GetName(1)
if guid then
nameplate.cachedGuid = guid
guidRegistry[guid] = frame
end
end
nameplates:OnDataChanged(nameplate)
end
nameplates.OnUpdate = function(frame)
local frame = frame or this
nameplates.OnUpdate = function(frame, state)
local nameplate = frame.nameplate
local now = GetTime()
local now = state and state.now or GetTime()
-- Performance: Skip invisible frames immediately
local isVisible = frame:IsVisible()
if not isVisible then return end
-- Update GUID registry (lightweight, needed for event routing)
if superwow_active then
local guid = frame:GetName(1)
if guid and guid ~= nameplate.cachedGuid then
if nameplate.cachedGuid and guidRegistry[nameplate.cachedGuid] == frame then
guidRegistry[nameplate.cachedGuid] = nil
end
nameplate.cachedGuid = guid
guidRegistry[guid] = frame
end
end
-- Intelligent throttling based on target/castbar status
local target = UnitExists("target") and frame:GetAlpha() >= 0.99 or nil
-- PERF: Intelligent throttling based on target/castbar status and plate count
local target = state and state.hasTarget and frame:GetAlpha() >= 0.99 or nil
local isCasting = nameplate.castbar and nameplate.castbar:IsShown()
local throttle
if target or isCasting then
throttle = 0.02 -- 50 FPS for target OR active castbar
elseif visiblePlateCount > 20 then
throttle = 0.15 -- ~7 FPS for mass pulls (20+ plates)
else
throttle = 0.1 -- 10 FPS for others (healthbar updates)
end
if (nameplate.lasttick or 0) + throttle > now then return end
-- Check for pending event updates (these bypass throttle for immediate response)
local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate
-- Event updates bypass throttle
if not hasEventUpdate and (nameplate.lasttick or 0) + throttle > now then return end
nameplate.lasttick = now
-- =========================================================================
-- EVERYTHING BELOW RUNS AT THROTTLED RATE (50 FPS target, 10 FPS others)
-- =========================================================================
local update
local original = nameplate.original
local name = original.name:GetText()
local mouseover = UnitExists("mouseover") and original.glow:IsShown() or nil
local namefightcolor = C.nameplates.namefightcolor == "1"
local mouseover = state and state.hasMouseover and original.glow:IsShown() or nil
-- trigger queued event update
if nameplate.eventcache then
if hasEventUpdate then
nameplates:OnDataChanged(nameplate)
nameplate.eventcache = nil
nameplate.auraUpdate = nil
nameplate.castUpdate = nil
nameplate.targetUpdate = nil
nameplate.comboUpdate = nil
end
-- OPTIMIZED: Cache strata changes
-- =========================================================================
-- VANILLA OVERLAP/CLICKTHROUGH HANDLING
-- =========================================================================
if pfUI.client <= 11200 then
local useOverlap = C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0"
local clickable = C.nameplates["clickthrough"] ~= "1"
if not clickable then
frame:EnableMouse(false)
nameplate:EnableMouse(false)
else
local plate = useOverlap and nameplate or frame
plate:EnableMouse(clickable)
end
if C.nameplates["overlap"] == "1" then
if frame:GetWidth() > 1 then
frame:SetWidth(1)
frame:SetHeight(1)
end
else
if not nameplate.dwidth then
nameplate.dwidth = floor(nameplate:GetWidth() * UIParent:GetScale())
end
if floor(frame:GetWidth()) ~= nameplate.dwidth then
frame:SetWidth(nameplate:GetWidth() * UIParent:GetScale())
frame:SetHeight(nameplate:GetHeight() * UIParent:GetScale())
end
end
local mouseEnabled = nameplate:IsMouseEnabled()
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
nameplate:EnableMouse(not mouseEnabled)
end
end
-- Cache strata changes
if nameplate.istarget ~= target then
nameplate.target_strata = nil
end
@@ -1092,21 +1348,11 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplate.istarget = target
-- set non-target plate alpha (cached to prevent flicker)
local configAlpha = tonumber(C.nameplates.notargalpha)
if not configAlpha or configAlpha < 0 then
configAlpha = 100
end
-- Ensure we're working with 0-1 range
if configAlpha > 1 then
configAlpha = configAlpha / 100
end
local desiredAlpha = (target or not UnitExists("target")) and 1 or configAlpha
-- Set non-target plate alpha
local configAlpha = cfg.notargalpha or 0.5
local desiredAlpha = (target or not state.hasTarget) and 1 or configAlpha
if nameplate.cachedAlpha ~= desiredAlpha then
-- Setze nur die nameplate Alpha, nicht den parent frame
nameplate:SetAlpha(desiredAlpha)
nameplate.cachedAlpha = desiredAlpha
end
@@ -1129,13 +1375,22 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
update = true
end
-- trigger update when name color changed
-- trigger update when name color changed (includes combat state check)
local r, g, b = original.name:GetTextColor()
if r + g + b ~= nameplate.cache.namecolor then
local inCombatWithPlayer = false
if superwow_active and cfg.namefightcolor then
local guid = nameplate.cachedGuid
if guid then
inCombatWithPlayer = UnitAffectingCombat(guid) and UnitAffectingCombat("player")
end
end
if r + g + b ~= nameplate.cache.namecolor or (cfg.namefightcolor and nameplate.cache.inCombat ~= inCombatWithPlayer) then
nameplate.cache.namecolor = r + g + b
nameplate.cache.inCombat = inCombatWithPlayer
if namefightcolor then
if r > .9 and g < .2 and b < .2 then
if cfg.namefightcolor then
if (r > .9 and g < .2 and b < .2) or inCombatWithPlayer then
nameplate.name:SetTextColor(1,0.4,0.2,1)
else
nameplate.name:SetTextColor(r,g,b,1)
@@ -1155,10 +1410,11 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
update = true
end
-- scan for debuff timeouts
-- PERF: scan for debuff timeouts using indexed access instead of pairs()
if nameplate.debuffcache then
for id, data in pairs(nameplate.debuffcache) do
if ( not data.stop or data.stop < now ) and not data.empty then
for id = 1, 16 do
local data = nameplate.debuffcache[id]
if data and ( not data.stop or data.stop < now ) and not data.empty then
data.empty = true
update = true
end
@@ -1176,12 +1432,12 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplate.tick = now + .5
end
-- OPTIMIZED: Cache zoom dimensions
if target and C.nameplates.targetzoom == "1" then
-- Zoom animation
if target and cfg.targetzoom then
if not nameplate.health.zoomed then
local zoomval = tonumber(C.nameplates.targetzoomval)+1
local wc = tonumber(C.nameplates.width)*zoomval
local hc = tonumber(C.nameplates.heighthealth)*(zoomval*.9)
local zoomval = cfg.zoomval
local wc = cfg.width * zoomval
local hc = cfg.heighthealth * (zoomval * .9)
nameplate.health.targetWidth = wc
nameplate.health.targetHeight = hc
end
@@ -1189,27 +1445,27 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
local wc, hc = nameplate.health.targetWidth, nameplate.health.targetHeight
-- Nutze kleine Toleranz um Fließkomma-Schwankungen zu vermeiden
if wc > w + 0.5 then
nameplate.health:SetWidth(w*1.05)
nameplate.health.zoomTransition = true
elseif hc > h + 0.5 then
nameplate.health:SetHeight(h*1.05)
nameplate.health.zoomTransition = true
else
if nameplate.health.zoomTransition then
nameplate.health:SetWidth(wc)
nameplate.health:SetHeight(hc)
nameplate.health.zoomTransition = nil
if wc and hc then
if wc > w + 0.5 then
nameplate.health:SetWidth(w*1.05)
nameplate.health.zoomTransition = true
elseif hc > h + 0.5 then
nameplate.health:SetHeight(h*1.05)
nameplate.health.zoomTransition = true
else
if nameplate.health.zoomTransition then
nameplate.health:SetWidth(wc)
nameplate.health:SetHeight(hc)
nameplate.health.zoomTransition = nil
end
nameplate.health.zoomed = true
end
nameplate.health.zoomed = true
end
elseif nameplate.health.zoomed or nameplate.health.zoomTransition then
local w, h = nameplate.health:GetWidth(), nameplate.health:GetHeight()
local wc = tonumber(C.nameplates.width)
local hc = tonumber(C.nameplates.heighthealth)
local wc = cfg.width
local hc = cfg.heighthealth
-- Nutze kleine Toleranz um Fließkomma-Schwankungen zu vermeiden
if w > wc + 0.5 then
nameplate.health:SetWidth(w*.95)
elseif h > hc + 0.5 then
@@ -1224,27 +1480,25 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end
end
-- OPTIMIZED: UNIT_CASTEVENT implementation
-- OPTIMIZED: 100% Nampower - libdebuff handles all cast events (SPELL_START/GO/FAILED)
-- Use multiple checks for target detection (target variable, istarget flag, or zoomed state)
local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed)
if C.nameplates["showcastbar"] == "1" and ( C.nameplates["targetcastbar"] == "0" or isTargetPlate ) then
if cfg.showcastbar and ( not cfg.targetcastbar or isTargetPlate ) then
local unitstr = nil
local targetGUID = nil
-- Get GUID for CastEvents lookup
if isTargetPlate and superwow_active then
-- Get target's GUID for event cache lookup
local _, guid = UnitExists("target")
targetGUID = guid
-- Get GUID for CastEvents lookup - use cached GUID when available
if isTargetPlate then
targetGUID = state and state.targetGuid
end
-- Use SuperWoW GUID for non-target plates
-- Use cached GUID for non-target plates
if superwow_active and not isTargetPlate then
unitstr = nameplate.parent:GetName(1)
unitstr = nameplate.cachedGuid
end
-- Check event-based cast cache first (use GUID)
local castInfo = (targetGUID and CastEvents[targetGUID]) or (unitstr and CastEvents[unitstr])
local castInfo = GetCastInfo(targetGUID) or (unitstr and GetCastInfo(unitstr))
if castInfo and castInfo.spellID then
-- Check if cast is still valid
@@ -1266,9 +1520,15 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end
nameplate.castbar:SetValue(barValue)
nameplate.castbar.text:SetText(round(now - castInfo.startTime, 1))
-- Show remaining time (countdown), not elapsed time
local remaining = castInfo.endTime - now
if C.unitframes.castbardecimals == "1" then
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
else
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
end
if C.nameplates.spellname == "1" then
if cfg.spellname then
nameplate.castbar.spell:SetText(castInfo.spellName)
else
nameplate.castbar.spell:SetText("")
@@ -1293,6 +1553,7 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end
if not cast and not channel then
-- No cast data = hide castbar (fixes stuck castbar on FAIL for non-target plates)
nameplate.castbar:Hide()
else
local effect = cast or channel
@@ -1304,7 +1565,14 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplate.castbar:SetMinMaxValues(0, duration/1000)
nameplate.castbar:SetValue(cur)
nameplate.castbar.text:SetText(round(cur,1))
-- Show remaining time (countdown), not elapsed time
local remaining = max - cur
if channel then remaining = cur end -- Channel already counts down
if C.unitframes.castbardecimals == "1" then
nameplate.castbar.text:SetText(floor(remaining * 10) / 10)
else
nameplate.castbar.text:SetText(string.format("%.2f", remaining))
end
if C.nameplates.spellname == "1" then
nameplate.castbar.spell:SetText(effect)
@@ -1349,6 +1617,9 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
nameplates:SetGameVariables()
nameplates.UpdateConfig = function()
-- Refresh config cache for all cfg.* values
CacheConfig()
-- update debuff filters
DebuffFilterPopulate()
@@ -1458,48 +1729,6 @@ pfUI:RegisterModule("nameplates", "vanilla", function ()
end
end
local hookOnUpdate = nameplates.OnUpdate
nameplates.OnUpdate = function(self)
-- initialize shortcut variables
local plate = (C.nameplates["overlap"] == "1" or C.nameplates["vertical_offset"] ~= "0") and this.nameplate or this
local clickable = C.nameplates["clickthrough"] ~= "1" and true or false
-- disable all click events
if not clickable then
this:EnableMouse(false)
this.nameplate:EnableMouse(false)
else
plate:EnableMouse(clickable)
end
if C.nameplates["overlap"] == "1" then
if this:GetWidth() > 1 then
-- set parent to 1 pixel to have them overlap each other
this:SetWidth(1)
this:SetHeight(1)
end
else
if not this.nameplate.dwidth then
-- cache initial sizing value for comparison
this.nameplate.dwidth = floor(this.nameplate:GetWidth() * UIParent:GetScale())
end
if floor(this:GetWidth()) ~= this.nameplate.dwidth then
-- align parent plate to the actual size
this:SetWidth(this.nameplate:GetWidth() * UIParent:GetScale())
this:SetHeight(this.nameplate:GetHeight() * UIParent:GetScale())
end
end
-- disable click events while spell is targeting
local mouseEnabled = this.nameplate:IsMouseEnabled()
if C.nameplates["clickthrough"] == "0" and C.nameplates["overlap"] == "1" and SpellIsTargeting() == mouseEnabled then
this.nameplate:EnableMouse(not mouseEnabled)
end
hookOnUpdate(self)
end
-- enable mouselook on rightbutton down
nameplates.mouselook = CreateFrame("Frame", nil, UIParent)
nameplates.mouselook.time = nil
+13 -291
View File
@@ -54,7 +54,15 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
local queue = CreateFrame("Frame")
queue:RegisterEvent("SPELL_QUEUE_EVENT")
queue:RegisterEvent("PLAYER_LOGOUT")
queue:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
local eventCode = arg1
local spellId = arg2
@@ -76,189 +84,7 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
end)
end
-- Enhanced Debuff Tracking using Nampower events
-- DEBUFF_ADDED_OTHER/DEBUFF_REMOVED_OTHER provide accurate debuff tracking with spellId
if libdebuff then
-- Storage for GUID-based debuff tracking
pfUI.nampower_debuffs = pfUI.nampower_debuffs or {}
local debuffdb = pfUI.nampower_debuffs
-- Get player GUID for tracking own debuffs
local playerGuid
local debuffTracker = CreateFrame("Frame")
debuffTracker:RegisterEvent("PLAYER_ENTERING_WORLD")
debuffTracker:RegisterEvent("DEBUFF_ADDED_OTHER")
debuffTracker:RegisterEvent("DEBUFF_REMOVED_OTHER")
debuffTracker:RegisterEvent("DEBUFF_ADDED_SELF")
debuffTracker:RegisterEvent("DEBUFF_REMOVED_SELF")
debuffTracker:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
-- Cache player GUID
if UnitExists then
local _, guid = UnitExists("player")
playerGuid = guid
end
return
end
-- DEBUFF events: arg1=guid, arg2=slot, arg3=spellId, arg4=stackCount, arg5=auraLevel
local guid = arg1
local slot = arg2
local spellId = arg3
local stackCount = arg4
local auraLevel = arg5
if not guid or not spellId then return end
if event == "DEBUFF_ADDED_OTHER" or event == "DEBUFF_ADDED_SELF" then
-- Initialize storage for this GUID
if not debuffdb[guid] then debuffdb[guid] = {} end
-- Get spell info
local spellName, spellRank, texture
if SpellInfo then
spellName, spellRank, texture = SpellInfo(spellId)
end
if not spellName then
spellName, spellRank = SafeGetSpellNameAndRank(spellId)
end
if spellName then
-- Get duration from libdebuff's duration table or GetSpellRec
local duration = 0
if libdebuff.GetDuration then
duration = libdebuff:GetDuration(spellName, spellRank)
end
-- Try GetSpellRec for duration if libdebuff doesn't have it
if duration == 0 and GetSpellRec and spellId then
local success, spellRec = pcall(GetSpellRec, spellId)
if success and spellRec and spellRec.durationIndex and spellRec.durationIndex > 0 then
-- Duration index maps to spell duration - common values:
-- This is a rough approximation since we don't have the duration table
duration = 30 -- Default fallback
end
end
-- Store debuff data
debuffdb[guid][spellId] = {
spellId = spellId,
name = spellName,
rank = spellRank,
texture = texture,
stacks = stackCount or 1,
start = GetTime(),
duration = duration,
slot = slot,
auraLevel = auraLevel,
caster = (event == "DEBUFF_ADDED_SELF" or guid == playerGuid) and "player" or nil
}
-- Also update libdebuff's internal tracking if we have unit info
local unitName = UnitName and guid and UnitName(guid)
local unitLevel = UnitLevel and guid and UnitLevel(guid)
if unitName and duration > 0 then
libdebuff:AddEffect(unitName, unitLevel or 0, spellName, duration, "player")
end
end
elseif event == "DEBUFF_REMOVED_OTHER" or event == "DEBUFF_REMOVED_SELF" then
-- Remove debuff from tracking
if debuffdb[guid] and debuffdb[guid][spellId] then
debuffdb[guid][spellId] = nil
end
end
end)
-- Enhanced UnitDebuff function that uses Nampower data
-- This provides more accurate debuff information when available
local originalUnitDebuff = libdebuff.UnitDebuff
function libdebuff:UnitDebuffNampower(unit, id)
-- First try the original method
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitDebuff(self, unit, id)
-- If we have Nampower data for this unit, try to enhance it
if not UnitExists then return effect, rank, texture, stacks, dtype, duration, timeleft, caster end
local exists, guid = UnitExists(unit)
if not exists or not guid or not debuffdb[guid] then
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- Find the debuff by slot
for spellId, data in pairs(debuffdb[guid]) do
if data.slot == id then
-- Use Nampower data for more accurate timing
if data.duration and data.duration > 0 and data.start then
duration = data.duration
timeleft = data.duration + data.start - GetTime()
if timeleft < 0 then timeleft = 0 end
caster = data.caster
stacks = data.stacks or stacks
end
break
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- Expose enhanced function
pfUI.api.libdebuff_nampower = libdebuff.UnitDebuffNampower
end
-- Enhanced buff tracking using BUFF events
if C.unitframes.nampower_buffs == "1" then
pfUI.nampower_buffs = pfUI.nampower_buffs or {}
local buffdb = pfUI.nampower_buffs
local buffTracker = CreateFrame("Frame")
buffTracker:RegisterEvent("BUFF_ADDED_OTHER")
buffTracker:RegisterEvent("BUFF_REMOVED_OTHER")
buffTracker:RegisterEvent("BUFF_ADDED_SELF")
buffTracker:RegisterEvent("BUFF_REMOVED_SELF")
buffTracker:SetScript("OnEvent", function()
local guid = arg1
local slot = arg2
local spellId = arg3
local stackCount = arg4
local auraLevel = arg5
if not guid or not spellId then return end
if event == "BUFF_ADDED_OTHER" or event == "BUFF_ADDED_SELF" then
if not buffdb[guid] then buffdb[guid] = {} end
local spellName, spellRank, texture
if SpellInfo then
spellName, spellRank, texture = SpellInfo(spellId)
end
if not spellName then
spellName, spellRank = SafeGetSpellNameAndRank(spellId)
end
if spellName then
buffdb[guid][spellId] = {
spellId = spellId,
name = spellName,
rank = spellRank,
texture = texture,
stacks = stackCount or 1,
start = GetTime(),
slot = slot,
auraLevel = auraLevel
}
end
elseif event == "BUFF_REMOVED_OTHER" or event == "BUFF_REMOVED_SELF" then
if buffdb[guid] and buffdb[guid][spellId] then
buffdb[guid][spellId] = nil
end
end
end)
end
-- NOTE: Buff tracking removed - was dead code (data collected but never used for display)
-- Direct Aura Access API using GetUnitField
-- Much faster than tooltip scanning - reads aura arrays directly from unit fields
@@ -429,22 +255,8 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
end
end
-- UNIT_DIED event handling
-- Can be used to clear tracking data or trigger effects on unit death
local deathTracker = CreateFrame("Frame")
deathTracker:RegisterEvent("UNIT_DIED")
deathTracker:SetScript("OnEvent", function()
local guid = arg1
if not guid then return end
-- Clean up debuff tracking for dead units
if pfUI.nampower_debuffs and pfUI.nampower_debuffs[guid] then
pfUI.nampower_debuffs[guid] = nil
end
if pfUI.nampower_buffs and pfUI.nampower_buffs[guid] then
pfUI.nampower_buffs[guid] = nil
end
end)
-- UNIT_DIED event handling - placeholder for future use
-- (Debuff/buff cleanup removed as tracking is now handled by libdebuff)
-- Trinket Management API
if GetTrinkets then
@@ -667,96 +479,6 @@ pfUI:RegisterModule("nampower", "vanilla", function ()
end
end
-- Enhanced Heal Prediction with AURA_CAST events
-- This helps libpredict detect HoT applications more accurately
if libpredict then
local auraCastFrame = CreateFrame("Frame")
auraCastFrame:RegisterEvent("AURA_CAST_ON_SELF")
auraCastFrame:RegisterEvent("AURA_CAST_ON_OTHER")
auraCastFrame:SetScript("OnEvent", function()
local casterGuid = arg1
local targetGuid = arg2
local spellId = arg3
if not spellId or not targetGuid then return end
-- Get spell name
local spellName
if SpellInfo then
spellName = SpellInfo(spellId)
end
if not spellName then
spellName = SafeGetSpellNameAndRank(spellId)
end
if not spellName then return end
-- Check if this is a HoT spell we care about
local hotSpells = {
["Rejuvenation"] = true,
["Renew"] = true,
["Regrowth"] = true,
["Verjüngung"] = true, -- German
["Erneuerung"] = true,
["Nachwachsen"] = true,
}
if hotSpells[spellName] then
-- Signal to libpredict that a HoT was applied
-- This can be used to update heal predictions
if pfUI.api.libpredict and pfUI.api.libpredict.OnHotApplied then
pfUI.api.libpredict:OnHotApplied(targetGuid, spellName, spellId)
end
end
end)
end
-- Swing Timer Integration
-- Track auto-attack swing timers for melee classes
local swingFrame = CreateFrame("Frame")
swingFrame.mainHand = { start = 0, speed = 0 }
swingFrame.offHand = { start = 0, speed = 0 }
swingFrame.ranged = { start = 0, speed = 0 }
swingFrame:RegisterEvent("PLAYER_ENTER_COMBAT")
swingFrame:RegisterEvent("PLAYER_LEAVE_COMBAT")
swingFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
swingFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES")
swingFrame:SetScript("OnEvent", function()
if event == "PLAYER_ENTER_COMBAT" then
local mainSpeed, offSpeed = UnitAttackSpeed("player")
this.mainHand.speed = mainSpeed or 2
this.offHand.speed = offSpeed or 0
this.mainHand.start = GetTime()
if this.offHand.speed > 0 then
this.offHand.start = GetTime()
end
elseif event == "CHAT_MSG_COMBAT_SELF_HITS" or event == "CHAT_MSG_COMBAT_SELF_MISSES" then
-- Reset swing timer on hit/miss
local mainSpeed, offSpeed = UnitAttackSpeed("player")
this.mainHand.speed = mainSpeed or 2
this.mainHand.start = GetTime()
end
end)
pfUI.api.GetSwingTimers = function()
local now = GetTime()
local mainRemaining = swingFrame.mainHand.speed - (now - swingFrame.mainHand.start)
local offRemaining = swingFrame.offHand.speed > 0 and (swingFrame.offHand.speed - (now - swingFrame.offHand.start)) or 0
return {
mainHand = {
remaining = math.max(0, mainRemaining),
speed = swingFrame.mainHand.speed,
progress = swingFrame.mainHand.speed > 0 and (1 - math.max(0, mainRemaining) / swingFrame.mainHand.speed) or 0,
},
offHand = {
remaining = math.max(0, offRemaining),
speed = swingFrame.offHand.speed,
progress = swingFrame.offHand.speed > 0 and (1 - math.max(0, offRemaining) / swingFrame.offHand.speed) or 0,
},
}
end
-- NOTE: HoT Detection (AURA_CAST events) removed - OnHotApplied callback was never implemented in libpredict
-- NOTE: Swing Timer removed - GetSwingTimers() was never called anywhere
end)
+2 -2
View File
@@ -15,8 +15,8 @@ pfUI:RegisterModule("player", "vanilla:tbc", function ()
if pfUI.uf.player:GetScript("OnUpdate") then
local originalOnUpdate = pfUI.uf.player:GetScript("OnUpdate")
pfUI.uf.player:SetScript("OnUpdate", function()
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.1
if (this.throttleTick or 0) > GetTime() then return end
this.throttleTick = GetTime() + 0.1
originalOnUpdate()
end)
end
+21
View File
@@ -113,6 +113,27 @@ pfUI:RegisterModule("raid", "vanilla:tbc", function ()
end
end
-- Smart GUID-based updates: only refresh frames where unit changed
if pfUI.uf.guidTracker then
local tracker = pfUI.uf.guidTracker
for i = 1, maxraid do
local frame = pfUI.uf.raid[i]
if frame and frame.id and frame.id > 0 then
local unit = "raid" .. frame.id
local _, newGuid = UnitExists(unit)
local oldGuid = tracker.frameToGuid[frame]
if newGuid ~= oldGuid then
-- GUID changed = different player = need full update
tracker.frameToGuid[frame] = newGuid
frame.update_full = true
frame.update_aura = true -- Force aura refresh!
end
end
end
end
-- rebuild unitmap after frame IDs are assigned
if pfUI.uf.RebuildUnitmap then
pfUI.uf.RebuildUnitmap()
+320 -67
View File
@@ -161,15 +161,42 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
end
-- Add support for druid mana bars
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.player and pfUI_config.unitframes.druidmanabar == "1" then
local parent = pfUI.uf.player.power.bar
local config = pfUI.uf.player.config
local mana = config.defcolor == "0" and config.manacolor or pfUI_config.unitframes.manacolor
local r, g, b, a = pfUI.api.strsplit(",", mana)
local rawborder, default_border = GetBorderSize("unitframes")
local _, class = UnitClass("player")
local width = config.pwidth ~= "-1" and config.pwidth or config.width
-- Uses Nampower's GetUnitField to get base mana when in shapeshift form
local hasNampower = (GetUnitField ~= nil)
-- Add support for player secondary power bar (shows base mana when in shapeshift form)
-- Works with SuperWoW OR Nampower - both extend UnitMana() to return base mana as second value
-- Only for Druids (only class that can shapeshift in Vanilla)
-- Controlled by "Show Druid Mana Bar" setting
local _, playerClass = UnitClass("player")
if hasNampower and pfUI.uf and pfUI.uf.player and playerClass == "DRUID" and pfUI_config.unitframes.druidmanabar == "1" then
local rawborder, default_border = GetBorderSize("unitframes")
local config = pfUI.uf.player.config
-- Create secondary mana bar below the power bar
local playerMana = CreateFrame("StatusBar", "pfPlayerSecondaryMana", pfUI.uf.player)
playerMana:SetFrameStrata(pfUI.uf.player:GetFrameStrata())
playerMana:SetFrameLevel(pfUI.uf.player:GetFrameLevel() + 5)
playerMana:SetStatusBarTexture(pfUI.media[config.pbartexture])
-- Mana color
local manacolor = config.defcolor == "0" and config.manacolor or C.unitframes.manacolor
local r, g, b, a = pfUI.api.strsplit(",", manacolor)
playerMana:SetStatusBarColor(r, g, b, a)
-- Use SAME size as normal power bar (pwidth/pheight from config)
local width = config.pwidth ~= "-1" and config.pwidth or config.width
local height = config.pheight
playerMana:SetWidth(width)
playerMana:SetHeight(height)
playerMana:SetPoint("TOPLEFT", pfUI.uf.player.power, "BOTTOMLEFT", 0, -2*default_border - (config.pspace or 0))
playerMana:SetPoint("TOPRIGHT", pfUI.uf.player.power, "BOTTOMRIGHT", 0, -2*default_border - (config.pspace or 0))
playerMana:Hide()
CreateBackdrop(playerMana)
CreateBackdropShadow(playerMana)
-- Text overlay - same font settings as power bar
local fontname = pfUI.font_unit
local fontsize = tonumber(pfUI_config.global.font_unit_size)
local fontstyle = pfUI_config.global.font_unit_style
@@ -180,75 +207,285 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
fontstyle = config.customfont_style
end
local druidmana = CreateFrame("StatusBar", "pfDruidMana", UIParent)
druidmana:SetFrameStrata(parent:GetFrameStrata())
druidmana:SetFrameLevel(parent:GetFrameLevel() + 16)
druidmana:SetStatusBarTexture(pfUI.media[config.pbartexture])
druidmana:SetStatusBarColor(r, g, b, a)
druidmana:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 0, -2*default_border - config.pspace)
druidmana:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, -2*default_border - config.pspace)
druidmana:SetWidth(width)
druidmana:SetHeight(tonumber(pfUI_config.unitframes.druidmanaheight) or 6)
druidmana:EnableMouse(true)
druidmana:Hide()
playerMana.text = playerMana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
playerMana.text:SetFontObject(GameFontWhite)
playerMana.text:SetFont(fontname, fontsize, fontstyle)
playerMana.text:SetPoint("CENTER", playerMana, "CENTER", 0, 0)
-- Set text color like normal power bar (mana = type 0)
local tr, tg, tb = 1, 1, 1
if config["powercolor"] == "1" then
tr = ManaBarColor[0].r
tg = ManaBarColor[0].g
tb = ManaBarColor[0].b
end
if C.unitframes.pastel == "1" then
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
end
playerMana.text:SetTextColor(tr, tg, tb, 1)
UpdateMovable(druidmana)
CreateBackdrop(druidmana)
CreateBackdropShadow(druidmana)
druidmana:RegisterEvent("UNIT_MANA")
druidmana:RegisterEvent("UNIT_MAXMANA")
druidmana:RegisterEvent("UNIT_DISPLAYPOWER")
druidmana:SetScript("OnEvent", function()
if UnitPowerType("player") == 0 then
this:Hide()
-- Update function
local function UpdatePlayerSecondaryMana()
local powerType = UnitPowerType("player")
-- Only show when NOT using mana (i.e., in Bear/Cat form)
if powerType == 0 then
playerMana:Hide()
return
end
local _, mana = UnitMana("player")
local _, max = UnitManaMax("player")
local perc = math.ceil(mana / max * 100)
if perc == 100 then
this.text:SetText(string.format("%s", Abbreviate(mana)))
else
this.text:SetText(string.format("%s - %s%%", Abbreviate(mana), perc))
-- Get base mana using Nampower's GetUnitField
local baseMana, baseMaxMana
if GetUnitField then
local _, guid = UnitExists("player")
if guid then
baseMana = GetUnitField(guid, "power1")
baseMaxMana = GetUnitField(guid, "maxPower1")
end
end
-- Round down power values (Nampower can return decimals, especially for rage)
if baseMana then baseMana = math.floor(baseMana) end
if baseMaxMana then baseMaxMana = math.floor(baseMaxMana) end
if type(baseMana) ~= "number" or type(baseMaxMana) ~= "number" or baseMaxMana == 0 then
playerMana:Hide()
return
end
-- Update bar
playerMana:SetMinMaxValues(0, baseMaxMana)
playerMana:SetValue(baseMana)
-- Update text based on power text config (uses same setting as normal power bar)
-- Check txtpowercenter first (centered text), then txtpowerright, then txtpowerleft
local textConfig = config.txtpowercenter or config.txtpowerright or config.txtpowerleft
if not textConfig or textConfig == "" or textConfig == "none" then
-- No text configured - hide text
playerMana.text:SetText("")
elseif textConfig == "power" then
playerMana.text:SetText(Abbreviate(baseMana))
elseif textConfig == "powermax" then
playerMana.text:SetText(Abbreviate(baseMaxMana))
elseif textConfig == "powerperc" then
local perc = math.ceil(baseMana / baseMaxMana * 100)
playerMana.text:SetText(perc)
elseif textConfig == "powermiss" then
local miss = math.ceil(baseMana - baseMaxMana)
playerMana.text:SetText(miss == 0 and "0" or Abbreviate(miss))
elseif textConfig == "powerdyn" then
local perc = math.ceil(baseMana / baseMaxMana * 100)
if perc == 100 then
playerMana.text:SetText(Abbreviate(baseMana))
else
playerMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
end
elseif textConfig == "powerminmax" then
playerMana.text:SetText(string.format("%s/%s", Abbreviate(baseMana), Abbreviate(baseMaxMana)))
else
-- Default: show dynamic (value + percentage if not full)
local perc = math.ceil(baseMana / baseMaxMana * 100)
if perc == 100 then
playerMana.text:SetText(Abbreviate(baseMana))
else
playerMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
end
end
playerMana:Show()
end
-- Register events
playerMana:RegisterEvent("UNIT_MANA")
playerMana:RegisterEvent("UNIT_MAXMANA")
playerMana:RegisterEvent("UNIT_DISPLAYPOWER")
playerMana:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
playerMana:RegisterEvent("PLAYER_LOGOUT")
playerMana:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if arg1 == nil or arg1 == "player" then
UpdatePlayerSecondaryMana()
end
this:SetMinMaxValues(0, max)
this:SetValue(mana)
this:Show()
end)
druidmana.text = druidmana:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall")
druidmana.text:SetFontObject(GameFontWhite)
druidmana.text:SetFont(fontname, fontsize, fontstyle)
druidmana.text:SetPoint("RIGHT", -2*(default_border + config.txtpowerrightoffx), 0)
druidmana.text:SetPoint("LEFT", 2*(default_border + config.txtpowerrightoffx), 0)
druidmana.text:SetJustifyH("RIGHT")
-- Initial update
UpdatePlayerSecondaryMana()
if config["powercolor"] == "1" then
local r = ManaBarColor[0].r
local g = ManaBarColor[0].g
local b = ManaBarColor[0].b
if pfUI_config.unitframes.pastel == "1" then
druidmana.text:SetTextColor((r+.75)*.5, (g+.75)*.5, (b+.75)*.5, 1)
else
druidmana.text:SetTextColor(r, g, b, a)
end
end
if pfUI_config.unitframes.druidmanatext == "1" then
druidmana.text:Show()
else
druidmana.text:Hide()
end
if class ~= "DRUID" then
druidmana:UnregisterAllEvents()
druidmana:Hide()
end
-- Store reference
pfUI.uf.player.secondaryMana = playerMana
end
-- Add support for target secondary power bar (shows base mana when target is in shapeshift form)
-- Works with SuperWoW OR Nampower - both extend UnitMana() to return base mana as second value
-- Controlled by "Show Druid Mana Bar" setting - available for ALL classes
if hasNampower and pfUI.uf and pfUI.uf.target and pfUI_config.unitframes.druidmanabar == "1" then
local rawborder, default_border = GetBorderSize("unitframes")
local config = pfUI.uf.target.config
-- Create secondary mana bar below the power bar
local targetMana = CreateFrame("StatusBar", "pfTargetSecondaryMana", pfUI.uf.target)
targetMana:SetFrameStrata(pfUI.uf.target:GetFrameStrata())
targetMana:SetFrameLevel(pfUI.uf.target:GetFrameLevel() + 5)
targetMana:SetStatusBarTexture(pfUI.media[config.pbartexture])
-- Mana color
local manacolor = config.defcolor == "0" and config.manacolor or C.unitframes.manacolor
local r, g, b, a = pfUI.api.strsplit(",", manacolor)
targetMana:SetStatusBarColor(r, g, b, a)
-- Use SAME size as normal power bar (pwidth/pheight from config)
local width = config.pwidth ~= "-1" and config.pwidth or config.width
local height = config.pheight -- Same height as power bar!
targetMana:SetWidth(width)
targetMana:SetHeight(height)
targetMana:SetPoint("TOPLEFT", pfUI.uf.target.power, "BOTTOMLEFT", 0, -2*default_border - (config.pspace or 0))
targetMana:SetPoint("TOPRIGHT", pfUI.uf.target.power, "BOTTOMRIGHT", 0, -2*default_border - (config.pspace or 0))
targetMana:Hide()
CreateBackdrop(targetMana)
CreateBackdropShadow(targetMana)
-- Text overlay - same font settings as power bar
local fontname = pfUI.font_unit
local fontsize = tonumber(pfUI_config.global.font_unit_size)
local fontstyle = pfUI_config.global.font_unit_style
if config.customfont == "1" then
fontname = pfUI.media[config.customfont_name]
fontsize = tonumber(config.customfont_size)
fontstyle = config.customfont_style
end
targetMana.text = targetMana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
targetMana.text:SetFontObject(GameFontWhite)
targetMana.text:SetFont(fontname, fontsize, fontstyle)
targetMana.text:SetPoint("CENTER", targetMana, "CENTER", 0, 0)
-- Set text color like normal power bar (mana = type 0)
-- Uses powercolor setting and pastel effect
local function UpdateTextColor()
local tr, tg, tb = 1, 1, 1
if config["powercolor"] == "1" then
tr = ManaBarColor[0].r
tg = ManaBarColor[0].g
tb = ManaBarColor[0].b
end
if C.unitframes.pastel == "1" then
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
end
targetMana.text:SetTextColor(tr, tg, tb, 1)
end
UpdateTextColor()
-- Update function
local function UpdateTargetSecondaryMana()
if not UnitExists("target") then
targetMana:Hide()
return
end
local powerType = UnitPowerType("target")
-- Only show if target is NOT using mana (i.e., in shapeshift form with energy/rage)
if powerType == 0 then
-- Target is using mana as primary power - no need for secondary bar
targetMana:Hide()
return
end
-- Get base mana using Nampower's GetUnitField
local baseMana, baseMaxMana
if GetUnitField then
local _, guid = UnitExists("target")
if guid then
baseMana = GetUnitField(guid, "power1")
baseMaxMana = GetUnitField(guid, "maxPower1")
end
end
-- Round down power values (Nampower can return decimals, especially for rage)
if baseMana then baseMana = math.floor(baseMana) end
if baseMaxMana then baseMaxMana = math.floor(baseMaxMana) end
-- Check if we got valid mana values
if type(baseMana) ~= "number" or type(baseMaxMana) ~= "number" or baseMaxMana == 0 then
targetMana:Hide()
return
end
-- Update bar
targetMana:SetMinMaxValues(0, baseMaxMana)
targetMana:SetValue(baseMana)
-- Update text based on power text config (uses same setting as normal power bar)
local textConfig = config.txtpowercenter or config.txtpowerright or config.txtpowerleft
if not textConfig or textConfig == "" or textConfig == "none" then
targetMana.text:SetText("")
elseif textConfig == "power" then
targetMana.text:SetText(Abbreviate(baseMana))
elseif textConfig == "powermax" then
targetMana.text:SetText(Abbreviate(baseMaxMana))
elseif textConfig == "powerperc" then
local perc = math.ceil(baseMana / baseMaxMana * 100)
targetMana.text:SetText(perc)
elseif textConfig == "powermiss" then
local miss = math.ceil(baseMana - baseMaxMana)
targetMana.text:SetText(miss == 0 and "0" or Abbreviate(miss))
elseif textConfig == "powerdyn" then
local perc = math.ceil(baseMana / baseMaxMana * 100)
if perc == 100 then
targetMana.text:SetText(Abbreviate(baseMana))
else
targetMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
end
elseif textConfig == "powerminmax" then
targetMana.text:SetText(string.format("%s/%s", Abbreviate(baseMana), Abbreviate(baseMaxMana)))
else
local perc = math.ceil(baseMana / baseMaxMana * 100)
if perc == 100 then
targetMana.text:SetText(Abbreviate(baseMana))
else
targetMana.text:SetText(string.format("%s - %s%%", Abbreviate(baseMana), perc))
end
end
targetMana:Show()
end
-- Register events
targetMana:RegisterEvent("PLAYER_TARGET_CHANGED")
targetMana:RegisterEvent("UNIT_MANA")
targetMana:RegisterEvent("UNIT_MAXMANA")
targetMana:RegisterEvent("UNIT_DISPLAYPOWER")
targetMana:RegisterEvent("PLAYER_LOGOUT")
targetMana:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "PLAYER_TARGET_CHANGED" then
UpdateTargetSecondaryMana()
elseif arg1 == "target" then
UpdateTargetSecondaryMana()
end
end)
-- Store reference
pfUI.uf.target.secondaryMana = targetMana
end
-- Add support for guid based focus frame
if SUPERWOW_VERSION and pfUI.uf and pfUI.uf.focus then
local focus = function(unitstr)
@@ -312,8 +549,16 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
trackFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
trackFrame:RegisterEvent("RAID_ROSTER_UPDATE")
trackFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
trackFrame:RegisterEvent("PLAYER_LOGOUT")
trackFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
-- Track party members
for i = 1, 4 do
local unit = "party" .. i
@@ -483,7 +728,15 @@ pfUI:RegisterModule("superwow", "vanilla", function ()
supercast:RegisterEvent("PLAYER_ENTERING_WORLD")
supercast:RegisterEvent("UNIT_CASTEVENT")
supercast:RegisterEvent("PLAYER_LOGOUT")
supercast:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "PLAYER_ENTERING_WORLD" then
-- Cache player GUID
if UnitExists then
+25
View File
@@ -90,7 +90,24 @@ pfUI:RegisterModule("unitxp", "vanilla", function ()
-- Also try on PLAYER_ENTERING_WORLD in case target frame wasn't ready
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:RegisterEvent("PLAYER_LOGOUT")
initFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
-- Stop indicator OnUpdate scripts
if pfUI.uf and pfUI.uf.target then
if pfUI.uf.target.behindIndicator then
pfUI.uf.target.behindIndicator:SetScript("OnUpdate", nil)
end
if pfUI.uf.target.losIndicator then
pfUI.uf.target.losIndicator:SetScript("OnUpdate", nil)
end
end
return
end
CreateTargetIndicators()
this:UnregisterAllEvents()
end)
@@ -102,8 +119,16 @@ pfUI:RegisterModule("unitxp", "vanilla", function ()
notifyFrame:RegisterEvent("CHAT_MSG_BN_WHISPER")
notifyFrame:RegisterEvent("READY_CHECK")
notifyFrame:RegisterEvent("RAID_INSTANCE_WELCOME")
notifyFrame:RegisterEvent("PLAYER_LOGOUT")
notifyFrame:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
pcall(UnitXP, "notify", "taskbarIcon")
pcall(UnitXP, "notify", "systemSound")
end)
+2 -2
View File
@@ -1,9 +1,9 @@
## Interface: 20400
## Title: |cff33ffccpf|cffffffffUI
## Author: Shagu
## Author: Shagu - modfied by me0wg4ming
## Notes: A complete user interface replacement.
## Notes-ruRU: Полная замена пользовательского интерфейса.
## Version: 6.2.6
## Version: 7.6.0 (experiment version)
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB
+5
View File
@@ -47,6 +47,11 @@ pfUI.version = {}
pfUI.hooks = {}
pfUI.env = {}
-- check if macro addons are loaded (disables macrotweak/macroscan)
function pfUI:MacroAddonsLoaded()
return IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros")
end
-- detect current addon path
local tocs = { "", "-master", "-tbc", "-wotlk" }
for _, name in pairs(tocs) do
+2 -2
View File
@@ -1,9 +1,9 @@
## Interface: 11200
## Title: |cff33ffccpf|cffffffffUI
## Author: Shagu
## Author: Shagu - modfied by me0wg4ming
## Notes: A complete user interface replacement.
## Notes-ruRU: Полная замена пользовательского интерфейса.
## Version: 6.2.6
## Version: 7.6.0 (experiment version)
## SavedVariables: pfUI_profiles, pfUI_addon_profiles, pfUI_cache
## SavedVariablesPerCharacter: pfUI_config, pfUI_init, pfUI_playerDB