81 Commits

Author SHA1 Message Date
Brues 7bbb8df010 remove pre pfUI 7.6 stuff 2026-09-11 00:27:05 -05:00
Brues 96dbe9aca8 Fail @focus clauses quietly when no focus is set
`[@focus,...] X` with no focus set printed "Invalid target" before falling
through to the next clause, which the new `[@mouseover][@focus][] X` idiom
turned into a message on every press. The noise came from a pre-ClassicAPI
name-based fallback in DoWithConditionals: with no native focus token,
TryTargetFocus resolved UnitName('focus') to nil and could never succeed,
so the branch only ever reached the error print. GetFocusUnitId's own
contract already says @focus should fall through silently.

Both evaluators now fail the clause quietly when GetFocusUnitId is nil,
the same way @mouseover does, so display and execution agree (TestAction
previously evaluated against "target" in that case). GetFocusName,
TryTargetFocus and pfUI's FocusNameHook were only reachable through that
path and are removed.
2026-09-10 17:35:49 -05:00
Brues 1de455f96e Support Blizzard-style bracket groups: [a][b] Spell and [] fall-through
A clause may now carry a leading run of [group] blocks sharing one action;
groups are OR'd, first pass wins, and [] always passes. The existing
;-separated single-block form is unchanged and mixes freely.

The OR loop lives in the two single-clause evaluators, DoWithConditionals
and TestAction, which expand a multi-group clause into one-group variants
and recurse. That covers every ;-split site, /castsequence (where ; is not
a separator and the sequence is keyed by the full args), /cancelaura,
/stopmacro and the editor highlight without rewriting strings, so
action.args, the ParsedMsg/Sequences caches and highlight offsets keep
their identity.

ParseMsg scans the leading group run (quote-aware) instead of the greedy
%[(.+)%], which previously turned [a][b] into the bogus key a][b and made
such clauses fail silently on both the cast and icon paths. The flag
pattern still runs on the whole clause when there are no groups, so !Spell
is byte-identical.

TestAction reports which variant passed and TestForActiveAction stores its
conditionals, so range/usable colouring follows the passing group's @unit
rather than group 1's. /target gains ; and group support; /pfcast injects
the resolved unit into every group, not just the first. The macro checker
validates every leading block, drops the EMPTY_CONDITIONAL error, and
catches a missing action after the last group.
2026-09-10 17:17:08 -05:00
Brues d93d97a469 Delete the write-only buff tracking tables and the overflow fallback
Following the aura data source to ClassicAPI exposed a chain of machinery that
nothing consumed any more.

ValidateAura's non-player overflow fallback is gone. It existed because a buff
can occupy server slots 33-48 with no client slot, so it read presence and
duration out of AllCasterAuraTracking behind a UnitDebuff slot scan that stopped
[buff:Name] matching a debuff. ClassicAPI settles this at the source: its
HELPFUL/HARMFUL filters select on each aura's real polarity flag rather than the
slot range it happens to sit in, so per docs/API.md "a debuff parked in a buff
slot still reads harmful" -- and an overflowed buff still reads helpful. The
ClassicAPI resolution already covers the case, and classifies it more accurately
than the slot-range guard did.

That removed the last caller of FindAllCasterAuraByName (67 lines), and the
player-side allBuffAuras timing lookup went with it: for the player, C_Spell...
C_UnitAuras reads expirationTime out of the engine's own player-buff table, so it
is the more authoritative source rather than a fallback.

With no readers left, three tables turn out to be pure overhead -- populated on
every buff aura event, swept periodically, cleared on removal and death, and
never read for aura state:
  - lib.allBuffAuras   [guid][name][caster] = {startTime, duration, rank}
  - lib.ownBuffCasts   [guid][name]         = {startTime, duration, spellId, ...}
  - lib.pendingBuffCasts [guid][spellId]    = {casterGuid, duration, name, time}
pendingBuffCasts only existed to correlate AURA_CAST_ON_OTHER with
BUFF_ADDED_OTHER so the other two could be filled, so removing them emptied the
BUFF_ADDED_OTHER handler entirely. That event is no longer registered, and pfUI
compat no longer unregisters an event we never ask for.

Fixes a regression from the previous commit in this series: removing the
allBuffAuras/ownBuffCasts blocks from BUFF_REMOVED_SELF and BUFF_REMOVED_OTHER
also deleted their `local spellName = C_Spell.GetSpellName(spellId)`, leaving
later `if spellName` guards reading a nil global. The OverflowBuffsByName and
AllCasterAuraTracking prunes in those branches had silently stopped running.
Both declarations are restored.

AllCasterAuraTracking itself stays: OverflowBuffFrame and the aura-tracking
writers still use it.
2026-09-10 14:40:16 -05:00
Brues 2a562e0550 Make ClassicAPI the source of truth for non-player aura timing
ValidateAura resolved remote aura timers from libdebuff's allBuffAuras and then
from AllCasterAuraTracking via FindAllCasterAuraByName. Both existed for one
reason: vanilla cannot report a timer for an aura on a unit other than the
player. ClassicAPI's Aura::Source cache does, reconstructing duration and
expirationTime for any unit from the observed SMSG_SPELL_GO -- caster-modified,
so talent extensions and combo-point finisher scaling are already applied, and
Carnage's roll-gated Rip/Rake refresh is handled in the DLL rather than mirrored
in Lua.

Replaced that ~70-line cascade with ResolveUnitAuraViaClassicAPI, a positional
C_UnitAuras.UnitAura scan over the relevant range. Matching stays ours -- by
spellID when the conditional supplied a number, else by lowercased name, since
C_UnitAuras' own by-name lookup is case-sensitive and exact and would miss
[debuff:thunder_clap]. It runs after the existing scans and overrides them, which
is what "source of truth" means here.

Also drops the libdebuff UnitBuff timeleft bug the old comments worked around
(timeleft reporting ~1000s), since nothing reads it on this path any more.

Presence and stacks from C_UnitAuras are reliable. Timing is best-effort by
design: an aura cast before login, or one refreshed at max stacks, yields no
expirationTime, and remaining is left nil so the caller keeps its found-and-0
default. A permanent aura (duration 0) reports -1, matching the existing
convention.

Still on the old machinery and not yet migrated: the non-player overflow-slot
buff path (AllCasterAuraTracking, guarded by a debuff-slot check) and the player
branches. AllCasterAuraTracking still has 56 references across 5 files, mostly
OverflowBuffFrame and Conditionals.
2026-09-10 14:40:16 -05:00
Brues daaab2fd11 Retire the hasPfUI76 flag and the pfUI 7.6 branches it gated
Assume a modern pfUI, and drop the version-tiered branching entirely: 31 call
sites across five files, plus the flag and its detector.

Note which side was actually dead. HasPfUI76() required four things -- pfUI
version >= 7.6, Nampower >= 2.40, pfUI.libdebuff_objects_guid, and
pfUI.libdebuff_casts. The last one does not exist: pfUI exports 28 libdebuff_*
tables and libdebuff_casts is not among them (libdebuff_recent_casts is a
dedup table keyed [targetGuid][spellName][casterGuid], a different shape). So
the detector returned false at any version, and it was the hasPfUI76-TRUE
branches that never ran, not the fallbacks. Removing those is behaviour-
preserving; keeping SCRM as the owner of castTracking is what the code has
always actually done.

Removed accordingly:
- lib:HasPfUI76(), both flag declarations, and the block that would have aliased
  castTracking to pfUI.libdebuff_casts and re-pointed lib.objects/iconCache.
- HookPfUILibdebuff's "7.6 handles durations internally" early return, so the
  GetDuration/AddEffect hooks below it are visibly the live path again.
- The SPELL_START_OTHER / SPELL_FAILED_OTHER unregister arm, the pfUI branch of
  GetAuraTrackingData, the pfUI76 arm of the libdebuff zone/death cleanup, the
  pfUI backing table in the /cleveroid aura dump, and two status-string suffixes.
- SyncComboDurationToPfUI (77 lines) with its two call sites: it returned
  immediately whenever hasPfUI76, and is inert once the tier split is gone.

Unconditionalised the paths that were gated on `not hasPfUI76`: the castTracking
populate/clear/sweep, InitPfUIIntegration on login, and ten libdebuff duration
lookups.

Verified no hasPfUI76 reference remains, and that per-file block balance is
unchanged from HEAD.
2026-09-10 14:40:16 -05:00
Brues 43f1ba378a use brues-code/packager@vCAPI for packaging 2026-09-10 14:40:16 -05:00
Brues 828acc0c4b Remove two silently-shadowed code paths
Both were invisible: no error, no warning, just logic that never ran.

Console.lua no longer defines the global RunMacro. Core.lua installs a hook of
the same name and loads first, so this file's name-only wrapper replaced it. What
that cost, whenever SuperMacro was absent: macro *index* arguments stopped
working (Blizzard's RunMacro accepts an index or a name, and ExecuteMacroByName
takes only a name), the delegation to SuperMacro went away, so did the fallback
to the saved Blizzard original when a macro would not resolve, and so did
clearing the stop/skip flags at the start of a top-level run -- which is what
lets /stopmacro, /skipmacro and /firstaction work across parent/child macro
boundaries. With SuperMacro loaded, Compatibility/SuperMacro.lua reassigns the
global afterwards anyway, so this definition only ever did harm.
SlashCmdList.RUNMACRO keeps using ExecuteMacroByName, which is correct there.

Utility.lua drops ClearReactiveProcOnCast and the wrappers it existed for, which
tried to clear a reactive proc when its spell was cast. Three independent bugs
meant it never worked: the wrappers guarded on CleveRoids.Frame, which Core.lua
creates after this file loads, so the guard was always false and neither wrapper
installed; the UNIT_CASTEVENT wrapper tested arg2 == "START" when that signature
is (caster, target, action, spell_id, cast_time), so it compared the target; and
both passed a spell ID to a lookup keyed by spell name. They also allocated an
arg table per call and unpack()'d it. Deleting is behaviour-preserving.

Swept for both patterns: no other duplicate global definitions, and no remaining
file-scope use of CleveRoids.Frame in a file that loads before Core.lua.
2026-09-10 14:40:16 -05:00
Brues f296425c94 Cut per-call allocations in the event and publish paths; merge SPELL_CAST_EVENT
Chasing high memory churn (~188 kb/s with pfUI). Four allocation sources and one
correctness bug found along the way.

The recurring cause: in 1.12's Lua 5.0 a function declared `function(...)` builds
a fresh `arg` table on every call. Three handlers were declared that way and none
of them read the vararg.

- Compatibility/pfUI.lua: the registered action handler. This was the dominant
  one. UpdateAllManagedCooldowns fans ACTIONBAR_UPDATE_COOLDOWN across every
  managed slot (up to 120) on each SPELL_UPDATE_COOLDOWN, so every GCD and
  cooldown tick allocated ~120 tables that the handler then discarded, because
  its whole body only ever applied to ACTIONBAR_SLOT_CHANGED. Dropped the vararg
  and added an early return before any work. Bongos' and UltimaMacros' handlers
  were already vararg-free.

- Core.lua SendEventForAction: dropped the vararg (every caller passes exactly
  one extra value) and replaced the inline "arg" .. i concatenations -- 30 per
  call across three loops -- with a prebuilt name table. The eight-branch arg.n
  fan-out collapses to one loop.

- Core.lua Frame OnEvent dispatcher: dropped the vararg. It fires for all ~48
  registered events, including the UNIT_HEALTH / UNIT_AURA / UNIT_*_GUID streams.

PublishDisplay now skips no-op publishes. Publishing is not free: the client
repaints holders of the macro through its own notifier, which returns as
ACTIONBAR_SLOT_CHANGED -> ClearAction + IndexActionSlot -> TestForActiveAction ->
publish. Publishing an unchanged value therefore tore down and re-resolved slots
for nothing and fed itself, since GetAction and IndexActionSlot both publish on
paths that fire constantly. This was the C_Macro.SetMacroDisplay regression.

Also merges the two same-named SPELL_CAST_EVENT handlers. Lua assigns in file
order, so the later definition silently replaced the earlier one and its half
never ran: channel-start detection, spell_tracking cleanup, and cast-sequence
advancement. AdvanceSequence has only two callers and the other is inside
UNIT_CASTEVENT, which is registered only under SuperWoW -- so /castsequence had
no way to advance on a successful cast for Nampower-only users.
2026-09-10 14:40:16 -05:00
Brues 4dd5326172 Drop the action-bar overrides now that ClassicAPI draws macro buttons
Removes the nine Lua replacements the macro display integration makes
redundant: GameTooltip.SetAction, GetActionCooldown, GetActionCount,
IsConsumableAction, IsUsableAction, IsActionInRange, ActionHasRange,
IsCurrentAction and GetActionTexture. The client now derives all of it from the
value published via C_Macro.SetMacroDisplay, including the drag cursor and macro
window grid that Lua could never reach, and answers range for the macro slot
itself instead of via a borrowed proxy slot. Also drops GetSlotMacroTexture,
which only GetActionTexture used.

Behavior change: a macro with a hand-picked icon now keeps that icon. The
deleted GetActionTexture substituted the active action's icon unconditionally;
the client only substitutes when the macro's own icon is the question mark.
That is the engine's rule, matching 3.3.5's macro icon getter.

Two things the integration guide expected to fall out did not, and are kept:

- GetProxyActionSlot still has six callers unrelated to macro display
  (/startattack, [stopattack], and [channeled] for Attack/Auto Shot/Shoot).
  Only three of its callers lived in the deleted range.
- Hooks.IsCurrentAction and Hooks.OriginalIsUsableAction are used outside the
  overrides they were saved for, so the aliases stay. They now simply name the
  unmodified globals. The auto-attack drift check specifically needs the real
  current-action state, which the old override could not give it.

PickupAction is untouched: it invalidates our per-slot caches on drag, which is
not display and is still needed.
2026-09-10 14:40:16 -05:00
Brues c5ddc6c313 Publish resolved macro actions to ClassicAPI's C_Macro.SetMacroDisplay
Hands ClassicAPI the action this addon resolved for each macro, so the icon,
tooltip, cooldown sweep, range and usable state come from the client instead of
from replaced action-bar globals. That also reaches the drag cursor and the macro
window grid, which Lua cannot touch, and lets the client answer range for a macro
slot directly rather than borrowing a proxy slot.

- CleveRoids.useClassicAPIDisplay feature-detects C_Macro.SetMacroDisplay rather
  than checking CLASSIC_API_VERSION, which reports a dev sentinel while the API is
  unreleased. ClassicAPIMacroDisplay tells ClassicAPI we drive macro display, which
  it otherwise stands down from when it sees this addon.
- macro.actions.macroID back-references the Blizzard index, since the update loop
  walks actions objects rather than macros. nil for SuperMacro macros, which have
  no index and so cannot be published.
- PublishDisplay sends false, not nothing, when no action matched: that claims the
  macro and shows the question mark, where silence would hand it back to
  ClassicAPI's own #showtooltip parser.
- PublishAllDisplays runs once the addon is ready and after every re-parse, since
  ClassicAPI re-evaluates nothing for us. It covers macros that aren't on a bar,
  which is what keeps the macro window grid correct. Deferred past load because
  SetMacroDisplay returns false until the player is in the world.
- ReleaseDisplays hands every macro back on DisableAddon.

The per-slot ACTIONBAR_SLOT_CHANGED fan-out is kept for the non-integrated path.
Publishing repaints every slot holding the macro through the client's own notifier,
so it is redundant when the API is present, but removing it outright would leave
buttons never repainting on a ClassicAPI build without the API.
2026-09-10 14:40:16 -05:00
liiora 9f8a5553bc feat: add /stopchanneling command (#5)
* feat: add /stopchannel command

* review: give /stopchanneling a real handler and warn when nampower can't honor it

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-09-10 14:39:16 -05:00
Brues 00caff0c9b Bump the README conditional count to match the wiki 2026-09-10 14:24:46 -05:00
liiora 080c1243b6 feat: add myspellhaste conditional (#4)
* feat: add myspellhaste conditional

* review: fix myspellhaste proxy fallback, document it, register with macrocheck

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-09-10 14:06:27 -05:00
Brues 6f78be8394 Detect the relic slot via UnitHasRelicSlot instead of hardcoded classes
IsRelicSlot no longer tests playerClass against PALADIN/DRUID/SHAMAN. It now
reads CleveRoids.hasRelicSlot, sampled once in PLAYER_LOGIN.

Also folds PerformEquipSwap's slot-18 check into the same helper. Its class list
(HUNTER/WARRIOR/ROGUE/MAGE/WARLOCK/PRIEST) was the exact complement of the relic
classes, so it becomes `not IsRelicSlot(inventoryId)` and the two lists can no
longer drift apart.
2026-09-09 12:50:36 -05:00
Brues 10ad9eb980 Add pkgmeta and luarc 2026-09-09 12:46:48 -05:00
Artur Morozov 0ef4fe1818 add support for 'button' conditional (#3)
* add support for 'button' conditional

* Route [button] through Multi, add [nobutton], register as static

- [button] now reads conditionals.button via Multi(...) instead of indexing
  _groups.button[1].values[1] directly, so OR/AND lists and repeated groups
  behave like every other argument conditional. [button:1/2] now matches left
  or right; previously only the first value was ever checked and the rest were
  silently dropped. This also removes the unguarded _groups reach-in, which
  Multi itself defends against (it checks _groups is present and falls back to
  _operators).

- Adds [nobutton:N] via NegatedMulti, following the convention that every
  conditional ships its negation (nomod, nostance, nokeydown, ...).

- Bare [button] / [nobutton] now mean "any / no mapped mouse button held",
  mirroring bare [mod]. Bare [nobutton] doubles as an "activated by a keybind
  rather than a click" test.

- Registers button/nobutton in STATIC_CONDITIONALS next to mod and keydown, so
  they are checked once up front rather than re-evaluated for every candidate
  unit during multiscan target scanning.

Still open: IsMouseButtonDown reports live physical state, so a keybound macro
has no button held and [button:N] is false for it, and whether the button still
reads down at macro-execution time depends on whether the action button fires on
mouse-down or mouse-up. Needs in-game verification.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-09-09 02:51:51 -05:00
Brues 269c6ba67e Fix the same 0-based slot assumption in the equipment Lua fallbacks
#2 corrected IsItemInSlot's native path, but the same 0-based assumption was
still present in the two Lua fallbacks, which run on nampower < v2.18. Per
nampower's SCRIPTS.md the slot is 1-19 (16=MainHand, 17=OffHand), and
ClassicAPI's GetInventoryItemID is 1-based as well:

- GetEquippedItem's fallback called GetInventoryItemID(slot + 1), contradicting
  both its own native path and its doc comment.
- GetEquippedItems' fallback built 0-18 keys while the native path returns
  1-19, so the two branches handed back differently-keyed tables.

Neither wrapper currently has any callers, so this was latent rather than
user-visible. Also records the 1-indexed convention at the IsItemInSlot call
site, where the misleading "0-indexed" comment used to be.
2026-09-09 02:40:57 -05:00
Sami Lappalainen c7905c2a70 fix for off-by-one error (#2) 2026-09-09 02:38:57 -05:00
Brues 77c0dba2c4 Alias [mouseuse] to [cursor] via a conditional alias table
Adds CleveRoids.conditionalAliases, applied in ParseMsg so a deprecated name
is rewritten to its current keyword before anything downstream (evaluation,
_groups) sees it. This is the mechanism for renamed modifiers, which -- unlike
predicates such as [stl] -> [stealth] -- have no Keywords function an alias
could simply point at.

[mouseuse] now resolves to [cursor], routing old macros through ClassicAPI's
C_Spell.CastAtCursor / C_Item.UseAtCursor. Drops the post-cast block that cast
normally and then faked a mouse click via CameraOrSelectOrMoveStart/Stop to
place the AoE circle, along with its workaround for that call spuriously
starting auto-attack.

MacroErrorChecker seeds VALID_CONDITIONALS from the alias table, so the old
names stay valid syntax without needing a placeholder ignoreKeywords entry.
2026-09-09 02:18:36 -05:00
Brues 61bab6c734 Require ClassicAPI v1.12.1; scan dispel auras via positional UnitAura
scanDispel now reads each slot through the positional C_UnitAuras.UnitAura
(added in ClassicAPI v1.12.1) instead of the table-return GetAuraDataByIndex,
dropping a throwaway AuraData table per slot on the up-to-48-slot scan behind
the [magic]/[curse]/[disease]/[poison]/[dispellable] conditionals.

Since UnitAura is called directly with no fallback, gate on it: the load-time
check in Core.lua warns (matching its warn-don't-disable design) when
ClassicAPI is older than v1.12.1, wiring up the previously-unused
ClassicAPI.HasMinimumVersion.
2026-09-02 14:40:21 -05:00
brues-code daa7d6f2bf Merge pull request #1 from Seraphic8x2244/feedpet-pethappiness
new command /feedpet, new conditional [pethappiness]
2026-08-25 09:43:41 -05:00
Brues 6d73c6b006 Localize Feed Pet via spell ID and fix IsReactive param
- Resolve Feed Pet from spell ID 6991 (C_Spell.GetSpellName) so /feedpet works on non-enUS clients without a Localized.Spells entry
- Fix CleveRoids.IsReactive referencing undefined global spellName instead of its name parameter
2026-08-25 09:42:36 -05:00
Seraphic8x2244 d5baecaf41 new command feedpet, new conditional pethappiness 2026-08-25 09:26:59 -05:00
Brues 5c31232b52 LoadSavedVariablesFirst 2026-08-23 14:40:01 -05:00
Brues d5cae2136c added CleveRoids_LearnedDurations nil check 2026-08-23 14:33:29 -05:00
Brues 004eebc9f7 Coalesce macro/spell rebuilds to cut the login event storm
UPDATE_MACROS and SPELLS_CHANGED each did a full spell + talent + pet +
120-slot action-bar rebuild inline, and both fire several times during
login as the spellbook and macros populate - so the rebuild ran 4-6
times in the first ~2s, most of it duplicated work fanning 120 button
updates out to Blizzard/pfUI/Bongos each pass.

Defer the rebuild instead: UPDATE_MACROS, SPELLS_CHANGED and PLAYER_LOGIN
arm a 0.3s debounce (macroRebuildTime); the update loop runs RebuildMacros
once when the burst settles. RebuildMacros skips the action-bar pass until
`ready` (GetAction early-returns before then, and the +1.5s init timer
builds the bars once anyway), and BAG_UPDATE_DELAYED's bar rebuild is
gated on `ready` too. Net: IndexActionBars runs once at login (the
suppressed init-timer pass) instead of 4-6 times, IndexSpells ~2x instead
of ~5x. Runtime macro edits and bag changes still do a full unsuppressed
rebuild, just debounced.
2026-08-21 21:39:11 -05:00
Brues 63b484b1ea Replace WDB tooltip warmup with GET_ITEM_INFO_RECEIVED handler
ClassicAPI hooks the global GetItemInfo to auto-warm the item cache on a
miss and fires GET_ITEM_INFO_RECEIVED when the async fill lands, so the
tooltip-scan warmup is obsolete: IndexItems's own GetItemInfo calls
already trigger the same warmup, and owned items (bags + equipped) are
priority-prefetched by the engine. The old warmup also assumed the fill
was synchronous, which no longer holds.

Drop DoWDBWarmup and its login scheduling; instead listen for
GET_ITEM_INFO_RECEIVED and run a debounced re-index. IndexItems records
owned itemIDs it could not resolve into pendingItemInfo, and the handler
ignores any fill not in that set (quest DB scans, AH sweeps, chat-link
hovers, inspects) in O(1) so unrelated bursts do not cause reindex churn.
2026-08-21 21:02:59 -05:00
Brues cf47d8caba Use SetSpellByID for tooltip 2026-08-06 00:33:12 -05:00
Brues 71f8f74237 Support #showtooltip spell:<id> and item:<id>
GetSpell/GetItem now resolve explicit spell:/item: ID forms. spell:<id>
prefers the player's spellbook entry via FindSpellBookSlotByID (per-rank
and pet aware) for full cost/cooldown/usability, falling back to an
id-only entry rendered via SetSpellByID for spells not in the book.
item:<id> reuses the existing numeric lookup for location-aware tooltips.

Guard the id-only path against nil spellSlot/cost in TestForActiveAction
and GetActionCooldown so display-only spell references don't crash.

Remove the dead, no-op GetSpellSlotByID stub.
2026-08-06 00:15:19 -05:00
Brues 1a98a05b7e Remove redundant local 'i' declarations
Delete unnecessary local 'i' declarations in Extensions/Mouseover/pfUI.lua (ResolvePfUnit, RegisterPartyScripts, RegisterRaidScripts, RegisterRaidMarkScripts). The for-loop headers already provide a local loop variable, so the explicit locals were redundant and could shadow variables. No functional change.
2026-08-02 15:18:48 -05:00
Brues 64e3f93042 Specifically disable pfUI mouseover hooks when unit attribute exists 2026-08-02 15:11:46 -05:00
Brues 0c86ed67e9 Revert "Drop redundant pfUI mouseover frame hooks; pfUI sets native mouseover"
This reverts commit c5bc69560e.
2026-08-02 15:07:35 -05:00
Brues 60c1b7f235 dont bother hooking into brues-code pfUI 2026-08-01 20:21:37 -05:00
Brues 8c10353567 removed useless GetSpellRecField checks 2026-07-30 20:41:23 -05:00
Brues fcd206756d Replace equipment cache with C_Item.IsEquippedItem
HasGearEquipped now wraps ClassicAPI's native slot-walk, which
short-circuits on first match. Removes BuildEquipmentCache, the
_equipped* cache tables/invalidation, and the InvalidateEquipmentCache
call from PLAYER_EQUIPMENT_CHANGED. [equipped] reads live engine state,
so no cache staleness surface remains; also gains item-link support.
2026-07-30 20:35:39 -05:00
Brues 8d86389a03 Remove dead IndexEquippedItems (no callers) 2026-07-30 20:28:39 -05:00
Brues 8c85ce572a Scope PLAYER_EQUIPMENT_CHANGED to the changed slot
Replace the full IndexItems + action-bar rebuild with a single-slot
IndexEquipSlot using the event's arg1 (slot) and arg2 (hasCurrent),
and drop the now-pointless throttle/deferral. Bag-side deltas remain
covered by BAG_UPDATE_DELAYED; [equipped] runs off its own cache.
2026-07-30 20:28:05 -05:00
Brues a2db3ebd42 Add indoors/outdoors conditionals via ClassicAPI 2026-07-30 19:59:50 -05:00
Brues 66d5f0c618 Switch to PLAYER_EQUIPMENT_CHANGED event
Replace UNIT_INVENTORY_CHANGED with PLAYER_EQUIPMENT_CHANGED in event registration and handler
2026-07-30 19:59:38 -05:00
Brues 7cf2ad95c3 Use BAG_UPDATE_DELAYED 2026-07-30 19:51:24 -05:00
Brues c5bc69560e Drop redundant pfUI mouseover frame hooks; pfUI sets native mouseover
pfUI.uf:EnableScripts binds pfUI.uf.OnEnter to every unitframe, which calls
Nampower's SetMouseoverUnit -- so the native "mouseover" token already resolves
to the hovered pfUI frame (player/target/party/raid/focus/pettarget/partytarget/
...). Every conditional consumer checks UnitExists("mouseover") before the
CleveRoids.mouseoverUnit fallback, so the 10 per-frame OnEnter/OnLeave hookers
here were pure duplication (they even chained pfUI's OnEnter).

Removed all of them. Kept RegisterRaidMarkScripts (raid-marker rows aren't
unitframes, so pfUI sets no mouseover for them -> still needed for mark1..mark8)
and HookPfCast (unrelated /pfcast conditional wrapper), plus the PfSet/PfClear/
resolve helpers the raid-mark path uses.
2026-07-30 00:28:00 -05:00
Brues ce10c9f456 Derive membership-only spell families by name, not hardcoded rank lists
Rip/Rake/Conflagrate/MoltenBlast/DarkHarvest were hardcoded {rankID=true} sets
used only for membership tests. Replaced with a metatable-backed RankSet(seedID):
t[spellID] name-matches the spellID against one seed rank via C_Spell.GetSpellName,
which resolves every rank (no enumeration) and TWoW's custom spells (confirmed
36916 -> 'Molten Blast' on the TWoW client). Read shape is unchanged, so all
consumers work untouched. Locale-safe (name derived from the seed ID). Custom
seeds absent on a stock client resolve to nil and simply never match -- correct,
those spells can't be cast there.

Left hardcoded: the pairs()-iterated sets (FerociousBite/FlameShock/Immolate),
BleedSpellIDs (Pounce cast/bleed name collision), ComboScalingSpellsByID (carries
base/increment), PounceToBleedMapping (trigger relationship), MobsThatBleed (GUIDs).
2026-07-28 12:21:38 -05:00
Brues 08dd3ba4dd Remove dead code from ComboPointTracker
PounceBleedSpellIDs, GetLastComboPointsForSpell, and GetLastDurationForSpell
were each definition-only with zero references anywhere in the addon. The
Pounce bleed IDs are still covered by BleedSpellIDs (detection) and
PounceToBleedMapping (cast->trigger).
2026-07-28 12:10:54 -05:00
Brues b1fb2af861 remove fallbacks 2026-07-28 10:39:41 -05:00
Brues 7382e8a55f Unify dispel scan on GetAuraDataByIndex with a filter
scanDispel now calls C_UnitAuras.GetAuraDataByIndex(unit, i, filter) instead of
selecting GetBuffDataByIndex/GetDebuffDataByIndex -- the two are documented as
GetAuraDataByIndex with a HELPFUL/HARMFUL filter, and the index runs within the
filtered range identically. Drops the helpful-and-A-or-B function pick.
2026-07-28 09:55:05 -05:00
Brues 37434911e4 Resolve tracked aura texture/stacks via C_UnitAuras, not slot scans
FindPlayerDebuff and FindPlayerBuff hand-scanned debuff/buff slots for a
spellID to pull icon + stacks. Both now use ClassicAPI.GetUnitAuraBySpellID
(AuraData.icon / .applications) -- FindPlayerDebuff unfiltered (walks both
ranges, was debuff-then-buff), FindPlayerBuff HELPFUL-filtered (buff-only).
Search order is immaterial since a spellID is only ever a buff or a debuff.
2026-07-28 09:20:23 -05:00
Brues da8ea22998 Match buff-based immunity via C_UnitAuras.GetAuraDataBySpellName
Replaces the 32-slot UnitBuff + C_Spell.GetSpellName name-compare loop with a
single ClassicAPI.GetAuraDataBySpellName(unit, name, "HELPFUL") lookup.
Locale-identical to the old compare (both use the localized Spell.dbc name).
Wrapper added next to GetUnitAuraBySpellID.
2026-07-28 09:16:33 -05:00
Brues d82eeabf22 Detect Banish via C_UnitAuras.GetUnitAuraBySpellID, not slot scan
The Banish immunity check hand-scanned 16 debuff slots plus 32 buff slots
(NPC overflow) for spellID 710/18647. ClassicAPI.GetUnitAuraBySpellID walks
the whole aura array in one call per rank, so it finds the debuff wherever it
lands -- including an NPC's overflow buff slots -- dropping the two loops and
the UnitIsPlayer gate. Wrapper added alongside UnitHasDispelType.
2026-07-28 09:12:29 -05:00
Brues dd61767eb9 Add [locked]/[nolocked] school-interrupt conditional via C_LossOfControl
Detects a spell-school interrupt lockout (Counterspell/Kick/Pummel/Earth
Shock) on the player -- the server-side lockout that no debuff scan can see,
since it's a SMSG_SPELL_COOLDOWN packet, not an aura. ClassicAPI's
C_LossOfControl aggregates it as SCHOOL_INTERRUPT with a lockoutSchool mask.

ClassicAPI.GetSchoolLockout() returns the locked school mask; ValidateSchoolLocked
tests a school name against it (bitmask check without the 5.1-only % operator).
[locked] = any school kicked, [locked:frost] = that school, [locked:fire/frost]
= either; [nolocked:...] negates with AND (neither). Registered in
BOOLEAN_CONDITIONALS for the bare form; LOSS_OF_CONTROL_ADDED/UPDATE wired to
refresh the icon. Full silences remain [mycc:silence].
2026-07-28 08:57:37 -05:00
Brues 2e8ed33a6a focus/focustarget is provided by classicapi now 2026-07-28 00:02:45 -05:00
Brues 403567b381 Scan nameplateN unit tokens for /target candidates, not SuperWoW WorldFrame
The /target candidate search's last SuperWoW-gated block walked WorldFrame
children and scraped GUIDs via pcall(frame.GetName, frame, 1). ClassicAPI now
provides modern nameplateN unit tokens that work with UnitExists/UnitCanAttack/
TargetUnit directly, so it collapses to iterating nameplate1..40 -- no SuperWoW
dependency, no WorldFrame hackery, and it also picks up default vanilla
nameplates. Scans the full range since assigned slots can be sparse.
2026-07-28 00:00:51 -05:00
Brues 7ecb66a8dc Add left/right modifier variants to kmods via ClassicAPI
kmods gains lctrl/rctrl, lalt/ralt, lshift/rshift (ClassicAPI's side-specific
IsLeft*/IsRight* key checks), so [mod:lshift]/[mod:ralt] and castsequence
reset=lctrl work. mod/nomod now use ClassicAPI's IsModifierKeyDown instead of
OR-ing the three individual checks.
2026-07-27 22:46:18 -05:00
Brues 494aced5bd Resolve [mhimbue:Name] via enchant DBC, not tooltip scan
CheckWeaponImbueByName now resolves the applied temp-enchant ID to its
localized name via ClassicAPI (C_Item.GetEnchantInfo) and matches that
directly -- exact and locale-clean, replacing the green-text tooltip
heuristics for named enchants (poisons, oils, Mongoose, Windfury). Only
nameless enchants (sharpening/weightstones, no DBC name) fall through to
the tooltip scan, so no regression.
2026-07-27 22:44:13 -05:00
Brues 4f76255f73 Mark GetWeaponEnchantInfo TODO item done 2026-07-27 22:42:47 -05:00
Brues fd8050aee1 Add [mhenchant]/[ohenchant] weapon temp-enchant conditionals
Uses ClassicAPI's modern C_Item.GetWeaponEnchantInfo (12-tuple with
enchant IDs) plus C_Item.GetEnchantInfo (ID -> localized name) to match
WHICH temporary weapon enchant is applied -- by SpellItemEnchantment ID or
name -- not just that one exists. Locale-proof and exact, unlike
[mhimbue:Name]'s green-text tooltip scan.

ClassicAPI.lua gains GetWeaponEnchant(slot) (centralizes the tuple
indexing, vanilla-global fallback) and GetEnchantName(id). ValidateWeaponImbue
now reads through GetWeaponEnchant. New keywords mhenchant/nomhenchant/
ohenchant/noohenchant support bare (any enchant), single ID/name, and
OR-lists ([mhenchant:2823/Deadly_Poison]); registered in BOOLEAN_CONDITIONALS
for the bare form and auto-added to VALID_CONDITIONALS via Keywords.
2026-07-27 22:42:09 -05:00
Brues 0e07eafbf9 Add [equipset]/[noequipset] conditionals for equipment sets
Checks whether a saved ClassicAPI equipment set is currently equipped, via
C_EquipmentSet.GetEquipmentSetInfo's isEquipped flag (wrapped as
ClassicAPI.IsEquipmentSetEquipped). Named [equipset] rather than [set] to avoid
colliding with the existing tier/item-set piece-count conditional. Supports
OR-lists ([equipset:Raid/Farm]) and negation ([noequipset:PvP]). Pairs with the
conditional /equipset command: swap with one, branch on the other.
2026-07-27 22:32:01 -05:00
Brues 6aaa8a409b Add conditional support to /equipset via ClassicAPI equipment sets
ClassicAPI registers a bare /equipset that resolves a name and calls
C_EquipmentSet.UseEquipmentSet with no conditional layer. DoEquipSet routes
it through DoWithConditionals like the other /equip* commands, so it gains
[combat]/[mod]/@unit/etc. and ';'-separated fallthrough (first set whose
conditionals pass wins). Console.lua reclaims the EQUIP_SET handler (keeping
ClassicAPI's localized aliases) and falls back to registering /equipset itself
if the API command isn't present.
2026-07-27 22:28:09 -05:00
Brues 2cf4886b01 Add [mana]/[rage]/[energy] type-specific power conditionals
Read a SPECIFIC power slot (Enum.PowerType) as a percentage via ClassicAPI
UnitPower/UnitPowerMax, rather than the unit's primary power. Adds mana/
mymana, rage/myrage, energy/myenergy (no-prefix = target, @unit-overridable;
my = player), each supporting operators and multi-comparison like [power]/
[mypower]. Works cross-form (druid mana in Cat) and cross-unit -- the
standout being [@target,mana:<15] to catch a caster near OOM. A unit with no
such pool (max <= 0) fails rather than reading 0%. Registered in the macro
error checker (needsArgs + operator hint); VALID_CONDITIONALS auto-populates
from Keywords.
2026-07-27 20:10:20 -05:00
Brues 77f985a927 Read HP deficit via ClassicAPI UnitHealthMissing
ValidateHpLost now gets the health deficit in one call through
CleveRoids.ClassicAPI.UnitHealthMissing, mirroring how ValidatePowerLost
uses UnitPowerMissing, instead of hand-computing max - current via
NampowerAPI.
2026-07-27 19:48:36 -05:00
Brues 7c71f8fc26 Use ClassicAPI UnitPower* for power reads; drop GetUnitField/SuperWoW paths
Add ClassicAPI UnitPower/UnitPowerMax/UnitPowerMissing/UnitPowerType wrappers
and route the power conditionals through them:
- ValidatePower/ValidateRawPower/GetCachedPlayerPower(Percent) use UnitPower /
  UnitPowerMax (omitted type = primary power, matching the old UnitMana path).
- ValidatePowerLost uses UnitPowerMissing (one call vs max - current).
- powertype/nopowertype use ClassicAPI.UnitPowerType.

Replace the SuperWoW "2nd return of UnitMana = caster mana" druid hack in
ValidateDruidRawMana and the #showtooltip OOM check with UnitPower(unit, 0),
which reads the mana slot directly and survives shapeshift. Verified in Cat
Form: UnitPower('player', 0) returns caster mana while UnitPower('player', 3)
returns energy.

Remove the now-dead Nampower GetUnitField-based GetUnitPower/GetUnitMaxPower
wrappers and POWER_FIELDS tables (no caller passed a powerType, so that path
never ran).
2026-07-27 19:27:25 -05:00
Brues efc98bcd6a Use ClassicAPI GetSpellBonusDamage/Healing for spell-power conditionals
Migrate the healing/*_power/spell_power conditionals off Nampower's
GetSpellPower to ClassicAPI GetSpellBonusDamage(school) / GetSpellBonusHealing.
Both read the same client field, so the per-school damage values are identical
while dropping the Nampower v2.31 gate.

Fixes healing/healingpower: GetSpellPower returns no healing value, so they were
returning the 2nd school (holy spell damage) as a stand-in for +healing.
GetSpellBonusHealing returns the real (derived) +healing.

Prune the now-unused Nampower GetSpellPower wrapper, feature-table entry, and
feature flag.
2026-07-27 18:57:58 -05:00
Brues 41d9283117 Use IsInGroup/IsInRaid for group checks
Replace numeric GetNumPartyMembers/GetNumRaidMembers checks with IsInGroup() and IsInRaid() in Conditionals.lua for the group and nogroup conditionals.
2026-07-27 18:45:02 -05:00
Brues 9bebbbb836 Fix addon initialization order with guard checks
Refactor mouseover extensions to split initialization into two phases: OnLoad (empty) and OnAddOnLoad (actual setup). This ensures hooks are only registered when the target addon's globals are defined. Add guard clauses to check for required functions before hooking. Also remove Cursive's premature initialization attempt.
2026-07-27 14:32:12 -05:00
Brues 586c51b53a Detect bleeds authoritatively via Spell.dbc mechanics
Add a GetSpellSchool bleed check (Priority 0.5) backed by DBC SpellMechanic
data instead of damage-event learning / name patterns:
- GetSpellMechanicByID == 15 catches spell-level bleeds (Garrote, Rupture,
  Rend, Rip, Pounce, Deep Wounds).
- New ClassicAPI GetSpellEffectMechanics catches effect-level bleeds that the
  spell-level field misses -- Rake is spell-level 0 with EffectMechanic[2]=15.

Wrap C_Spell.GetSpellEffectMechanics in ClassicAPI.lua (nil-guarded so older
builds fall back to spell-level only). Verified against the client Spell.dbc.

Also drop Hemorrhage from the bleed name-pattern fallback: the DBC gives it no
bleed mechanic (physical damage), so bleed-immune mobs don't resist it -- it was
a false positive. Existing learning / split-damage / patterns remain as fallback.
2026-07-26 15:40:26 -05:00
Brues 2ce4d11698 Use ClassicAPI EventUtil for addon-load/login extension wiring
Replace hand-rolled ADDON_LOADED/PLAYER_LOGIN handlers with
EventUtil.ContinueOnAddOnLoaded / ContinueOnPlayerLogin, which fire
immediately if the event already happened -- removing the "we loaded before
the target addon and missed its ADDON_LOADED" workarounds.

- pfUI compat: ContinueOnAddOnLoaded("pfUI") + ContinueOnPlayerLogin; drops
  the missed-event fallback (login path still re-runs SetupCompatibility).
- MacroErrorUI / MacroLengthWarn: ContinueOnAddOnLoaded("Blizzard_MacroUI"),
  folding their manual "already loaded" checks.
- 9 Mouseover extensions (ag_UnitFrames, CT_RaidAssist, CT_UnitFrames,
  DiscordUnitFrames, Grid, NotGrid, Cursive, sRaidFrames, PerfectRaid):
  ContinueOnAddOnLoaded("<AddonName>", OnLoad). Since immediate-fire passes no
  event args, the old `arg1 == "X"` checks are replaced by the addon-name gate
  (global guards kept where present); also removes the buggy
  UnregisterEvent("ADDON_LOADED", "Onload") no-ops that never fired.

Addon names match file names; drops support for renamed folders (e.g. -master).
2026-07-26 14:53:34 -05:00
Brues e594fda295 Use C_Spell.GetSpellTexture in GetCachedIcon 2026-07-26 14:20:14 -05:00
Brues 22633aa16c Replace GetSpellRecField name/rank reads with C_Spell equivalents
Swap 166 call sites from GetSpellRecField(id, "name") to
C_Spell.GetSpellName(id) and GetSpellRecField(id, "rank") to
C_Spell.GetSpellSubtext(id) across Core, Conditionals, Utility,
ComboPointTracker, CursiveCustomSpells, pfUI, OverflowBuffFrame, and
Generic. Guard forms and the _GetSpellRecField alias calls collapse to the
direct C_Spell call.

GetSpellRecField stays for fields with no C_Spell equivalent (school,
spellIconID, mechanic, effectMechanic, effectApplyAuraName, stackAmount,
rangeIndex) and inside the NampowerAPI wrapper layer.
2026-07-26 14:07:31 -05:00
Brues 6718a01d79 Add [mounted]/[nomounted] and [standing]/[sitting] conditionals
Back them with ClassicAPI IsMounted() and UnitStandState("player").
mounted/nomounted are player mount state; standing = stand state 0,
sitting = any non-standing pose (its complement). Player-only, registered
in Keywords, BOOLEAN_CONDITIONALS, and STATIC_CONDITIONALS like [stealth].
2026-07-26 13:44:16 -05:00
Brues 1770f7b6e1 Make SendChatMessage reclaim cycle-safe and cover LVL
The previous reclaim stack-overflowed when LeafVillageAchievements loaded
after us (its captured "original" was our own function, so reclaiming onto
the top created an ours<->LVA loop). Add a reentrancy guard plus a base
SendChatMessage reference so a hook cycle routes straight to the base
function instead of recursing.

Gate the reclaim on LeafVillageAchievements OR LeafVillageLegends (both hook
SendChatMessage and can bump ours off the top -- LVA orphans it, LVL wraps
and re-installs on timers). Keep the anchored ^#showtooltip match since being
on top means we see the pristine line.
2026-07-24 14:09:36 -05:00
Brues d060b04908 Reclaim SendChatMessage hook so #showtooltip stays filtered
Our #showtooltip filter hooks the global SendChatMessage. An addon that
snapshots SendChatMessage at its own file-load and later calls that
snapshot directly (e.g. LeafVillageAchievements at PLAYER_ENTERING_WORLD+3s)
orphans our hook if it loaded before us -- its snapshot predates our filter,
so #showtooltip leaks to chat. This bites the fork specifically: it sorts
after LeafVillageAchievements alphabetically, so it loads too late to be in
the snapshot, whereas upstream "CleveRoidMacros" sorted before it.

Make the filter a named function and add EnsureSendChatMessageHook, which
re-asserts it as the outermost SendChatMessage hook (no-op once on top).
OnUpdate calls it each throttled tick via a cheap identity check, so we
reclaim the top of the chain within a frame of being displaced. Normal
messages still flow through the chained-over hook untouched.
2026-07-24 09:41:26 -05:00
Brues 6ac69501f5 pass base object rather than ItemLocation 2026-07-20 10:35:09 -05:00
Brues 782c685fba Replace tooltip-scanning GetSpellCost with ClassicAPI DBC reads
GetSpellCost now reads power cost and reagents straight from Spell.dbc via
C_Spell.GetSpellPowerCost (effective, talent-modified cost) and
C_Spell.GetSpellReagents (itemID), dropping the GameTooltip owner/scan
frames, their font strings, and all locale-dependent line parsing.

Reagent counting is now itemID-based end to end: GetReagentCount takes an
itemID and matches by id in the Items cache / bag scan. This removes the
hardcoded English reagent tables (_ReagentBySpell, _ReagentIdByName) and
the name-matching bag-scan tooltip, so it works on any client locale.
Verified in-game that GetSpellReagents covers DBC reagent spells (Vanish
-> Flash Powder), which the hand table previously special-cased.

The localized reagent name is still used for countedItemTypes registration
(recognizing a reagent item placed on the action bar); when that name
isn't cached yet, warm it via the ClassicAPI Item mixin
(Item:CreateFromItemID/ContinueOnItemLoad) and register it once it lands.
2026-07-19 16:34:19 -05:00
Brues 97e898f14c Add /cancelform command and CancelShapeshiftForm
Adds a new /cancelform slash command (Console.lua) wired to CleveRoids.DoCancelForm. Replaces the previous _unshiftAction with _cancelFormAction that calls CancelShapeshiftForm, and updates DoUnshift to delegate to DoCancelForm for backwards compatibility. Also fixes a unit name check by replacing GetUnitName with UnitName in DoRetarget.
2026-07-19 14:32:13 -05:00
Brues b84835a567 Use GetInventoryItemID instead of string parsing 2026-07-17 21:25:25 -05:00
Brues 40cf2c8010 More ClassicAPI porting 2026-07-15 19:45:31 -05:00
Brues a5a0b9c00a Adopt ClassicAPI C_Item.GetItemName in Utility.lua equip-modifier debug
Replace the debug-only GetInventoryItemLink name scrape with the
decorated C_Item.GetItemName location form.
2026-07-15 19:36:18 -05:00
Brues 6e57ff01dc Adopt ClassicAPI C_Item item reads in Tooltip/Generic.lua indexers
Migrate the link-scrape-then-discard sites to direct ClassicAPI reads:

- IndexEquippedItems / IndexEquipSlot: GetInventoryItemID for the id
  (GetItemInfo still supplies the stored base link/texture)
- IndexItems bag+equip scans: GetContainerItemID/GetInventoryItemID +
  C_Item.GetItemName (decorated) for the dedup fast-path; drop the now
  dead GetContainerItemLink/GetInventoryItemLink locals
- GetItemFast cache validation: alloc-free id compare via
  GetInventoryItemID/GetContainerItemID, decorated-name compare otherwise
- IsItemEquipped: GetInventoryItemID + C_Item.GetItemName

Left untouched: makeInventoryItem/makeBagItem and the GetItem/GetItemFast
scan loops, which build a real link that is stored on the item and later
fed to GameTooltip:SetHyperlink.

Note: the equipped-scan loops now start at slot 1 (GetInventoryItemID
requires slot>=1), so the ammo slot (0) is no longer walked by these
indexers -- ammo is not referenced by name anywhere in the addon.
2026-07-15 19:33:07 -05:00
Brues 9cd79d682d Adopt ClassicAPI C_Item item reads in Conditionals.lua
Replace GetInventoryItemLink/GetContainerItemLink link-scraping with
direct ClassicAPI reads across the item conditionals:

- [equipped] cache build: GetInventoryItemID + C_Item.GetItemName
  (decorated name replaces the old bracket-name / GetItemInfo two-step)
- slot-number -> item name resolves ([cd], [usable]/[nousable]):
  C_Item.GetItemName location form
- HasItem / GetItemCooldown substring fallbacks: match the decorated
  name instead of the raw link string
- numeric-slot presence check: GetInventoryItemID

Suffix decoration is preserved via the location form, so partial/
suffixed [equipped:...] matches behave as before.
2026-07-15 19:17:00 -05:00
Brues f54eda5ae3 Adopt ClassicAPI C_Item/C_Container item reads in Core.lua
Replace GetInventoryItemLink/GetContainerItemLink link-scraping (built
solely to regex out item:ID or the bracketed name) with direct ClassicAPI
reads:

- id/presence: GetInventoryItemID, C_Container.GetContainerItemID
- suffix-sensitive names (equip-by-name on gear): C_Item.GetItemName
  location form, which now carries random-suffix decoration
- suffix-free names (consumables/reagents/poisons) in hot bag scans:
  id + C_Item.GetItemNameByID (base name, zero table allocation)

Also covers /use equipped-slot resolution and the WDB warm scans. Drops
the now-dead GetContainerItemLink/GetInventoryItemLink/string_find
upvalue aliases. No behavior change; equip-by-name keeps full suffixed
matching via the decorated location form.
2026-07-15 19:12:56 -05:00
Brues 653f8db3d7 Reconcile /startattack against real action-bar state, not cached flag
The autoAttack flag could drift stale-true (optimistic set after
AttackTarget, or a target dying without PLAYER_LEAVE_COMBAT), making
/startattack skip the attack until the target was dropped and reselected.
The old fallback called the overridden IsCurrentAction, which just echoes
the cached flag for the attack slot, so it never detected drift. Use the
original Hooks.IsCurrentAction as ground truth and sync the flag to it.
2026-07-04 19:38:37 -05:00
31 changed files with 2444 additions and 3374 deletions
+1 -1
View File
@@ -18,6 +18,6 @@ jobs:
fetch-depth: 0
- name: Package and release to GitHub
uses: BigWigsMods/packager@v2
uses: brues-code/packager@vCAPI
env:
GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
{
"runtime": {
"version": "Lua 5.1"
},
"diagnostics": {
"disable": ["deprecated"],
"globals": [
"this",
"event",
"arg",
"arg1",
"arg2",
"arg3",
"arg4",
"arg5",
"arg6",
"arg7",
"arg8",
"arg9"
]
}
}
+4
View File
@@ -0,0 +1,4 @@
package-as: SuperCleveRoidMacros
ignore:
- .luarc.json
+199 -14
View File
@@ -3,9 +3,11 @@
ClassicAPI is a client mod (sibling to Nampower/SuperWoW) that backports the
modern C_* API into the 1.12.1 Lua environment. It is a HARD REQUIREMENT of
this addon, so the wrappers below call the API directly — no fallbacks. The
load-time requirement check (Core.lua) uses IsAvailable() to warn when the
DLL is missing; users who don't want ClassicAPI should run the upstream addon.
this addon (ClassicAPI v1.12.1+, which added the positional
C_UnitAuras.UnitAura), so the wrappers below call the API directly — no
fallbacks. The load-time requirement check (Core.lua) uses IsAvailable() to
warn when the DLL is missing and HasMinimumVersion() when it's too old; users
who don't want ClassicAPI should run the upstream addon.
Detection: the global CLASSIC_API_VERSION is defined once the client has
booted, encoded as X*10000 + Y*100 + Z for a vX.Y.Z tag (untagged dev builds
@@ -52,16 +54,17 @@ end
-- C_UnitAuras
--------------------------------------------------------------------------------
-- Scan a unit's auras (indexFn = C_UnitAuras.GetBuffDataByIndex or
-- GetDebuffDataByIndex) for one matching the dispel type. Vanilla descriptors
-- hold 32 helpful / 16 harmful slots; cap as a backstop.
local function scanDispel(indexFn, unit, dispelType, wantAny)
-- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
-- matching the dispel type. Uses the positional C_UnitAuras.UnitAura (added in
-- ClassicAPI v1.12.1, this addon's minimum) -- no table allocated per slot, with
-- dispelName as the 4th return. The filtered index self-terminates at the end of
-- the range (nil name); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
local function scanDispel(unit, filter, dispelType, wantAny)
local i = 1
while i <= 48 do
local data = indexFn(unit, i)
if not data then return false end
local dn = data.dispelName
if dn and dn ~= "" and (wantAny or dn == dispelType) then
local name, _, _, dispelName = C_UnitAuras.UnitAura(unit, i, filter)
if not name then return false end
if dispelName and dispelName ~= "" and (wantAny or dispelName == dispelType) then
return true
end
i = i + 1
@@ -76,10 +79,29 @@ end
-- false -> scan debuffs (defensive cleanse, default)
function API.UnitHasDispelType(unit, dispelType, helpful)
if not unit or not UnitExists(unit) then return false end
local indexFn = helpful and C_UnitAuras.GetBuffDataByIndex
or C_UnitAuras.GetDebuffDataByIndex
local filter = helpful and "HELPFUL" or "HARMFUL"
local wantAny = (dispelType == nil or dispelType == "any")
return scanDispel(indexFn, unit, dispelType, wantAny)
return scanDispel(unit, filter, dispelType, wantAny)
end
-- First matching aura on `unit` by spellID, or nil. With no filter it walks the
-- whole aura array (helpful then harmful), so it finds a debuff even when it has
-- overflowed into an NPC's buff slots -- no 16+32 slot scan, no UnitIsPlayer
-- gate. filter ("HELPFUL"/"HARMFUL") restricts the search. Returns the modern
-- AuraData (spellId, name, applications, duration, expirationTime, dispelName, ...).
function API.GetUnitAuraBySpellID(unit, spellID, filter)
if not unit or not spellID then return nil end
return C_UnitAuras.GetUnitAuraBySpellID(unit, spellID, filter)
end
-- First matching aura on `unit` by spell NAME, or nil. Same whole-array search as
-- GetUnitAuraBySpellID; the name is case-sensitive and locale-resolved, so pass it
-- in the client's locale (what C_Spell.GetSpellName returns). filter
-- ("HELPFUL"/"HARMFUL") restricts the search. Prefer the by-ID variant for
-- portability where a spellID is known.
function API.GetAuraDataBySpellName(unit, spellName, filter)
if not unit or not spellName or spellName == "" then return nil end
return C_UnitAuras.GetAuraDataBySpellName(unit, spellName, filter)
end
--------------------------------------------------------------------------------
@@ -155,6 +177,76 @@ function API.GetItemSetInfo(setID)
return C_Item.GetItemSetInfo(setID)
end
--------------------------------------------------------------------------------
-- Equipment Set
--------------------------------------------------------------------------------
-- True if the saved equipment set named `name` is currently equipped -- every
-- resolvable item in its target slot (a missing bank-stored piece doesn't
-- disqualify, matching C_EquipmentSet.GetEquipmentSetInfo's isEquipped). Name is
-- an exact, case-sensitive match per GetEquipmentSetID. Returns false for an
-- unknown name or a client without the EquipmentSet API. Powers [equipset]/
-- [noequipset]; the swap side is the reclaimed /equipset command.
function API.IsEquipmentSetEquipped(name)
if not name or name == "" then return false end
local setID = C_EquipmentSet.GetEquipmentSetID(name)
if not setID then return false end
local _, _, _, isEquipped = C_EquipmentSet.GetEquipmentSetInfo(setID)
return isEquipped and true or false
end
--------------------------------------------------------------------------------
-- Weapon Enchant
--------------------------------------------------------------------------------
-- Temporary weapon-enchant state for a slot: "mh" (main), "oh" (off), or
-- "ranged". Returns hasEnchant, expirationMs, charges, enchantID. The enchantID
-- comes from ClassicAPI's modern C_Item.GetWeaponEnchantInfo 12-tuple (the
-- vanilla global omits it), so [mhenchant] can tell WHICH imbue is applied, not
-- just that one exists. Falls back to the vanilla 6-tuple global (enchantID nil,
-- no ranged slot) when the C_Item version is unavailable.
function API.GetWeaponEnchant(slot)
local hasM, mExp, mChg, mID, hasO, oExp, oChg, oID, hasR, rExp, rChg, rID =
C_Item.GetWeaponEnchantInfo()
if slot == "oh" then
return hasO, oExp, oChg, oID
elseif slot == "ranged" then
return hasR, rExp, rChg, rID
end
return hasM, mExp, mChg, mID
end
-- Localized name of an item-enchant ID (poison/oil/sharpening stone/permanent),
-- read straight from SpellItemEnchantment.dbc via ClassicAPI -- or nil for an
-- unknown id / a client without C_Item.GetEnchantInfo. Lets [mhenchant:Name]
-- resolve the applied enchant's name without scraping the weapon tooltip.
function API.GetEnchantName(enchantID)
if not enchantID or enchantID == 0 then return nil end
local info = C_Item.GetEnchantInfo(enchantID)
return info and info.name or nil
end
--------------------------------------------------------------------------------
-- Loss Of Control
--------------------------------------------------------------------------------
-- Locked spell-school mask from an active SCHOOL_INTERRUPT (Counterspell / Kick /
-- Pummel / Earth Shock lockout) on the player, or 0 when not kicked. Read from
-- C_LossOfControl, which synthesizes the lockout from the server's own
-- SMSG_SPELL_COOLDOWN packet -- a state no debuff scan can see. Also returns the
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast). Returns
-- 0 for a client without C_LossOfControl. Player-only (vanilla LoC is local-only).
function API.GetSchoolLockout()
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, n do
local d = C_LossOfControl.GetActiveLossOfControlData(i)
if d and d.locType == "SCHOOL_INTERRUPT" then
return d.lockoutSchool or 0, d.timeRemaining
end
end
return 0, nil
end
--------------------------------------------------------------------------------
-- Spell
--------------------------------------------------------------------------------
@@ -168,6 +260,76 @@ function API.GetSpellMechanicByID(spellID)
return C_Spell.GetSpellMechanicByID(spellID)
end
-- Per-effect SpellMechanic ids (Spell.dbc EffectMechanic[3]) as {m1, m2, m3},
-- or nil for an invalid spell / 0 for an effect with no mechanic. Complements
-- GetSpellMechanicByID, which only reads the spell-level Mechanic field: vanilla
-- stores some mechanics on an effect instead (e.g. Rake's bleed is effect-level,
-- so GetSpellMechanicByID returns 0 but this returns {0,15,0}). Nil-guarded so an
-- older ClassicAPI build without the function degrades gracefully.
function API.GetSpellEffectMechanics(spellID)
return C_Spell.GetSpellEffectMechanics(spellID)
end
-- Flat spell-damage bonus (spell power) for a magic school, as a number.
-- school is 1-based: 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow,
-- 7=Arcane. Reads the same client field nampower's GetSpellPower does -- exact,
-- with gear/enchants/buffs/talents/set bonuses baked in.
function API.GetSpellBonusDamage(school)
return GetSpellBonusDamage(school)
end
-- Flat healing bonus (+healing), as a number. Vanilla has no healing-done field,
-- so ClassicAPI derives it from gear/enchant/buff MOD_HEALING_DONE plus
-- stat-conversion talents (e.g. Spiritual Guidance) -- exact, not a holy-damage
-- proxy.
function API.GetSpellBonusHealing()
return GetSpellBonusHealing()
end
-- Spell haste for `unit` as a percentage: 0 unhasted, positive when casts are
-- sped up, negative while slowed (Curse of Tongues). 0 for a unit that doesn't
-- resolve. Vanilla has no haste API at all -- ClassicAPI derives this from the
-- UNIT_MOD_CAST_SPEED descriptor field the server folds into the cast time, so
-- it's exact and needs no nampower GetUnitField dependency.
function API.UnitSpellHaste(unit)
return UnitSpellHaste(unit)
end
--------------------------------------------------------------------------------
-- Unit Health
--------------------------------------------------------------------------------
-- Health deficit (max - current) for `unit` in one call. Falls back to
-- UnitHealthMax - UnitHealth without ClassicAPI.
API.UnitHealthMissing = UnitHealthMissing or function(unit)
return (UnitHealthMax(unit) or 0) - (UnitHealth(unit) or 0)
end
--------------------------------------------------------------------------------
-- Unit Power
--------------------------------------------------------------------------------
-- Current power for a specific Enum.PowerType (0=Mana, 1=Rage, 2=Focus,
-- 3=Energy, 4=Happiness), or the unit's primary power when powerType is omitted.
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
API.UnitPower = UnitPower or function(unit, powerType)
return UnitMana(unit)
end
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
return UnitManaMax(unit)
end
-- Power deficit (max - current) for the type / primary power, in one call.
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
end
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
function API.UnitPowerType(unit)
return UnitPowerType(unit)
end
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
@@ -182,6 +344,29 @@ function API.IsSwimming()
return IsSwimming() and true or false
end
-- True if the player is currently mounted.
function API.IsMounted()
return IsMounted() and true or false
end
-- True if the player is under a WMO roof (building, cave, instance interior).
-- Live engine geometry query, not zone-based; nil (-> false) pre-world.
function API.IsIndoors()
return IsIndoors() and true or false
end
-- True if the player is outdoors (open sky / not inside a WMO interior).
-- Exact complement of IsIndoors for a resolvable player.
function API.IsOutdoors()
return IsOutdoors() and true or false
end
-- Player's stand state: 0 = standing, non-zero = sitting/sleeping/kneeling/etc.
-- (see UnitStandState). Player-only.
function API.GetPlayerStandState()
return UnitStandState("player") or 0
end
--------------------------------------------------------------------------------
-- Cursor
--------------------------------------------------------------------------------
+40 -67
View File
@@ -32,6 +32,35 @@ CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
timestamp = 0
}
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
-- is just a name match against one seed rank. seedNameCache memoizes the seed's
-- localized name (locale-safe: derived from the ID, not a hardcoded string; a
-- seed absent from the DBC, e.g. a TWoW spell on a stock client, resolves to nil
-- and the set simply never matches -- which is correct, that spell can't be cast).
local seedNameCache = {}
local function SeedName(seedID)
local n = seedNameCache[seedID]
if n == nil then
n = C_Spell.GetSpellName(seedID) or false
seedNameCache[seedID] = n
end
return n or nil
end
-- Stand-in for a hardcoded {spellID=true,...} rank set: t[spellID] is true iff
-- spellID is any rank of the family seeded by seedID. Same read shape as the old
-- tables, so every membership consumer (`X and X[id]`) keeps working unchanged.
-- Only valid for membership tests -- these are not iterable (pairs() sees empty).
local function RankSet(seedID)
return setmetatable({}, { __index = function(_, spellID)
if type(spellID) ~= "number" then return nil end
local name = SeedName(seedID)
return (name and C_Spell.GetSpellName(spellID) == name) and true or nil
end })
end
-- Define spells that scale with combo points by SPELL ID and their duration formulas
-- Duration = base + (combo_points - 1) * increment
CleveRoids.ComboScalingSpellsByID = {
@@ -70,32 +99,9 @@ CleveRoids.FerociousBiteSpellIDs = {
[31018] = true, -- Rank 6
}
-- Rip spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RipSpellIDs = {
[1079] = true, -- Rank 1
[9492] = true, -- Rank 2
[9493] = true, -- Rank 3
[9752] = true, -- Rank 4
[9894] = true, -- Rank 5
[9896] = true, -- Rank 6
}
-- Rake spell IDs (for Carnage talent - refreshed when Carnage procs)
CleveRoids.RakeSpellIDs = {
[1822] = true, -- Rank 1
[1823] = true, -- Rank 2
[1824] = true, -- Rank 3
[9904] = true, -- Rank 4
}
-- Pounce Bleed spell IDs (for immunity detection - bleed portion of Pounce)
-- Note: Pounce (cast) TRIGGERS a separate Pounce Bleed spell with different IDs
-- Cast IDs: 9005, 9823, 9827 → Trigger Bleed IDs: 9007, 9824, 9826
CleveRoids.PounceBleedSpellIDs = {
[9007] = true, -- Rank 1 (triggered by Pounce 9005)
[9824] = true, -- Rank 2 (triggered by Pounce 9823)
[9826] = true, -- Rank 3 (triggered by Pounce 9827)
}
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
CleveRoids.RipSpellIDs = RankSet(1079)
CleveRoids.RakeSpellIDs = RankSet(1822)
-- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
@@ -132,14 +138,9 @@ CleveRoids.PounceToBleedMapping = {
-- When Molten Blast hits, it refreshes Flame Shock duration on the target
-- Detection: Monitor combat log for Molten Blast damage, then refresh Flame Shock
-- =============================================================================
CleveRoids.MoltenBlastSpellIDs = {
[36916] = true, -- Rank 1
[36917] = true, -- Rank 2
[36918] = true, -- Rank 3
[36919] = true, -- Rank 4
[36920] = true, -- Rank 5
[36921] = true, -- Rank 6
}
-- TWoW custom; seed resolves on the TWoW client (nil/never-matches on stock,
-- where Molten Blast can't be cast anyway).
CleveRoids.MoltenBlastSpellIDs = RankSet(36916)
CleveRoids.FlameShockSpellIDs = {
[8050] = true, -- Rank 1
@@ -154,12 +155,7 @@ CleveRoids.FlameShockSpellIDs = {
-- WARLOCK: Conflagrate → Immolate Duration Reduction
-- When Conflagrate is cast, it reduces Immolate duration by 3 seconds
-- =============================================================================
CleveRoids.ConflagrateSpellIDs = {
[17962] = true, -- Rank 1
[18930] = true, -- Rank 2
[18931] = true, -- Rank 3
[18932] = true, -- Rank 4
}
CleveRoids.ConflagrateSpellIDs = RankSet(17962)
CleveRoids.ImmolateSpellIDs = {
[348] = true, -- Rank 1
@@ -177,11 +173,8 @@ CleveRoids.ImmolateSpellIDs = {
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
-- =============================================================================
CleveRoids.DarkHarvestSpellIDs = {
[52550] = true, -- Rank 1
[52551] = true, -- Rank 2
[52552] = true, -- Rank 3
}
-- TWoW custom (see MoltenBlast note).
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
-- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist
@@ -489,7 +482,7 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
end
else
-- Second, check if name-based tracking has recent data for this spell
local spellName = GetSpellRecField(spellID, "name")
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
-- Remove rank info for comparison
local baseName = CleveRoids.StripRank(spellName)
@@ -548,26 +541,6 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
return duration
end
-- API function to get last tracked combo points for a spell
function CleveRoids.GetLastComboPointsForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].combo_points
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_combo_points
end
return nil
end
-- API function to get last calculated duration for a spell
function CleveRoids.GetLastDurationForSpell(spellName)
if CleveRoids.ComboPointTracking[spellName] then
return CleveRoids.ComboPointTracking[spellName].duration
elseif CleveRoids.spell_tracking[spellName] then
return CleveRoids.spell_tracking[spellName].last_duration
end
return nil
end
-- Utility function to display current combo tracking info
function CleveRoids.ShowComboTracking()
CleveRoids.Print("=== Combo Point Tracking ===")
@@ -683,7 +656,7 @@ if _G.UseAction then
local spellName = nil
if actionType == "SPELL" and actionID then
spellName = GetSpellRecField(actionID, "name")
spellName = C_Spell.GetSpellName(actionID)
end
if currentCP and currentCP > 0 then
+47 -388
View File
@@ -2,9 +2,10 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("Compatibility_pfUI")
Extension.RegisterEvent("ADDON_LOADED", "ADDON_LOADED")
Extension.RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN")
Extension.Debug = false
-- pfUI-loaded and player-login handlers are wired via ClassicAPI's EventUtil at
-- the bottom of the file (ContinueOnAddOnLoaded fires immediately if pfUI already
-- loaded, so no separate "we missed pfUI's ADDON_LOADED" fallback is needed).
-- Track pfUI state
Extension.pfUILoaded = false
@@ -21,19 +22,6 @@ function Extension.DLOG(msg)
end
end
function Extension.FocusNameHook()
local hook = Extension.internal.memberHooks[CleveRoids]["GetFocusName"]
local target = hook.original()
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.unitname then
target = pfUI.uf.focus.unitname
end
--Extension.DLOG(target)
return target
end
-- Check if pfUI's macrotweak module is loaded
function Extension.IsPfUIMacrotweakLoaded()
if not pfUI then return false end
@@ -91,334 +79,6 @@ function Extension.HandleSendChatMessageHook()
end
end
-- Helper function to check for Carnage duration override
-- Returns override duration and timeleft if found, nil otherwise
local function GetCarnageOverride(effect)
if not effect or not CleveRoids.carnageDurationOverrides then
return nil, nil
end
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
local spellName = GetSpellRecField(spellID, "name")
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then
local timeleft = override.duration - (GetTime() - override.timestamp)
if timeleft < 0 then timeleft = 0 end
return override.duration, timeleft
end
end
end
return nil, nil
end
-- Hook pfUI's libdebuff to use our combo-aware durations
-- NOTE: pfUI 7.6+ (GetUnitField edition) handles combo durations and Carnage internally.
-- We only inject when there's a mismatch between pfUI's data and ours.
function Extension.HookPfUILibdebuff()
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
return false
end
local pflib = pfUI.api.libdebuff
-- Check if pfUI 7.6+ with GetUnitField-based libdebuff is active
-- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch
local hasPfUI76 = CleveRoids.hasPfUI76
-- pfUI 7.6+ handles all durations internally - no hooks needed
if hasPfUI76 then
Extension.DLOG("Skipped all libdebuff hooks (pfUI 7.6+ handles internally)")
return false
end
-- Hook GetDuration if it exists
-- pfUI's GetDuration signature: function(effect, rank) where effect is spell NAME
if pflib.GetDuration and not Extension.pfLibDebuffHooked then
local originalGetDuration = pflib.GetDuration
pflib.GetDuration = function(self, effect, rank)
local pfuiDuration = originalGetDuration(self, effect, rank)
-- Check for Carnage duration overrides (only if pfUI doesn't have it)
local carnageDuration = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if pfUI's duration is significantly different (>1s difference)
if not pfuiDuration or math.abs(carnageDuration - pfuiDuration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Duration Override]|r %s: Carnage %.1fs (pfUI: %.1fs)",
effect, carnageDuration, pfuiDuration or 0)
)
end
return carnageDuration
end
end
-- Check name-based tracking for fresh combo casts (only if pfUI returned 0 or nil)
if (not pfuiDuration or pfuiDuration == 0) and CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
local tracking = CleveRoids.ComboPointTracking[effect]
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Duration Override]|r %s: Combo tracking %.1fs (pfUI: %.1fs)",
effect, tracking.duration, pfuiDuration or 0)
)
end
return tracking.duration
end
end
return pfuiDuration
end
Extension.pfLibDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration (mismatch-only mode)")
end
-- Hook AddEffect if it exists (pre-7.6 only - 7.6+ returns early above)
if pflib.AddEffect and not Extension.pfLibAddEffectHooked then
local originalAddEffect = pflib.AddEffect
pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster)
-- RANK CHECKING: Preserve higher rank's remaining time if lower rank was cast
-- NOTE: 'unit' is a unit NAME (e.g., "Expert Training Dummy"), not a unit ID
-- Defensive: verify libdebuff is a table, not a function
if caster == "player" and type(CleveRoids.libdebuff) == "table" and duration and duration > 0 then
-- Try to find the GUID for this unit name
local unitGUID = nil
-- Check if this is the current target
if UnitName("target") == unit then
unitGUID = CleveRoids.GetGUID("target")
end
-- If we couldn't match to current target, check guidToName mapping
if not unitGUID and CleveRoids.libdebuff.guidToName then
for guid, name in pairs(CleveRoids.libdebuff.guidToName) do
if name == unit then
unitGUID = guid
break
end
end
end
-- Check if a higher rank of this spell is already active
if unitGUID and CleveRoids.libdebuff.objects and CleveRoids.libdebuff.objects[unitGUID] then
-- Find all spell IDs that match this effect name
for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
if rec and rec.start and rec.duration then
-- Get spell name for this ID
local spellName = GetSpellRecField(spellID, "name")
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect then
-- Same spell - check if still active
local remaining = rec.duration + rec.start - GetTime()
if remaining > 0 then
-- If incoming duration > remaining time, we're trying to add more time
-- This means either a refresh or lower rank cast - preserve existing timer
if duration > remaining then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Rank Preserve]|r %s: Preserving timer (%.1fs remaining vs %.1fs incoming)",
effect, remaining, duration)
)
end
-- Preserve the existing timer
duration = remaining
end
break
end
end
end
end
end
end
end
-- Check for Carnage duration overrides FIRST (highest priority)
local carnageDuration = GetCarnageOverride(effect)
if carnageDuration then
duration = duration or carnageDuration
caster = caster or "player" -- Ensure caster is set for UnitOwnDebuff filtering
end
-- Check if this is a combo scaling spell by name
if not duration and CleveRoids.IsComboScalingSpell and CleveRoids.IsComboScalingSpell(effect) then
if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
local tracking = CleveRoids.ComboPointTracking[effect]
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
duration = tracking.duration
caster = caster or "player"
end
end
end
return originalAddEffect(self, unit, unitlevel, effect, duration, caster)
end
Extension.pfLibAddEffectHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect")
end
-- Hook UnitDebuff to return Carnage override duration to display code
-- Only override when pfUI's duration differs significantly from ours
if pflib.UnitDebuff and not Extension.pfLibUnitDebuffHooked then
local originalUnitDebuff = pflib.UnitDebuff
pflib.UnitDebuff = function(self, unit, id)
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitDebuff(self, unit, id)
-- Only check Carnage override if pfUI returned data but duration might be wrong
if effect then
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if there's a significant difference (>1s)
if not duration or math.abs(carnageDuration - duration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI UnitDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
)
end
duration = carnageDuration
timeleft = carnageTimeleft
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
Extension.pfLibUnitDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitDebuff (mismatch-only mode)")
end
-- Hook UnitOwnDebuff to return Carnage override duration when selfdebuff is enabled
-- Only override when pfUI's duration differs significantly from ours
if pflib.UnitOwnDebuff and not Extension.pfLibUnitOwnDebuffHooked then
local originalUnitOwnDebuff = pflib.UnitOwnDebuff
pflib.UnitOwnDebuff = function(self, unit, id)
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitOwnDebuff(self, unit, id)
if effect then
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if there's a significant difference (>1s)
if not duration or math.abs(carnageDuration - duration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI UnitOwnDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
)
end
duration = carnageDuration
timeleft = carnageTimeleft
end
end
-- If UnitOwnDebuff returned nil but we have a Carnage override, synthesize from UnitDebuff
-- This fallback is only needed for edge cases where pfUI doesn't track the debuff yet
elseif not effect and CleveRoids.carnageDurationOverrides then
-- Use pflib:UnitDebuff which includes our Carnage override hook
local baseEffect, baseRank, baseTex, baseStacks, baseDtype, baseDur, baseLeft, _ = pflib:UnitDebuff(unit, id)
if baseEffect then
local carnageDuration2, carnageTimeleft2 = GetCarnageOverride(baseEffect)
if carnageDuration2 then
return baseEffect, baseRank, baseTex, baseStacks, baseDtype, carnageDuration2, carnageTimeleft2, "player"
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
Extension.pfLibUnitOwnDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitOwnDebuff (mismatch-only mode)")
end
return Extension.pfLibDebuffHooked or Extension.pfLibAddEffectHooked or Extension.pfLibUnitDebuffHooked or Extension.pfLibUnitOwnDebuffHooked
end
-- Synchronize combo durations to pfUI's libdebuff objects
-- NOTE: pfUI 7.6+ handles combo durations internally - skip sync entirely
function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
return
end
-- pfUI 7.6+ handles all durations internally
if CleveRoids.hasPfUI76 then
return
end
-- Get unit name from GUID
local unitName = nil
local unitLevel = 0
-- Check if this is the current target
local targetGUID = CleveRoids.GetGUID("target")
if targetGUID == guid then
unitName = UnitName("target")
unitLevel = UnitLevel("target") or 0
end
-- If we couldn't find the unit, use GUID to name mapping from libdebuff
if not unitName and CleveRoids.libdebuff and CleveRoids.libdebuff.guidToName then
unitName = CleveRoids.libdebuff.guidToName[guid]
-- Default to level 0 if we don't have the unit targeted
unitLevel = 0
end
if not unitName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find unit name for GUID")
end
return
end
-- Get spell name from spell ID
local spellName = GetSpellRecField(spellID, "name")
if not spellName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID)
end
return
end
-- Remove rank from spell name to match pfUI's format
local effectName = CleveRoids.StripRank(spellName)
-- Update pfUI's stored debuff duration
local pflib = pfUI.api.libdebuff
if pflib.objects and pflib.objects[unitName] then
-- Try both the specific level and level 0 (fallback)
for _, level in ipairs({unitLevel, 0}) do
if pflib.objects[unitName][level] and pflib.objects[unitName][level][effectName] then
local old_duration = pflib.objects[unitName][level][effectName].duration
pflib.objects[unitName][level][effectName].duration = duration
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[pfUI Sync]|r Updated %s on %s (L%d): %ds -> %ds",
effectName, unitName, level, old_duration or 0, duration)
)
end
return
end
end
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaa00[pfUI Sync]|r Effect not found in pfUI storage: %s on %s",
effectName, unitName)
)
end
end
-- Register action event handler for pfUI button updates
function Extension.RegisterPfUIActionEventHandler()
if not pfUI or Extension.actionHandlerRegistered then
@@ -428,7 +88,17 @@ function Extension.RegisterPfUIActionEventHandler()
-- Register a handler that will be called whenever CleveRoids updates macro states
if CleveRoids.RegisterActionEventHandler then
Extension.DLOG("Registering pfUI action event handler")
CleveRoids.RegisterActionEventHandler(function(slot, event, ...)
-- Declared without `...` on purpose: in 1.12's Lua 5.0 a vararg function
-- allocates an `arg` table on every call, and this handler never read it.
-- That mattered because UpdateAllManagedCooldowns fans
-- ACTIONBAR_UPDATE_COOLDOWN out across every managed slot -- up to 120 calls
-- -- on each SPELL_UPDATE_COOLDOWN, which fires on every GCD and cooldown
-- tick. Those calls all allocated a table and then did nothing, because the
-- whole body only ever applied to ACTIONBAR_SLOT_CHANGED. Hence the early
-- return before any work.
CleveRoids.RegisterActionEventHandler(function(slot, event)
if event ~= "ACTIONBAR_SLOT_CHANGED" then return end
local button = pfUI.bars and pfUI.bars.buttons and pfUI.bars.buttons[slot]
if Extension.Debug then
@@ -440,23 +110,21 @@ function Extension.RegisterPfUIActionEventHandler()
))
end
-- For slot change events, do a full button update so the icon,
-- cooldown, and tooltip refresh through CleveRoids' hooked
-- GetActionTexture / GetActionCooldown / GameTooltip:SetAction.
-- Full button update so the icon, cooldown and tooltip refresh.
-- pfUI's ButtonMacroScan defers to us for managed macros (leaves
-- spellslot nil), so its ButtonFullUpdate routes through those hooks
-- and follows the active conditional — no manual cooldown override
-- needed.
if event == "ACTIONBAR_SLOT_CHANGED" then
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
if pfUI.bars and pfUI.bars.update then
pfUI.bars.update[slot] = true
end
-- spellslot nil), so ButtonFullUpdate reads the stock action-bar
-- functions -- which now resolve through the value we publish with
-- C_Macro.SetMacroDisplay, rather than the Lua overrides this addon
-- used to install.
-- Also directly call ButtonFullUpdate if the button exists
if button and pfUI.bars.ButtonFullUpdate then
pfUI.bars.ButtonFullUpdate(button)
end
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
if pfUI.bars and pfUI.bars.update then
pfUI.bars.update[slot] = true
end
-- Also directly call ButtonFullUpdate if the button exists
if button and pfUI.bars.ButtonFullUpdate then
pfUI.bars.ButtonFullUpdate(button)
end
end)
@@ -473,9 +141,6 @@ function Extension.SetupCompatibility()
if Extension.pfUILoaded then
Extension.DLOG("pfUI detected")
-- Hook libdebuff for combo duration support
Extension.HookPfUILibdebuff()
-- Register action event handler for button updates
Extension.RegisterPfUIActionEventHandler()
@@ -630,16 +295,9 @@ function Extension.SetupPfUIEventHooks(lib)
ev:UnregisterEvent("AURA_CAST_ON_OTHER")
ev:UnregisterEvent("DEBUFF_ADDED_OTHER")
ev:UnregisterEvent("DEBUFF_REMOVED_OTHER")
ev:UnregisterEvent("BUFF_ADDED_OTHER")
ev:UnregisterEvent("BUFF_REMOVED_SELF")
ev:UnregisterEvent("BUFF_REMOVED_OTHER")
-- pfUI 7.6+ also handles cast tracking internally
if lib.hasPfUI76 then
ev:UnregisterEvent("SPELL_START_OTHER")
ev:UnregisterEvent("SPELL_FAILED_OTHER")
end
-- Keep registered: SPELL_START_SELF (channel duration capture before early return),
-- UNIT_DIED (AllCasterAuraTracking + OverflowBuff cleanup), UNIT_CASTEVENT (SuperWoW),
-- PLAYER_TARGET_CHANGED, UNIT_AURA (SeedUnit)
@@ -699,7 +357,6 @@ end
function Extension.OnLoad()
Extension.DLOG("Extension pfUI Loaded.")
Extension.HookMethod(CleveRoids, "GetFocusName", "FocusNameHook", true)
-- Export extension for external access
CleveRoids.Compatibility_pfUI = Extension
@@ -726,24 +383,23 @@ function Extension.OnLoad()
SLASH_PFUICD1 = "/pfuicd"
end
function Extension.ADDON_LOADED()
-- Check if pfUI just loaded AND the global actually exists
-- (another addon could be named "pfUI" without being the real UI framework)
if arg1 == "pfUI" and pfUI then
Extension.pfUILoaded = true
-- pfUI modules load after ADDON_LOADED, so schedule a check
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
-- Fires once pfUI has loaded (immediately if it loaded before us, via EventUtil).
function Extension.OnPfUILoaded()
-- Guard: only the real pfUI framework sets this global (another addon could be
-- named "pfUI" without being the UI framework).
if not pfUI then return end
Extension.pfUILoaded = true
-- pfUI's submodules initialize after its ADDON_LOADED, so defer the setup.
if CleveRoids.ScheduleTimer then
CleveRoids.ScheduleTimer(function()
Extension.SetupCompatibility()
end, 0.5)
end
end
function Extension.PLAYER_LOGIN()
-- If pfUI loaded before SCRM (alphabetical order), ADDON_LOADED for pfUI was missed.
-- Re-run InitPfUIIntegration here to ensure lib.objects is linked correctly.
if pfUI and not CleveRoids.hasPfUI76 then
function Extension.OnPlayerLogin()
-- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
if pfUI then
local lib = CleveRoids.libdebuff
if lib and lib.InitPfUIIntegration then
lib:InitPfUIIntegration()
@@ -775,9 +431,6 @@ function Extension.PLAYER_LOGIN()
-- Print startup status only if pfUI global exists and compatibility was set up
if Extension.pfUILoaded and pfUI then
local statusMsg = "|cff00ff00[SCRM]|r pfUI compatibility loaded"
if CleveRoids.hasPfUI76 then
statusMsg = statusMsg .. " (7.6+ GUID cast tracking)"
end
-- statusMsg = statusMsg .. ". Use /pfuicd for debug."
DEFAULT_CHAT_FRAME:AddMessage(statusMsg)
if not Extension.actionHandlerRegistered then
@@ -797,4 +450,10 @@ if not CleveRoids.ScheduleTimer then
end
end
-- Wire handlers via ClassicAPI EventUtil (fires immediately if the event already
-- happened, so load order relative to pfUI no longer matters). Registered here,
-- after the handlers are defined, since ContinueOnAddOnLoaded may fire inline.
EventUtil.ContinueOnAddOnLoaded("pfUI", Extension.OnPfUILoaded)
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
_G["CleveRoids"] = CleveRoids
+554 -532
View File
File diff suppressed because it is too large Load Diff
+47 -8
View File
@@ -63,10 +63,24 @@ SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1
SLASH_EQSLOT141 = "/equip14"
SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2
-- Reclaim ClassicAPI's /equipset (command name EQUIP_SET) so it supports
-- conditionals. ClassicAPI registers the localized aliases against EQUIP_SET;
-- swapping the handler keeps every alias and adds the conditional engine.
if SlashCmdList.EQUIP_SET then
SlashCmdList.EQUIP_SET = CleveRoids.DoEquipSet
else
SLASH_EQUIPSET1 = "/equipset"
SlashCmdList.EQUIPSET = CleveRoids.DoEquipSet
end
SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
SLASH_CANCELFORM1 = "/cancelform"
SlashCmdList.CANCELFORM = CleveRoids.DoCancelForm
SLASH_UNQUEUE1 = "/unqueue"
SlashCmdList.UNQUEUE = SpellStopCasting
@@ -116,6 +130,12 @@ local StopAttack = function(msg)
CleveRoids.DeferStopAttack()
end
local StopChanneling = function(msg)
-- No Blizzard equivalent: 1.12 has SpellStopCasting (immediate) and nothing
-- that waits for the next channel tick, so this is nampower-only.
CleveRoids.StopChanneling()
end
-- Register slash commands and assign original handlers.
-- These will be hooked immediately after.
SLASH_STARTATTACK1 = "/startattack"
@@ -127,6 +147,9 @@ SlashCmdList.STOPATTACK = StopAttack
SLASH_STOPCASTING1 = "/stopcasting"
SlashCmdList.STOPCASTING = SpellStopCasting
SLASH_STOPCHANNELING1 = "/stopchanneling"
SlashCmdList.STOPCHANNELING = StopChanneling
SLASH_CLEARTARGET1 = "/cleartarget"
SlashCmdList.CLEARTARGET = ClearTarget
@@ -188,6 +211,21 @@ SlashCmdList.STOPCASTING = function(msg)
end
end
-- /stopchanneling hook
CleveRoids.Hooks.STOPCHANNELING_SlashCmd = SlashCmdList.STOPCHANNELING
SlashCmdList.STOPCHANNELING = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
-- If conditionals are present, let the function handle it.
-- It will only stop the channel if the conditions are met.
CleveRoids.DoConditionalStopChanneling(msg)
else
-- If no conditionals, run the original command.
CleveRoids.Hooks.STOPCHANNELING_SlashCmd(msg)
end
end
-- /unqueue hook
CleveRoids.Hooks.UNQUEUE_SlashCmd = SlashCmdList.UNQUEUE
SlashCmdList.UNQUEUE = function(msg)
@@ -320,14 +358,15 @@ SlashCmdList.RUNMACRO = function(msg)
return CleveRoids.ExecuteMacroByName(CleveRoids.Trim(msg))
end
-- Global RunMacro wrapper for user convenience (delegates to namespaced internal function)
-- This pattern ensures internal logic uses CleveRoids.ExecuteMacroByName and won't break
-- if another addon overwrites the global RunMacro
-- NOTE: When SuperMacro is also loaded, Compatibility/SuperMacro.lua redirects this to
-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted)
function RunMacro(name)
return CleveRoids.ExecuteMacroByName(name)
end
-- The global RunMacro is installed by Core.lua, which loads before this file. Do not
-- redefine it here: Core's hook accepts a macro index as well as a name (Blizzard's
-- RunMacro takes either), delegates to SuperMacro when that is driving execution,
-- falls back to the saved Blizzard original when a macro will not resolve, and clears
-- the stop/skip flags at the start of a top-level run, which is what lets /stopmacro,
-- /skipmacro and /firstaction work across parent/child macro boundaries. A plain
-- name-only wrapper here silently replaced all of that.
-- When SuperMacro is loaded, Compatibility/SuperMacro.lua redirects the global to
-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted).
SLASH_RETARGET1 = "/retarget"
SlashCmdList.RETARGET = function(msg)
+863 -1454
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -209,8 +209,8 @@ local function InjectCustomSpells()
local count = 0
for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do
-- Get texture from GetSpellRecField + GetSpellIconTexture
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if texture then
-- Always update/add (in case Cursive reloaded and cleared them)
@@ -524,8 +524,8 @@ CleveRoids.HandleConsoleCommand = function(msg)
return
end
local name = GetSpellRecField(spellID, "name")
local rank = GetSpellRecField(spellID, "rank")
local name = C_Spell.GetSpellName(spellID)
local rank = C_Spell.GetSpellSubtext(spellID)
local texture = CleveRoids.libdebuff and CleveRoids.libdebuff:GetCachedIcon(spellID)
if not name then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.")
+43 -37
View File
@@ -697,30 +697,9 @@ local function EnsureCondHighlightPool()
end
end
-- Test whether conditionals pass for a given command + alternative text.
-- Returns: true (passes), false (fails), nil (unconditional / no conditionals)
local function TestConditionalPasses(cmd, alternative)
-- Strip ? tooltip hints (irrelevant for conditional evaluation)
if string.find(alternative, "?", 1, true) then
alternative = string.gsub(alternative, "%?", "")
end
local hasConditional = string.find(alternative, "%[") ~= nil
-- Dynamic commands: delegate to TestAction
if CleveRoids.dynamicCmds[cmd] then
local result = CleveRoids.TestAction(cmd, alternative)
if not hasConditional then
return nil -- unconditional
end
return result ~= nil and result ~= false
end
-- Non-dynamic commands: parse and evaluate Keywords manually
if not hasConditional then
return nil -- unconditional
end
-- Evaluate one single-group clause of a non-dynamic command against Keywords.
-- Returns: true (passes), false (fails), nil (could not parse)
local function EvaluateNonDynamic(alternative)
local ok, action, conditionals = pcall(CleveRoids.GetParsedMsg, alternative)
if not ok or not conditionals then
return nil
@@ -745,6 +724,43 @@ local function TestConditionalPasses(cmd, alternative)
return passes
end
-- Test whether conditionals pass for a given command + alternative text.
-- Returns: true (passes), false (fails), nil (unconditional / no conditionals)
local function TestConditionalPasses(cmd, alternative)
-- Strip ? tooltip hints (irrelevant for conditional evaluation)
if string.find(alternative, "?", 1, true) then
alternative = string.gsub(alternative, "%?", "")
end
local hasConditional = string.find(alternative, "%[") ~= nil
-- Dynamic commands: delegate to TestAction
if CleveRoids.dynamicCmds[cmd] then
local result = CleveRoids.TestAction(cmd, alternative)
if not hasConditional then
return nil -- unconditional
end
return result ~= nil and result ~= false
end
-- Non-dynamic commands: evaluate Keywords manually. `[a][b] X` passes when
-- any of its groups does.
if not hasConditional then
return nil -- unconditional
end
local variants = CleveRoids.ExpandBracketGroups(alternative)
if not variants then
return EvaluateNonDynamic(alternative)
end
for i = 1, variants.n do
if EvaluateNonDynamic(variants[i]) then
return true
end
end
return false
end
-- Find character offset ranges for each semicolon-separated alternative.
-- Returns array of { start, finish } pairs (1-based indices within argsText).
local function FindAlternativeOffsets(argsText)
@@ -1391,12 +1407,6 @@ end
-- Extension Entry Points
-- ============================================================================
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
InstallHooks()
end
end
function Extension.OnLoad()
-- Skip if macro checker is disabled
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end
@@ -1404,13 +1414,9 @@ function Extension.OnLoad()
-- Skip if SuperMacro is loaded (detected at load time)
if SuperMacroFrame ~= nil then return end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- If MacroFrame already exists (unlikely but safe), hook immediately
if MacroFrame and MacroFrameText then
InstallHooks()
end
-- Install once Blizzard's macro UI is available (fires immediately if already
-- loaded), replacing the ADDON_LOADED listener + manual "already loaded" check.
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", InstallHooks)
end
_G["CleveRoids"] = CleveRoids
+4 -15
View File
@@ -133,12 +133,6 @@ function Extension.OnMacroFrameLoad()
end
end
function Extension.OnAddonLoaded()
if arg1 == "Blizzard_MacroUI" then
Extension.OnMacroFrameLoad()
end
end
function Extension.OnLoad()
-- Schedule messages to show after UI is ready
local function ShowMessages()
@@ -178,16 +172,11 @@ function Extension.OnLoad()
end
end
-- Listen for macro UI loading
Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded")
-- Hook the macro UI once available (fires immediately if already loaded).
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", Extension.OnMacroFrameLoad)
-- Also try to hook MacroFrame_SaveMacro if it already exists
if MacroFrame_SaveMacro then
Extension.OnMacroFrameLoad()
end
-- Register PLAYER_LOGIN to show status messages
Extension.RegisterEvent("PLAYER_LOGIN", "OnPlayerLogin")
-- Status messages on login (currently disabled inside OnPlayerLogin).
EventUtil.ContinueOnPlayerLogin(Extension.OnPlayerLogin)
-- Store the message function for later
Extension.ShowMessages = ShowMessages
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_RaidAssist")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
local tempOptions = CT_RAMenu_Options["temp"]
@@ -48,15 +47,14 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "CT_RaidAssist" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_RA_MemberFrame_OnEnter then return end
Extension.Hook("CT_RA_MemberFrame_OnEnter", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_RaidAssist", Extension.OnAddOnLoad)
+5 -4
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("CT_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.SetHook(widget)
local hookedOnEnter = widget:GetScript("OnEnter")
@@ -24,8 +23,10 @@ function Extension.SetHook(widget)
end)
end
function Extension.OnLoad()
if arg1 ~= "CT_UnitFrames" or not CT_AssistFrame then
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not CT_AssistFrame then
return
end
CleveRoids.Print("CT_UnitFrames module loaded.")
@@ -36,4 +37,4 @@ function Extension.OnLoad()
Extension.SetHook(CT_AssistFrame_Drag)
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("CT_UnitFrames", Extension.OnAddOnLoad)
+8 -17
View File
@@ -9,7 +9,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("CursiveMouseover")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
local hooked = false
@@ -66,21 +65,13 @@ local function HookCursiveUI()
end
function Extension.OnLoad()
-- Try to hook when Cursive loads
if arg1 == "Cursive" then
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Delay slightly to ensure Cursive.ui is initialized
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
if HookCursiveUI() then
this:Hide()
end
end)
end
-- Also try to hook immediately in case Cursive is already loaded
if Cursive and Cursive.ui then
HookCursiveUI()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Cursive", Extension.OnLoad)
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("DiscordUnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnterFrame()
CleveRoids.SetMouseoverFrom("duf", this.unit)
@@ -26,11 +25,10 @@ function Extension.OnLeaveElement()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "DiscordUnitFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not DUF_UnitFrame_OnEnter then return end
CleveRoids.ClearHooks()
Extension.Hook("DUF_UnitFrame_OnEnter", "OnEnterFrame")
Extension.Hook("DUF_UnitFrame_OnLeave", "OnLeaveFrame")
@@ -39,4 +37,4 @@ function Extension.OnLoad()
Extension.Hook("DUF_Element_OnLeave", "OnLeaveElement")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("DiscordUnitFrames", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("Grid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("grid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "Grid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not GridFrame then return end
CleveRoids.Hooks.Grid = { CreateFrames = GridFrame.frameClass.prototype.CreateFrames}
GridFrame.frameClass.prototype.CreateFrames = CleveRoids.GrdCreateFrames
end
@@ -111,4 +109,4 @@ function CleveRoids:GrdCreateFrames()
ClickCastFrames[self.frame] = true
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("Grid", Extension.OnAddOnLoad)
+4 -4
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
local CreateFrames = nil
local Extension = CleveRoids.RegisterExtension("NotGrid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter()
CleveRoids.SetMouseoverFrom("ngrid", this.unit)
@@ -39,7 +38,9 @@ function CleveRoids:NotGrid_CreateFrames()
end
end
function Extension.OnLoad()
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
-- NotGrid loads before CleveRoids, so if NotGrid is enabled, then it's global will exist.
if not NotGrid then
return
@@ -48,7 +49,6 @@ function Extension.OnLoad()
CreateFrames = NotGrid.CreateFrames
NotGrid.CreateFrames = CleveRoids.NotGrid_CreateFrames
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("NotGrid", Extension.OnAddOnLoad)
+4 -6
View File
@@ -8,7 +8,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("PerfectRaid")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("praid", unit)
@@ -19,11 +18,10 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "PerfectRaid" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not PerfectRaid then return end
CleveRoids.Hooks.PerfectRaid = { CreateFrame = PerfectRaid.CreateFrame }
PerfectRaid.CreateFrame = CleveRoids.PerfectRaidCreateFrame
end
@@ -134,4 +132,4 @@ function CleveRoids.PerfectRaidCreateFrame(self, num)
--]]
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("PerfectRaid", Extension.OnAddOnLoad)
+1 -3
View File
@@ -4,7 +4,6 @@ local CleveRoids = _G.CleveRoids or {}
CleveRoids.Hooks = CleveRoids.Hooks or {}
local Extension = CleveRoids.RegisterExtension("ag_UnitFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension.OnEnter(unit)
CleveRoids.SetMouseoverFrom("aguf", unit)
@@ -23,7 +22,6 @@ function Extension.OnLoad()
CleveRoids.Hooks.ag_UnitFrames = { OnEnter = aUF.classes.aUFunit.prototype.OnEnter, OnLeave = aUF.classes.aUFunit.prototype.OnLeave}
aUF.classes.aUFunit.prototype.OnEnter = CleveRoids.aUFOnEnter
aUF.classes.aUFunit.prototype.OnLeave = CleveRoids.aUFOnLeave
Extension.UnregisterEvent("ADDON_LOADED", "Onload")
end
-- Taken from ag_UnitClass.lua
@@ -41,4 +39,4 @@ function CleveRoids:aUFOnLeave()
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("ag_UnitFrames", Extension.OnLoad)
+3 -4
View File
@@ -8,6 +8,9 @@
- Resolving a real UnitID when .unit isn't set
- Properly hooking party group[0] (your own party slot) with a safe closure and defaulting to "player"
]]
if pfPlayer and pfPlayer.GetAttribute and pfPlayer:GetAttribute('unit') == 'player' then return end
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
@@ -31,7 +34,6 @@ local function ResolvePfUnit(frame, fallbackName)
name = strlower(name)
local candidates = { "target", "targettarget", "player", "pet" }
local i
for i = 1, 4 do
table.insert(candidates, "party"..i)
table.insert(candidates, "partypet"..i)
@@ -153,7 +155,6 @@ end
function Extension.RegisterPartyScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.group then return end
local i
for i = 0, 4 do
local frame = pfUI.uf.group[i]
if frame then
@@ -181,7 +182,6 @@ end
function Extension.RegisterRaidScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end
local i
for i = 1, 40 do
local frame = pfUI.uf.raid[i]
if frame then
@@ -333,7 +333,6 @@ end
function Extension.RegisterRaidMarkScripts()
if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end
local i
for i = 1, 8 do
local row = pfUI.raidmarkers.rows[i]
if row then
+4 -6
View File
@@ -6,7 +6,6 @@ local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("sRaidFrames")
Extension.RegisterEvent("ADDON_LOADED", "OnLoad")
function Extension:OnEnter(frame)
CleveRoids.SetMouseoverFrom("sraid", frame.unit)
@@ -17,14 +16,13 @@ function Extension.OnLeave()
CleveRoids.ClearMouseoverFrom("native")
end
function Extension.OnLoad()
if arg1 ~= "sRaidFrames" then
return
end
function Extension.OnLoad() end
function Extension.OnAddOnLoad()
if not sRaidFrames then return end
Extension.HookMethod(sRaidFrames, "UnitTooltip", "OnEnter")
Extension.HookMethod(_G["GameTooltip"], "Hide", "OnLeave")
Extension.HookMethod(_G["GameTooltip"], "FadeOut", "OnLeave")
end
_G["CleveRoids"] = CleveRoids
EventUtil.ContinueOnAddOnLoaded("sRaidFrames", Extension.OnAddOnLoad)
+2 -2
View File
@@ -122,7 +122,7 @@ local function CreateIconButton(parent, index, iconTable)
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName
if data.spellId then
spellName = GetSpellRecField and GetSpellRecField(data.spellId, "name") or ("Spell " .. data.spellId)
spellName = C_Spell.GetSpellName(data.spellId) or ("Spell " .. data.spellId)
else
spellName = data.displayName
end
@@ -649,7 +649,7 @@ local function InjectTestTargetData()
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") or ("TestSpell" .. spellId)
local spellName = C_Spell.GetSpellName(spellId) or ("TestSpell" .. spellId)
if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then
CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {}
end
+127 -139
View File
@@ -58,11 +58,7 @@ function CleveRoids.IndexSpells()
bookType = CleveRoids.bookTypes[book]
spells[bookType] = {}
else
local cost, reagent = CleveRoids.GetSpellCost(i, bookType)
-- Fallback for known reagent spells if tooltip scan failed
if (not reagent or reagent == "") and CleveRoids.ReagentBySpell then
reagent = CleveRoids.ReagentBySpell[spellName]
end
local cost, reagent, reagentId = CleveRoids.GetSpellCost(i, bookType, spellId)
if not spells[bookType][spellName] then
spells[bookType][spellName] = {
spellSlot = i,
@@ -71,7 +67,7 @@ function CleveRoids.IndexSpells()
bookType = bookType,
texture = texture,
cost = cost,
reagent = reagent,
reagentId = reagentId,
}
end
if spellRank and not spells[bookType][spellName][spellRank] then
@@ -83,7 +79,7 @@ function CleveRoids.IndexSpells()
bookType = bookType,
texture = texture,
cost = cost,
reagent = reagent
reagentId = reagentId,
}
spells[bookType][spellName].highest = spells[bookType][spellName][spellRank]
end
@@ -94,6 +90,13 @@ function CleveRoids.IndexSpells()
if reagent then
CleveRoids.countedItemTypes[reagent] = true
elseif reagentId and Item then
Item:CreateFromItemID(reagentId):ContinueOnItemLoad(function()
local loadedName = C_Item.GetItemNameByID(reagentId)
if loadedName and loadedName ~= "" then
CleveRoids.countedItemTypes[loadedName] = true
end
end)
end
end
end
@@ -153,56 +156,28 @@ end
-- Lightweight equipment-only indexing for combat situations
-- Updates existing cache rather than rebuilding it
function CleveRoids.IndexEquippedItems()
local items = CleveRoids.Items or {}
for inventoryID = 0, 19 do
local link = GetInventoryItemLink("player", inventoryID)
if link then
local _, _, itemID = string.find(link, "item:(%d+)")
local name, link, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
if not items[name] then
items[name] = {
inventoryID = inventoryID,
id = itemID,
name = name,
count = count,
texture = texture,
link = link,
}
items[itemID] = name
local lowerName = string.lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
-- Update existing entry with current equipment state
items[name].inventoryID = inventoryID
items[name].count = count
end
end
else
-- Slot is now empty - clear inventoryID from any item that was there
-- This is handled lazily by GetItem() fallback, so we skip expensive iteration
end
end
CleveRoids.lastGetItem = nil
CleveRoids.Items = items
end
-- PERFORMANCE: Index a single equipment slot instead of all 20
-- Use when we know exactly which slot changed (e.g., from EquipBagItem)
function CleveRoids.IndexEquipSlot(inventoryID)
-- Use when we know exactly which slot changed (e.g., from EquipBagItem or
-- PLAYER_EQUIPMENT_CHANGED). hasCurrent is the event's arg2 (does the slot now
-- hold an item); pass false to skip the API probe on a slot we know is empty.
function CleveRoids.IndexEquipSlot(inventoryID, hasCurrent)
if not inventoryID then return end
local items = CleveRoids.Items or {}
local link = GetInventoryItemLink("player", inventoryID)
if link then
local _, _, itemID = string.find(link, "item:(%d+)")
-- Clear any stale inventoryID still pointing at this slot: the item that was
-- here is now unequipped or swapped out (its real location comes from the
-- paired BAG_UPDATE rebuild). Equip changes are user-paced, so this table
-- scan is off the hot path.
for _, entry in pairs(items) do
if type(entry) == "table" and entry.inventoryID == inventoryID then
entry.inventoryID = nil
end
end
-- hasCurrent == false (arg2) => slot is now empty, nothing to add.
local itemID = hasCurrent ~= false and GetInventoryItemID("player", inventoryID)
if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
@@ -263,23 +238,27 @@ function CleveRoids.IndexItems()
local items = {}
local NUM_BAG_SLOTS = NUM_BAG_SLOTS -- Upvalue for bag constant
-- Rebuilt each pass: itemIDs the player owns that GetItemInfo couldn't
-- resolve yet (cold cache under ClassicAPI's async warmup). The
-- GET_ITEM_INFO_RECEIVED handler consults this so it only re-indexes for
-- our own uncached items and ignores the flood of unrelated fills (quest
-- DB scans, AH, chat-link hovers, inspects) in O(1).
local pendingItemInfo = {}
CleveRoids.pendingItemInfo = pendingItemInfo
-- PERFORMANCE: Local function references
local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemLink = GetContainerItemLink
local GetContainerItemInfo = GetContainerItemInfo
local GetInventoryItemLink = GetInventoryItemLink
local GetInventoryItemCount = GetInventoryItemCount
-- Scan bags (reverse order to prefer first stack)
for bagID = 0, NUM_BAG_SLOTS do
local numSlots = GetContainerNumSlots(bagID)
for slot = numSlots, 1, -1 do
local link = GetContainerItemLink(bagID, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
-- PERFORMANCE: Try to extract name from link first to check for duplicates
local _, _, linkName = string_find(link, "%[(.+)%]")
local itemID = C_Container.GetContainerItemID(bagID, slot)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ bagID = bagID, slotIndex = slot })
local existing = linkName and items[linkName]
if existing then
@@ -310,6 +289,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
@@ -317,13 +299,11 @@ function CleveRoids.IndexItems()
end
-- Scan equipped items
for inventoryID = 0, 19 do
local link = GetInventoryItemLink("player", inventoryID)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
-- PERFORMANCE: Try to extract name from link first
local _, _, linkName = string_find(link, "%[(.+)%]")
for inventoryID = 1, 19 do
local itemID = GetInventoryItemID("player", inventoryID)
if itemID then
-- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local existing = linkName and items[linkName]
if existing then
@@ -349,6 +329,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
@@ -383,9 +366,9 @@ function CleveRoids.GetActionButtonInfo(slot)
if not actionType then return end
if actionType == "spell" and id then
local rank = GetSpellRecField(id, "rank")
local rank = C_Spell.GetSpellSubtext(id)
if rank == "" then rank = nil end
return "SPELL", id, GetSpellRecField(id, "name"), rank
return "SPELL", id, C_Spell.GetSpellName(id), rank
elseif actionType == "item" and id then
local item = CleveRoids.GetItem(id)
return "ITEM", id, (item and item.name)
@@ -440,7 +423,9 @@ function CleveRoids.IndexActionSlot(slot)
end
end
end
CleveRoids.TestForActiveAction(CleveRoids.GetAction(slot))
local actions = CleveRoids.GetAction(slot)
CleveRoids.TestForActiveAction(actions)
CleveRoids.PublishDisplay(actions)
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
end
@@ -452,6 +437,27 @@ end
function CleveRoids.GetSpell(text)
text = CleveRoids.Trim(text)
-- Explicit "spell:<id>" form. If the player knows the spell, reuse its cached
-- spellbook entry (full cost/cooldown/usability). FindSpellBookSlotByID
-- (ClassicAPI) resolves per-rank IDs and pet spells natively and returns the
-- same bookType string CleveRoids.Spells is keyed by. If the spell is not in
-- either book, fall back to an id-only entry that SetAction renders via
-- SetSpellByID; cost=0 keeps the downstream active-action checks nil-safe.
local _, _, spellId = string.find(text, "^spell:(%d+)$")
if spellId then
spellId = tonumber(spellId)
local slot, book = FindSpellBookSlotByID(spellId)
if slot then
local name, rank = GetSpellInfo(slot, book)
local byName = name and CleveRoids.Spells[book] and CleveRoids.Spells[book][name]
if byName then
return (rank and rank ~= "" and byName[rank]) or byName.highest or byName
end
end
return { id = spellId, texture = C_Spell.GetSpellTexture(spellId) or CleveRoids.unknownTexture, cost = 0 }
end
local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$")
local name = rank and string.sub(text, 1, rs) or text
@@ -477,10 +483,8 @@ local function makeInventoryItem(inventoryID, link, Items)
if not link then link = GetInventoryItemLink("player", inventoryID) end
if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local name = itemID and GetItemInfo(itemID) or nil
local itemID = GetInventoryItemID("player", inventoryID)
local name = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local texture = GetInventoryItemTexture("player", inventoryID)
local count = GetInventoryItemCount("player", inventoryID)
@@ -509,14 +513,12 @@ local function makeBagItem(bagID, slot, link, Items)
end
if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = C_Container.GetContainerItemID(bagID, slot)
local name, _, _, _, _, _, _, _, texture = GetItemInfo(itemID)
local name = C_Item.GetItemName({bagID = bagID, slotIndex = slot})
local count = 0
local tex, itemCount = GetContainerItemInfo(bagID, slot)
local texture, itemCount = GetContainerItemInfo(bagID, slot)
if itemCount then count = itemCount end
if not texture then texture = tex end
local it = {
bagID = bagID,
@@ -543,6 +545,12 @@ end
function CleveRoids.GetItem(text)
if not text or text == "" then return end
-- Explicit "item:<id>" form: force resolution by item ID. Reuses the numeric
-- lookup below (equipped -> bags -> GetItemInfo), so location-aware tooltip
-- rendering (SetInventoryItem/SetBagItem) still applies when the player has it.
local _, _, prefixedId = string.find(text, "^item:(%d+)$")
if prefixedId then text = prefixedId end
local Items = CleveRoids.Items
local item = Items[text] or Items[tostring(text)]
if not item then
@@ -574,9 +582,7 @@ function CleveRoids.GetItem(text)
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = GetInventoryItemID("player", inv)
if qid and itemID and qid == itemID then
return makeInventoryItem(inv, link, Items)
elseif qname then
@@ -595,9 +601,7 @@ function CleveRoids.GetItem(text)
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
itemID = itemID and tonumber(itemID) or nil
local itemID = C_Container.GetContainerItemID(bag, slot)
if qid and itemID and qid == itemID then
return makeBagItem(bag, slot, link, Items)
elseif qname then
@@ -720,18 +724,16 @@ function CleveRoids.FindItemQuick(text)
if cached then
-- Validate: check if item is actually at the cached location
if cached.inventoryID then
local link = GetInventoryItemLink("player", cached.inventoryID)
if link then
local nm = GetNameFromLink(link)
if qid then
if GetInventoryItemID("player", cached.inventoryID) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ equipmentSlotIndex = cached.inventoryID })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
elseif qid then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID and tonumber(itemID) == qid then
cached._validated = true
return cached -- Cache is valid
end
end
end
-- Cache is stale - item not at cached equipped slot, invalidate
@@ -740,18 +742,16 @@ function CleveRoids.FindItemQuick(text)
Items[string_lower(cached.name)] = nil
end
elseif cached.bagID and cached.slot then
local link = GetContainerItemLink(cached.bagID, cached.slot)
if link then
local nm = GetNameFromLink(link)
if qid then
if C_Container.GetContainerItemID(cached.bagID, cached.slot) == qid then
cached._validated = true
return cached -- Cache is valid
end
else
local nm = C_Item.GetItemName({ bagID = cached.bagID, slotIndex = cached.slot })
if nm and qname and string_lower(nm) == qname then
cached._validated = true
return cached -- Cache is valid
elseif qid then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID and tonumber(itemID) == qid then
cached._validated = true
return cached -- Cache is valid
end
end
end
-- Cache is stale - item not at cached bag slot, invalidate
@@ -767,20 +767,17 @@ function CleveRoids.FindItemQuick(text)
for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID then
itemID = tonumber(itemID)
-- ID match: fast path
if qid and qid == itemID then
local itemID = GetInventoryItemID("player", inv)
-- ID match: fast path
if qid and qid == itemID then
return makeInventoryItem(inv, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeInventoryItem(inv, link, Items)
end
end
end
end
end
@@ -791,20 +788,17 @@ function CleveRoids.FindItemQuick(text)
for slot = 1, slots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemID = string_find(link, "item:(%d+)")
if itemID then
itemID = tonumber(itemID)
-- ID match: fast path
if qid and qid == itemID then
local itemID = C_Container.GetContainerItemID(bag, slot)
-- ID match: fast path
if qid and qid == itemID then
return makeBagItem(bag, slot, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
-- Name match: extract name from link (faster than GetItemInfo)
if qname then
local nm = GetNameFromLink(link)
if nm and string_lower(nm) == qname then
return makeBagItem(bag, slot, link, Items)
end
end
end
end
end
@@ -818,25 +812,19 @@ end
function CleveRoids.IsItemEquipped(text, inventoryId)
if not text or not inventoryId then return false end
local link = GetInventoryItemLink("player", inventoryId)
if not link then return false end
local _, _, currentID = string_find(link, "item:(%d+)")
local currentID = GetInventoryItemID("player", inventoryId)
if not currentID then return false end
-- Check by ID (fast path)
local textId = tonumber(text)
if textId and textId == tonumber(currentID) then
if textId and textId == currentID then
return true
end
-- Check by name - extract from link instead of GetItemInfo for performance
local currentName = GetNameFromLink(link)
if currentName then
local textLower = string_lower(text)
if string_lower(currentName) == textLower then
return true
end
-- Check by name (decorated, so suffixed gear still matches)
local currentName = C_Item.GetItemName({ equipmentSlotIndex = inventoryId })
if currentName and string_lower(currentName) == string_lower(text) then
return true
end
return false
+28 -6
View File
@@ -24,7 +24,22 @@ CleveRoids.hasSuperwow = SetAutoloot and true or false
CleveRoids.hasTurtle = (type(_G.TURTLE_WOW_VERSION) ~= "nil")
CleveRoids.supported = CleveRoids.hasTurtle
-- ClassicAPI macro display (C_Macro.SetMacroDisplay): hand ClassicAPI the resolved
-- action per macro instead of replacing the action-bar globals in Lua, so the icon,
-- tooltip, cooldown sweep, range and usable state all come from the client -- including
-- the drag cursor and the macro window grid, which Lua cannot reach.
--
-- Feature-detect rather than version-check: the API is unreleased, so
-- CLASSIC_API_VERSION reports the dev sentinel. ClassicAPI stands down from macro
-- display entirely when it sees this addon loaded; ClassicAPIMacroDisplay is what
-- tells it we drive it instead. A fork that leaves the flag unset keeps the old
-- behavior -- both must never drive the same buttons.
CleveRoids.useClassicAPIDisplay =
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
CleveRoids.ClassicAPIMacroDisplay = CleveRoids.useClassicAPIDisplay
CleveRoids.ParsedMsg = {}
CleveRoids.ExpandedGroups = {}
CleveRoids.Items = {}
CleveRoids.Spells = {}
CleveRoids.PetSpells = {}
@@ -43,13 +58,11 @@ CleveRoids.unknownTexture = "Interface\\Icons\\INV_Misc_QuestionMark"
CleveRoids.spell_tracking = {}
-- GUID-based cast tracking (populated by pfUI 7.6 or standalone SPELL_START events)
-- Format: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
-- GUID-based cast tracking, populated from our own SPELL_START handlers and pruned
-- in OnUpdate. Format: [casterGuid] = {spellID, spellName, icon, startTime, duration,
-- endTime}
CleveRoids.castTracking = {}
-- pfUI 7.6+ with Nampower 2.31.0+ detected (GUID-based cast tracking available)
CleveRoids.hasPfUI76 = false
-- Combo point tracking (initialized early for /cast hook)
CleveRoids.lastComboPoints = 0
CleveRoids.lastComboPointsTime = 0
@@ -124,6 +137,7 @@ CleveRoids.dynamicCmds = {
["/castpet"] = true,
["/castsequence"] = true,
["/use"] = true,
["/feedpet"] = true,
["/equip"] = true,
["/equipmh"] = true,
["/equipoh"] = true,
@@ -186,11 +200,19 @@ CleveRoids.ignoreKeywords = {
_operators = true, -- Metadata for AND/OR operator tracking
_groups = true, -- Grouped conditional values for AND/OR evaluation
multiscan = true, -- Processed before Keywords loop (target resolution)
mouseuse = true, -- Post-cast modifier: auto-click AOE targeting circle at cursor
cursor = true, -- Modifier: place ground-target spell/item at cursor (CastAtCursor)
stopattack = true, -- Post-cast modifier: stop autoattack after cast (CheapShot pattern)
}
-- Deprecated conditional names, rewritten to their current keyword in ParseMsg
-- before anything downstream (evaluation, _groups) sees them. This is the path
-- for renamed *modifiers* -- ignoreKeywords entries, which have no Keywords
-- predicate that an alias could just point at the way [stl] does for [stealth].
-- MacroErrorChecker also reads this table, so the old names stay valid syntax.
CleveRoids.conditionalAliases = {
mouseuse = "cursor", -- pre-ClassicAPI name, back when it clicked the AoE reticle
}
-- TODO: Localize?
CleveRoids.countedItemTypes = {
["Consumable"] = true,
+41 -16
View File
@@ -35,6 +35,14 @@ if CleveRoids.ignoreKeywords then
end
end
-- Deprecated names still valid in macros; ParseMsg rewrites them to the current
-- keyword, so they never reach Keywords/ignoreKeywords under their old name.
if CleveRoids.conditionalAliases then
for alias, _ in pairs(CleveRoids.conditionalAliases) do
VALID_CONDITIONALS[alias] = true
end
end
-- Known valid commands
local VALID_COMMANDS = {
-- Core commands NOT registered via SlashCmdList (so not auto-discoverable):
@@ -498,6 +506,7 @@ local COMMANDS_NO_ACTION_NEEDED = {
["/startattack"] = true,
["/stopattack"] = true,
["/stopcasting"] = true,
["/stopchanneling"] = true,
["/unqueue"] = true,
["/retarget"] = true,
["/stopmacro"] = true,
@@ -552,7 +561,6 @@ end
local ERROR_TYPES = {
INVALID_CONDITIONAL = "Invalid conditional",
MISMATCHED_BRACKETS = "Mismatched brackets",
EMPTY_CONDITIONAL = "Empty conditional block",
INVALID_OPERATOR = "Invalid operator",
MISSING_ARGUMENT = "Missing argument",
INVALID_COMMAND = "Unknown command",
@@ -651,6 +659,8 @@ local function validateConditional(conditional, args, action)
combo = true,
hp = true, myhp = true, rawhp = true, myrawhp = true,
power = true, mypower = true, rawpower = true, myrawpower = true,
mana = true, mymana = true, rage = true, myrage = true,
energy = true, myenergy = true,
hplost = true, myhplost = true,
powerlost = true, mypowerlost = true,
stat = true,
@@ -659,6 +669,7 @@ local function validateConditional(conditional, args, action)
button = true,
form = true, stance = true,
level = true, mylevel = true,
myspellhaste = true,
distance = true, nodistance = true,
swingtimer = true, stimer = true,
rangedtimer = true, rtimer = true,
@@ -678,6 +689,8 @@ local function validateConditional(conditional, args, action)
-- Check operator syntax for numeric comparisons
if args and type(args) == "string" then
local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or
safeStringFind(baseCond, "mana") or safeStringFind(baseCond, "energy") or
safeStringFind(baseCond, "rage") or safeStringFind(baseCond, "haste") or
safeStringFind(baseCond, "combo") or baseCond == "stat"
if hasHpOrPower then
local hasOperator = safeStringFind(args, "[<>=~]+")
@@ -835,16 +848,19 @@ local function validateLine(line, lineNum)
actionPart = "/" .. actionPart -- Add leading slash if missing after split
end
-- Parse conditionals if present - use non-greedy match
local condStart = safeStringFind(actionPart, "%[")
-- Parse the leading run of [conditional] blocks, if any. `[a][b] X` is
-- Blizzard-style OR chaining: every block is validated, and the action
-- is what follows the last one.
local blocks = {}
local condEnd = nil
local conditionBlock = nil
local condStart = safeStringFind(actionPart, "%[")
local len = safeStringLen(actionPart)
if condStart then
while condStart do
-- Find matching closing bracket
local depth = 0
local inQuotes = false
local len = safeStringLen(actionPart)
local closePos = nil
for i = condStart, len do
local char = safeStringSub(actionPart, i, i)
@@ -858,23 +874,30 @@ local function validateLine(line, lineNum)
elseif char == "]" then
depth = depth - 1
if depth == 0 then
condEnd = i
conditionBlock = safeStringSub(actionPart, condStart + 1, i - 1)
closePos = i
break
end
end
end
end
if not closePos then break end
table.insert(blocks, safeStringSub(actionPart, condStart + 1, closePos - 1))
condEnd = closePos
-- Another block directly after this one (whitespace allowed)?
local _, wsEnd = safeStringFind(actionPart, "^%s*", closePos + 1)
local nextPos = (wsEnd or closePos) + 1
if safeStringSub(actionPart, nextPos, nextPos) == "[" then
condStart = nextPos
else
condStart = nil
end
end
if conditionBlock then
if safeTrim(conditionBlock) == "" then
table.insert(localErrors, {
type = ERROR_TYPES.EMPTY_CONDITIONAL,
line = lineNum,
message = "Empty conditional block []"
})
else
for _, conditionBlock in ipairs(blocks) do
-- `[]` is the always-true group; nothing to validate
if safeTrim(conditionBlock) ~= "" then
-- Check for invalid @ target syntax
local _, _, target = safeStringFind(conditionBlock, "(@[^%s,]+)")
if target and not safeStringFind(target, "^@[a-z]+%d*") then
@@ -922,7 +945,9 @@ local function validateLine(line, lineNum)
end
end
end
end
if condEnd then
-- Check for action after conditionals
-- Extract the command from this action part
local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+%d*)")
+7 -46
View File
@@ -71,7 +71,6 @@
Spell Miss Events (v2.31+):
- SPELL_MISS_SELF / SPELL_MISS_OTHER - Spell miss/resist/immune/dodge/etc.
- GetSpellPower([mode]) - Player mod damage done for all 7 schools
Aura Event State Parameter (v2.32+):
- Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified)
@@ -364,9 +363,8 @@ API.VERSION_REQUIREMENTS = {
["AuraDurationEvents"] = { 2, 30, 0 },
["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" },
-- v2.31+ - Spell miss events and spell power query
-- v2.31+ - Spell miss events
["SpellMissEvents"] = { 2, 31, 0 }, -- SPELL_MISS_SELF/OTHER events
["GetSpellPower"] = { 2, 31, 0, "GetSpellPower" },
-- v2.32+ - Aura event state parameter and stack removal fix
["AuraEventState"] = { 2, 32, 0 },
@@ -575,9 +573,8 @@ local function InitializeFeatures()
f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents")
f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration")
-- v2.31+ Spell miss events and spell power
-- v2.31+ Spell miss events
f.hasSpellMissEvents = API.HasFeature("SpellMissEvents")
f.hasGetSpellPower = API.HasFeature("GetSpellPower")
-- v2.32+ Aura event state parameter
f.hasAuraEventState = API.HasFeature("AuraEventState")
@@ -1477,7 +1474,7 @@ end
-- Get all equipped items for a unit
-- Requires v2.18+ for native GetEquippedItems (with Lua fallback for player only)
-- Returns table with slot indices (0-18) as keys, item info tables as values
-- Returns table with slot indices (1-19) as keys, item info tables as values
function API.GetEquippedItems(unitToken)
unitToken = unitToken or "player"
@@ -1492,8 +1489,8 @@ function API.GetEquippedItems(unitToken)
end
local items = {}
for slot = 0, 18 do
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot + 1)
for slot = 1, 19 do
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot)
if itemId then
items[slot] = {
itemId = itemId,
@@ -1521,7 +1518,7 @@ function API.GetEquippedItem(unitToken, slot)
return nil
end
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot + 1) -- 1-indexed
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot) -- both are 1-indexed
if itemId then
return {
itemId = itemId,
@@ -1675,7 +1672,7 @@ function API.IsItemInSlot(itemIdOrName, inventorySlot)
-- Use native v2.18 GetEquippedItem if available
if GetEquippedItem then
local slotInfo = GetEquippedItem("player", inventorySlot - 1) -- 0-indexed
local slotInfo = GetEquippedItem("player", inventorySlot) -- 1-indexed (1-19)
if slotInfo and slotInfo.itemId then
local checkId = tonumber(itemIdOrName)
if checkId then
@@ -3387,15 +3384,6 @@ API.MISS_INFO = {
-- SPELL POWER QUERY (v2.31+)
--------------------------------------------------------------------------------
-- Get spell power for all 7 damage schools (v2.31+)
-- mode: optional mode parameter passed to GetSpellPower
-- Returns: physical, holy, fire, nature, frost, shadow, arcane (or nil if unavailable)
function API.GetSpellPower(mode)
if not API.features.hasGetSpellPower or not _G.GetSpellPower then
return nil, nil, nil, nil, nil, nil, nil
end
return _G.GetSpellPower(mode)
end
-- Get duration of a spell in milliseconds (v2.38+)
-- For channeling spells: returns the channel duration.
@@ -3504,32 +3492,5 @@ function API.GetUnitMaxHealth(unitToken)
return UnitHealthMax(unitToken)
end
-- powerType: nil=current, 0=mana, 1=rage, 2=focus, 3=energy
-- GetUnitField uses power1-power4 fields
local POWER_FIELDS = { [0] = "power1", [1] = "power2", [2] = "power3", [3] = "power4" }
local MAX_POWER_FIELDS = { [0] = "maxPower1", [1] = "maxPower2", [2] = "maxPower3", [3] = "maxPower4" }
function API.GetUnitPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitMana(unitToken)
end
function API.GetUnitMaxPower(unitToken, powerType)
if API.features.hasGetUnitField and GetUnitField and powerType then
local field = MAX_POWER_FIELDS[powerType]
if field then
local val = GetUnitField(unitToken, field)
if val then return val end
end
end
return UnitManaMax(unitToken)
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+2 -1
View File
@@ -27,6 +27,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
- **Arguments** use colon: `[mod:alt]`, `[hp:>50]`
- **Negation** with `no` prefix: `[nobuff]`, `[nomod:alt]`
- **Target** with `@`: `[@mouseover,help]`, `[@party1,hp:<50]`
- **Fallback groups**, Blizzard-style: `[@mouseover,help][@focus,help][] Rejuvenation` tries each `[...]` in order and the first that passes casts the shared spell; `[]` always passes. Mixes freely with `;`
- **Spell names** with spaces: `"Mark of the Wild"` or `Mark_of_the_Wild`
**Multi-value logic:**
@@ -46,7 +47,7 @@ See the **[Wiki](https://github.com/brues-code/SuperCleveRoidMacros/wiki)** for
- **[Quick Start](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Quick-Start)** — Syntax, multi-value logic, comparisons, special prefixes
- **[Slash Commands](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Slash-Commands)** — All 35+ commands, priority macros, UnitXP scanning
- **[Conditionals](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Conditionals)** — 70+ conditionals (player & target), extended unit tokens, multiscan
- **[Conditionals](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Conditionals)** — 150+ conditionals (player & target), short aliases, extended unit tokens, multiscan
- **[Reference Tables](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Reference-Tables)** — CC types, damage schools, stat types, swing types
- **[Features](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Features)** — Debuff timers, combo tracking, talent modifiers, TWoW mechanics
- **[Overflow Buff Frame](https://github.com/brues-code/SuperCleveRoidMacros/wiki/Overflow-Buff-Frame)** — Hidden buff display for 32+ buffs
+1
View File
@@ -5,6 +5,7 @@
## Version: @project-version@
## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar, Cursive, XPerl, LunaUnitFrames, DragonflightReloaded, -Dragonflight3
## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools
## LoadSavedVariablesFirst: 1
Localization.lua
Init.lua
NampowerAPI.lua
+301 -563
View File
File diff suppressed because it is too large Load Diff
+66 -19
View File
@@ -25,10 +25,23 @@ API references are line numbers into `C:\Git\ClassicAPI\docs\API.md`.
`applications` (stacks), `duration`.
- **Unlocks:** `[dispellable]` / `[curse]` / `[magic]` target conditionals and
spellID-based (rank/locale-proof) aura matching.
- **Caveat:** `expirationTime` is only populated for `unit=="player"`; it's `0`
for target/focus (vanilla server limitation). Target debuff *timers* still
need the existing libdebuff tracking — only presence/stacks/school/spellId
are reliable cross-unit.
- **~~Caveat: `expirationTime` is player-only~~ — NO LONGER TRUE.** This note said
target/focus `expirationTime` was always `0`, so debuff *timers* had to stay on
libdebuff. ClassicAPI has since added the `Aura::Source` cache: for a non-player
unit `expirationTime` is reconstructed from the observed `SMSG_SPELL_GO`, and
`duration` is the **caster-modified** value (talent extensions like Improved
Shadow Word: Pain included). It also handles combo-point finisher scaling
automatically (Rupture at 4 CP reads 14s, no registration) and ships Carnage's
roll-gated Rip/Rake refresh in the DLL (`src/turtle/Carnage.cpp`, exposed as
`RegisterAuraDurationModifierByTrigger`).
- **Real remaining caveats** (all best-effort, from `docs/API.md`):
- Only auras observed *after login* carry caster/timing; older ones report
`expirationTime` 0 and `sourceUnit` nil.
- Max-stack refresh is a blind spot: re-applying at max stacks (Shadow Weaving
5→5) emits no client-visible change, so the entry elapses and evicts.
- Out-of-range group members are spell-ID only, with `applications` always 1.
- **Consequence:** most of libdebuff is now redundant — see
"libdebuff retirement" below.
- **DONE (slice 1 — dispel-type conditionals):** added `ClassicAPI.lua`
detection module (`CleveRoids.ClassicAPI`, mirrors NampowerAPI's
@@ -49,16 +62,15 @@ API references are line numbers into `C:\Git\ClassicAPI\docs\API.md`.
- class-aware `[dispellable]` (only types this character can actually remove).
- optional name filtering on the type keywords (e.g. `[magic:Polymorph]`).
### 2. `C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs
- **API:** `API.md:7159` — returns 12-tuple including `enchantID` for main/off/ranged.
- **Unlocks:** detect *which* temp enchant (poison/oil/sharpening stone) is
applied to a weapon, not just that one exists.
- **Naming:** `[poison]` is now taken by the target dispel-type conditional
(slice 1 above). Use weapon-specific keywords for this — e.g.
`[mhenchant:<id>]` / `[ohenchant:<id>]` (or `[mhpoison:<id>]`/`[ohpoison:<id>]`),
not a bare `[poison]`.
- Vanilla's global only reports presence; this is a genuinely new capability
(rogue/shaman/enhance).
### 2. ~~`C_Item.GetWeaponEnchantInfo()` — temp-enchant IDs~~ — DONE
- Shipped as `[mhenchant]` / `[ohenchant]` (+ `no` variants). Matches the applied
temp enchant by SpellItemEnchantment ID *or* localized name — the name path
resolves via `C_Item.GetEnchantInfo(id).name` (the ID→name table this doc
assumed we lacked), so no tooltip scan. Wrappers `ClassicAPI.GetWeaponEnchant`
/ `GetEnchantName`; `ValidateWeaponImbue` now reads through the former.
- Bare = any temp enchant; OR-lists supported (`[mhenchant:2823/Deadly_Poison]`).
- Follow-up (optional): route `[mhimbue:Name]`'s match through `GetEnchantName`
too, retiring the green-text tooltip scan in `CheckWeaponImbueByName`.
### 3. `GetUnitSpeed(unit)` + `IsFalling()` / `IsSwimming()`
- **API:** `API.md:8312`, `API.md:7629`.
@@ -151,11 +163,13 @@ Findings from the full Conditionals.lua audit. These look like ClassicAPI
candidates but are **worse** than the current implementation — recorded so we
don't re-investigate.
- **Auras / `ValidateAura`** — `C_UnitAuras.expirationTime` is **player-only**
(`0` for every other unit). It cannot replace the nampower `GetUnitField`
batch read or the remote-duration tracking (libdebuff / overflow slots). The
dispel-*type* path already uses `C_UnitAuras` (that's the one thing it's good
for); aura *timing/stacks* on non-player units must stay on nampower/libdebuff.
- ~~**Auras / `ValidateAura`** — `C_UnitAuras.expirationTime` is player-only~~ —
**STALE, do not trust this entry.** It was written before the `Aura::Source`
cache landed. Non-player `expirationTime`/`duration` now work (caster-modified,
talent extensions included), so this is no longer a reason to keep libdebuff's
remote-duration tracking. See Tier 1 §1 and "libdebuff retirement" for the
current picture and the real caveats. The nampower `GetUnitField` batch read is
a separate question and has not been re-examined.
- **`GetCurrentShapeshiftIndex` form loop / `[stance]`·`[form]`** —
`GetShapeshiftFormID()` returns the **DBC form id** (Cat=1, Bear=5,
Shadowform=28…), NOT the 1-based **bar index** these conditionals compare
@@ -184,6 +198,39 @@ don't re-investigate.
added `[focus]`/`[nofocus]`. `/focus` is provided by ClassicAPI's companion
addon.
## libdebuff retirement
libdebuff exists because vanilla cannot report debuff durations on units other
than the player. ClassicAPI's `Aura::Source` cache now does exactly that, so most
of the library is redundant. This is a staged replacement, not a delete: measure
first, then remove per group.
Current footprint: 28 public `lib:` methods, ~726 internal references in
`Utility.lua`, consumers in 8 files (`Conditionals.lua` 45, `Compatibility/pfUI.lua`
39, `Core.lua` 9). Note pfUI did **not** delete its own libdebuff — v9.0.25 still
ships ~1695 lines of it, re-based on `C_UnitAuras.GetAuraDataByIndex` /
`GetAuraDataBySpellName`. "Re-base on C_UnitAuras", not "remove", is the precedent.
**Group A — replaceable by `C_UnitAuras` (do these first):**
`GetDuration`, `GetDebuffCaster`, `IsOurDebuff`, `UnitBuff`/`UnitDebuff`,
`FindPlayerDebuff`/`FindPlayerBuff`, `GetAllDebuffsOnTarget`, `GetCachedIcon`,
`ApplyCarnageRefresh`, and the Dark Harvest trio (`ApplyDarkHarvestStart`/`End`,
`GetDarkHarvestReduction`, `GetTimeRemainingWithDarkHarvest`) — ClassicAPI ships
Carnage refresh and Dark Harvest tick compression in the DLL.
**Group B — no `C_UnitAuras` equivalent, keep:**
`ShouldApplyDebuffRank`, `DidSpellFail`, `WasSpellReflected`, `DidTargetEvade`,
`ProcessMissReason`, `IsPersonalDebuff`, `GetSpellRank`/`GetSpellBaseName`,
`HasPendingCast`. These are miss/rank/learning logic, not aura state.
**Gate before removing Group A:** confirm parity in-game on one spell where
ClassicAPI does the hard part — Rip under Carnage. Compare
`CleveRoids.libdebuff:GetDuration(spellID)` against
`C_UnitAuras.GetUnitAuraBySpellID(unit, spellID).duration` and the derived
remaining (`expirationTime - GetTime()`), on a target you have debuffed. If those
agree across a Carnage proc, Group A can go. Watch the documented best-effort
gaps: an aura cast before you logged in, and refresh-at-max-stacks.
## Marginal / optional follow-ups
- **`[swimming]`** → `IsSwimming()` would drop the nampower-2.36 version gate +