100 Commits

Author SHA1 Message Date
Brues 37ca7cef40 Document the CC, immunity and DR design
Records why the three identities are separate, the DBC mechanic table each
[cc:type] now follows, and which DR pools the immunity safeguard may act on.

Every vMaNGOS and nampower claim behind the preceding four commits is cited
to the source line that establishes it, so the next person to touch this can
check the premises instead of re-deriving them: the DRTYPE_ALL set is three
stun pools and nothing else, Charge and Intercept are the only triggered
stuns forced into the controlled pool, and SPELL_MISS puts the spell ID in
arg3.

Also notes the mouseover learning gap as designed behavior rather than a bug,
since the failure on the other side is permanent bad SavedVariables data.
2026-09-15 08:10:12 -05:00
Brues 4474a607f7 Stop DR suppressing NPC immunity learning past its window
The DR safeguard keyed recentCCHits by ccType and never reset the counter,
so three stuns on a target left it at count >= 3 forever. Every later
IMMUNE on that target was read as diminishing returns and no permanent
immunity was ever learned from it again.

Count within a window instead: a hit more than DR_RESET_WINDOW seconds
after the last one starts over at 1, matching the 20s DR decay the skip
check already used. Both the hit path and the skip check now key on the DR
pool from GetSpellImmunityDRType rather than the mechanic, so only the three
groups that actually diminish against creatures can hold learning back.

Learning itself records immunityType, the exact mechanic, so a target that
resists Gouge is no longer written down as stun-immune.

CheckImmunity also resolves a spell name through GetSpellImmunityType now,
so bare [immune]/[noimmune] answers for mechanic immunity and not just
schools.
2026-09-15 08:10:12 -05:00
Brues ca6c55a5e7 Give every [cc:type] its own exact DBC mechanic
stun matched mechanics 12, 14 and 30 at once through CCMechanicGroups, so
[cc:stun] fired on a gouged or sapped target and there was no way to ask
about any of the three on its own.

Name each mechanic instead: knockout (14) and sap (30) join the table, with
fumble (6), turn (23) and interrupt (26) filling the remaining gaps, and
grip stays as an alias of fumble so existing macros keep working. The group
indirection had no entries left afterwards, so it and its lookup branch are
gone; aliases in CCMechanics already cover every name that shares a mechanic.

CCTypesLossOfControl is untouched, so bare [cc] and [cc:any] still span the
whole loss-of-control set.
2026-09-15 08:10:12 -05:00
Brues c1a7bdb51b Fix the Nampower SPELL_MISS argument order
TriggerSpellMissEvent signals casterGuid, targetGuid, spellId, missInfo for
both SPELL_MISS_SELF and SPELL_MISS_OTHER. Both handlers read arg1 as the
spell ID, so every miss dispatched a GUID string where an ID was expected
and no miss ever reached the immunity recorder through the Nampower path.

Name mechanic 14 knockout to match the 1.12 DBC, and normalise CC type input
through one place so incap and incapacitate stay accepted as aliases for it.
2026-09-15 08:10:12 -05:00
Brues 5d5f987555 Learn NPC CC immunity per exact DBC mechanic
Learned immunity shared one coarse bucket with the [cc:*] conditionals, so
mechanic 13 (freeze), 14 (knockout) and 30 (sap) all collapsed into "stun".
An NPC immune to Gouge was then recorded as stun-immune, and every later
Kidney Shot was suppressed against a target that had never resisted one.

Split the two identities. MECHANIC_TO_IMMUNITY_TYPE keeps the mechanic
one-to-one for learning, and GetSpellImmunityType resolves it from the DBC
spell/effect mechanic fields, falling back to the aura type only where a
spell leaves those empty.

The DR safeguard needs the real Vanilla DR pool rather than the mechanic:
GetSpellImmunityDRType separates controlled stun, triggered stun and Kidney
Shot, since vMaNGOS marks only those three DRTYPE_ALL against creatures.
The remaining staged groups are player-only and must not hold back learning.
2026-09-15 08:03:43 -05:00
Brues eddc49fc60 Make [button:N] mean the invoking button, as retail does
[button:N] was a held-state modifier: IsMouseButtonDown(name), true while you
physically hold that button. Retail's is a property of the activation -- which
button clicked the action button -- and a keybind press counts as button 1
because it activates through the left-click path. Different in kind, and the
visible break was [button:1] Rejuvenation; Regrowth casting Regrowth from a
keybind, where retail casts Rejuvenation.

It now reads ClassicAPI's GetMouseButtonClicked, defaulting to LeftButton when
no click dispatch is running. That default IS the keybind rule: Button:Click()
with no argument reports LeftButton, and a macro run from anywhere else answers
button 1 the same way.

This also reverts 891467c. That commit refreshed the action bar on
GLOBAL_MOUSE_DOWN/UP so the icon could track the held state -- under the real
semantics there is no such state to track, and the icon correctly sits on
whatever button 1 resolves to. PLAYER_TOTEM_UPDATE stays; it shared the comment
and inherits it.

Requires ClassicAPI v1.15.8, up from v1.15.0. v1.15.8 scoped
GetMouseButtonClicked to the click dispatch; before it a HELD button kept the
value alive (by design -- OnDragStart reads it), so turning the camera with
right-click down made every keybind press read RightButton. The function exists
below v1.15.8 and answers wrongly, and a conditional that is quietly wrong is
worse than one that warns, so the floor moved rather than the conditional being
gated.

Bare [button] / [nobutton] survive as an extension retail has no equivalent for,
now meaning "a click dispatch is running" -- the practical "activated by a
keybind, not a click" test, and no longer leaning on anything but the documented
nil.
2026-09-15 01:17:06 -05:00
Brues 54e3c3b3f4 Drop the dead C_LossOfControl gate
The LOSS_OF_CONTROL_ADDED/UPDATE registration was wrapped in a
`type(C_LossOfControl) == "table"` check, to keep an older ClassicAPI without
the namespace from erroring on an unknown event. That can't happen:
C_LossOfControl ships in v1.10.0, and the addon refuses to finish loading below
v1.15.0. Same shape as the health/power fallbacks removed just before this.

GetSchoolLockout's comment claimed it "Returns 0 for a client without
C_LossOfControl", which was not merely obsolete but wrong -- the function
indexes C_LossOfControl unguarded on its first line, so a missing namespace
raises rather than returning 0. The sentence describes a guard that was never
there.

Left alone: the C_Macro.SetMacroDisplay feature-detect in Init.lua looks like
the same pattern but isn't. SetMacroDisplay lands in v1.15.0, exactly the floor,
so a nil there means the client mod is absent outright, and the flag doubles as
the internal "may we publish" guard that ReleaseDisplays clears.
2026-09-14 20:15:42 -05:00
Brues 4fa254b4e6 Drop the unreachable fallbacks in the health/power wrappers
UnitHealthMissing, UnitPower, UnitPowerMax and UnitPowerMissing each carried an
`X or function(...)` fallback computing the value from UnitHealth/UnitMana. None
could run: the addon refuses to finish loading below ClassicAPI v1.15.0, and all
four globals ship at or below v1.10.0. The file header already promised as much
-- "the wrappers below call the API directly -- no fallbacks" -- and these four
were the only ones that didn't.

The UnitPower fallback was worse than dead. It dropped powerType and returned
UnitMana(unit), so a rage or energy read would have quietly answered with mana;
UnitPowerMax did the same with UnitManaMax. Only unreachability kept that from
being a bug.

They were also the file's only plain `API.X =` assignments, bound to the global
at load time. Now all 38 wrappers are `function API.X(...)`, resolving per call
like their neighbours -- no practical difference, since the DLL registers these
globals before any addon Lua runs.

Every caller passes (unit) or (unit, powerType); both pass straight through.
2026-09-14 19:47:46 -05:00
Brues b82d06d061 Add [totem] and [nototem] conditionals
A totem is a summoned creature, not an aura, so nothing in the addon could see
one. Strength of Earth and Mana Spring could be faked through [nomybuff], but
Searing, Magma, Fire Nova, Tremor and Grounding put nothing on the player at
all, and those are the ones worth conditioning a recast on. There was no
workaround.

ClassicAPI backports the TBC totem bar (src/totem/Tracker.cpp), tracking the
player's own summons from their SMSG_SPELL_GO and classifying them by the
summon spell's SUMMON_TOTEM_SLOT1..4 effect, so Turtle's custom totems
self-classify. Ungated like GLOBAL_MOUSE_*: the tracker landed in v1.10.0, far
below the v1.15.0 floor the addon already refuses to run below.

[totem:X] takes an element (fire/earth/water/air), a slot number, or the totem's
own name -- ResolveTotemSlot tries the slot table first, then matches X against
the name standing in each slot, so [totem:Searing_Totem] asks for that totem
rather than whatever holds the fire slot. Bare [totem] takes the action as its
argument the way [mybuff] does, making /cast [nototem] Searing Totem a complete
recast macro.

Comparisons follow the aura precedent exactly: an empty slot reads -1, the same
"missing counts as least" ValidateAura uses, so [totem:Searing_Totem<5] passes
while the totem is expiring AND while it is absent -- one clause, no [nototem]
needed. A totem is either up or it isn't, so #N stack comparisons read 1 for a
standing totem. The parser needed nothing: its name/operator/amount capture is
generic, so multi-comparisons ([totem:X>2&<8]), OR groups and the De Morgan
flip all came for free.

Occupancy is read from GetTotemInfo's totemName, never from the time left.
GetTotemInfo's first return is TOOL presence -- whether the player carries the
slot's Earth/Fire/Water/Air Totem item -- not whether a totem is out, and a
summon spell with no SpellDuration row reports 0 seconds, which is "up with no
timer", not "absent".

Icon refresh needs two feeds. PLAYER_TOTEM_UPDATE covers a slot changing hands
(dropped, expired, killed, recalled). It does not fire as a totem counts toward
zero, and unlike an aura there is no ambient event stream to ride -- UNIT_AURA
is what quietly keeps [mybuff:X<N] honest -- so a [totem:X<N] icon held its last
answer until some unrelated event queued a refresh: the macro sat on the
question mark through the window it should have lit up. Hence the once-a-second
re-test in OnUpdate while any totem timer runs, skipped in realtime mode, which
re-tests everything anyway. Only the icon was ever at stake; the click path
evaluates conditionals live.
2026-09-14 19:28:24 -05:00
Brues 891467cd7e Refresh action bars on ClassicAPI's global mouse events
A [button:N] macro's icon never moved. The conditional reads IsMouseButtonDown
live, but nothing ever noticed that state changing: the OnUpdate's input poll
samples alt/shift/ctrl only, and no event fired for a mouse button, so no
refresh was queued and the icon sat on whatever the macro resolved to with no
button held.

ClassicAPI fires GLOBAL_MOUSE_DOWN / GLOBAL_MOUSE_UP on every raw press and
release, whether or not the click lands on a frame. Registering both and queuing
an action update is the whole fix -- the existing path re-runs TestAction per
action, so the conditional is read fresh and PublishDisplay hands the new answer
to C_Macro.SetMacroDisplay.

Ungated, unlike the Nampower-backed input events next to it: GLOBAL_MOUSE_* has
been in ClassicAPI since v1.0.0, far below the v1.15.0 floor the addon already
refuses to run below.

The handlers set isActionUpdateQueued directly rather than calling
QueueActionUpdate, as KEY_DOWN does: these fire from the message pump, which can
beat VARIABLES_LOADED to the CleveRoidMacros table QueueActionUpdate reads.

arg1 carries the button name and goes deliberately unused. The mask behind
IsMouseButtonDown is maintained by the same hook that fires the event, so
mirroring the payload into Lua would copy state the API already exposes -- and
the refresh covers every macro regardless, since any of them may test any
button.
2026-09-14 15:31:43 -05:00
Brues 87ea8bdd41 Delete the Lua paladin judgement refresh
A paladin's white swing refreshes their own judgement debuff on the victim, and
the server's edit is packet-silent, so pfUI#45 was the visible symptom: the
timer ran to zero while the debuff stayed up. This addon worked around it three
ways at once -- a UNIT_CASTEVENT MAINHAND/OFFHAND handler in Core.lua, an
AUTO_ATTACK_OTHER fallback in Conditionals.lua, and a CHAT_MSG_COMBAT_SELF_HITS
parser in Utility.lua that string-matched "hit"/"crit" and guessed at melee vs
spell by looking for a parenthesis -- each rewriting rec.start and pushing the
result into pfUI by hand.

ClassicAPI mirrors the server rule in the DLL (src/aura/JudgementRefresh.cpp),
keyed off SMSG_ATTACKERSTATEUPDATE, which is the visible trigger for the
invisible edit. It matches judgements by their DBC marker -- SPELLFAMILY_PALADIN
with SPELL_ATTR_EX3_ALWAYS_HIT -- for any paladin's judgements, not only ours.

That last detail retires the auto-detection too. The hardcoded judgementSpells
list could not cover Turtle's custom judgement ranks, so a cast queued a pending
scan that read the target's debuffs 0.5s later, matched names against
"^Judgement", and grew the list at runtime. Matching on the DBC attribute needs
no list to grow. Gone with it: judgementSpells, pendingJudgements,
detectedJudgementDebuffIDs, the pending-scan pass and its queue buffer, and both
cast-site hooks.

The scan also registered each ID it discovered into lib.sharedDebuffs so
[debuff] fallback matching worked for custom IDs. That mattered when detection
went through the tracking store; the read path resolves auras through
C_UnitAuras now, which returns a debuff whether or not we have a row for it.
2026-09-12 20:33:43 -05:00
Brues 53a1cf1441 Delete the Lua Dark Harvest acceleration
Same shape as Carnage, opposite direction: Dark Harvest is a channel that makes
the caster's DoTs on the target tick 30% faster, and the server never tells an
observer the remaining durations changed. So this addon stamped dhStartTime on
every tracked record when the channel began, dhEndTime when it stopped, and
subtracted 30% of the elapsed window from each remaining time on read.

ClassicAPI compresses the ticks in the DLL (src/turtle/DarkHarvest.cpp), so
expirationTime already reflects it.

Removed: GetDarkHarvestReduction, ApplyDarkHarvestStart, ApplyDarkHarvestEnd,
GetTimeRemainingWithDarkHarvest, the channel-start capture on both the
UNIT_CASTEVENT and nampower paths, the channel-stop finalizer in
SPELLCAST_CHANNEL_STOP, the darkHarvestData state, and DarkHarvestSpellIDs.

The darkHarvest flag in Extensions/CursiveCustomSpells.lua stays -- that is
spell metadata this addon exports to Cursive, not tracking of our own.
2026-09-12 20:27:16 -05:00
Brues 073621e233 Delete the Lua Carnage refresh
Carnage refreshes the caster's Rip and Rake when Ferocious Bite procs it, and
1.12 tells an observing caster nothing about it -- no packet carries a debuff's
new remaining duration on another unit. So this addon inferred the proc: track
every Ferocious Bite cast with its target, watch PLAYER_COMBO_POINTS for combo
points failing to drop to 0 within 0.5s, then rewrite the stored Rip and Rake
records and push the new durations into pfUI's tables by hand. Roughly 570 lines
across three files, and it could only ever be a guess, because the roll that
decides whether Carnage procs is server-side.

ClassicAPI does it in the DLL (src/turtle/Carnage.cpp), roll-gated, so
expirationTime is simply correct and there is nothing to infer.

Removed with it: ApplyCarnageRefresh, the Ferocious Bite cast tracking on both
the UNIT_CASTEVENT and nampower paths, the avoided-Bite clearing in the miss
handler, the Rip/Rake cast snapshots on the UNIT_CASTEVENT and SPELL_GO paths,
the Rake landed/failed verification hooks, the proc detector in
ComboPointTracker, and lastRipCast / lastRakeCast / RipSpellIDs / RakeSpellIDs,
which had no readers left afterwards.

carnageDurationOverrides goes too, and it never worked: every reference to it
was a write, a clear, or the 30-second sweep in Core.lua's cleanup. Nothing ever
read it to change a duration. FerociousBiteSpellIDs stays -- the combo-point
snapshot still needs to know a finisher when it sees one.
2026-09-12 20:25:12 -05:00
Brues 3e2b3db31f Read auras positionally where we scan
C_UnitAuras.UnitDebuff / UnitBuff return the same fifteen values as
GetAuraDataByIndex without building a table, and the two index readers are
called in a loop over every aura on a unit -- one table per slot per refresh is
exactly the garbage the positional form exists to avoid. Only the first seven
values are wanted (name, icon, count, dispelType, duration, expirationTime,
source), and the range-locked variants keep the HARMFUL/HELPFUL split without a
filter string.

FindPlayerDebuff / FindPlayerBuff stay on GetUnitAuraBySpellID: one lookup
rather than a scan, so the AuraData table costs a single allocation and buys the
PLAYER filter. The -1-means-unknown rule moves into RemainingFrom so both shapes
share it.
2026-09-12 08:20:12 -05:00
Brues 7101ad167a Re-base the libdebuff read path on C_UnitAuras
The four readers that answered "what is on this unit, for how long, cast by
whom" spliced two sources together: the engine's aura array for existence and
icon, lib.objects for duration and caster. Everything the second half supplied
is now in the AuraData the first half already returns -- duration is the
caster-modified value when ClassicAPI saw the cast, expirationTime gives the
true remaining, sourceUnit names the caster -- so they read one source.

That deletes the overflow-slot rule with them. UnitDebuff used to split its
index space at 16 and map 17-48 onto buff slots, because a debuff can be parked
in a buff slot on an NPC, then filter the results through GetDuration to guess
which of those buffs were really debuffs. isHarmful is the aura's real polarity,
so the HARMFUL range already contains them and the guess is gone.

timeleft keeps its -1-means-unknown convention. ClassicAPI reports
expirationTime 0 for an aura whose cast it never observed -- one predating
login, or a max-stack refresh whose cache entry elapsed -- which is the same
"present but untimed" state the store expressed by having no record, so callers
testing `timeleft > 0` see no change.

GetDebuffCaster, IsOurDebuff and GetAllDebuffsOnTarget are deleted outright
rather than re-based: all three have no callers anywhere in the addon, and they
were the only readers of the slotOwnership and allSlots mirrors.
2026-09-12 07:59:28 -05:00
Brues afc5e513ca Finish the C_UnitAuras swap in CheckImmunity
CheckImmunity asks "is this NPC immune to that school, and if the entry is
conditional, does it hold the buff that grants it" in two places. The
plain-school path was migrated to C_UnitAuras.GetAuraDataBySpellName -- one
by-name lookup across the unit's auras. The split-damage path was not, and was
still walking 32 buff slots calling C_Spell.GetSpellName on each to compare the
name back, twice over, once for the initial school and once for the DoT's.

Both copies were the same twenty lines with the variable names changed, so they
fold into a SchoolImmune(unitId, school, targetName) helper that the split path
calls twice. Same answer, same order of checks: a permanent entry is immune, a
table entry without a buff is immune, a table entry with one is immune only
while the aura is up.

The plain-school path keeps its own inline version -- it is not the same
predicate. It gates on the recorded spell name first for the "unknown" school,
and a table entry carrying neither buff nor spell falls through to false there
where the split path calls it immune. Unifying those is a behavior change, not a
refactor, so it is left alone.
2026-09-12 07:48:31 -05:00
Brues 5e27e6fe58 Name the macro slot count
Three loops walked 1..36 by hand, two of them re-arguing in comments why 36
is the right number. It is: the client addresses 18 account-wide macros at
1-18 and 18 character-specific ones at 19-36, and that range is what
GetMacroInfo and C_Macro.SetMacroDisplay index. Nothing in the API hands it
back -- GetNumMacros() returns how many of each tab are used, which cannot be
summed into a range, because the character block still starts at 19 when the
account block is empty. Blizzard's MAX_MACROS is no better: it lives in the
load-on-demand Blizzard_MacroUI and is nil until the macro window is opened.

So CleveRoids.MAX_MACRO_SLOTS in Init.lua, carrying that reasoning once.
ValidateAllMacros also dropped a numAccountMacros it computed and never read.

ReleaseDisplays keeps sweeping the full range rather than the macros it knows
it published: PublishAllDisplays clears that record on each re-parse, so a
macro claimed before one and gone after it would keep our last published value
forever. Releasing a slot we never claimed costs nothing, and it runs once.
2026-09-11 13:57:11 -05:00
Brues 6edd37a76f Require ClassicAPI v1.15.0
An out-of-date ClassicAPI did not degrade the addon, it decapitated it.
Utility.lua calls ev:RegisterUnitEvent at file scope (v1.15.0), so on an
older build that call raises and Lua abandons the rest of the chunk --
every definition below line 4118 of a 10040-line file, some 6000 lines,
silently never happens. What the user sees is the first one a common path
reaches: PLAYER_TARGET_CHANGED calling a nil ClearResistState, once per
target swap, forever.

The requirement check still said v1.12.1 (the positional
C_UnitAuras.UnitAura), so the affected client sailed through it and the
warning it did print promised only that "dispel-type conditionals will be
unavailable" -- describing a graceful degradation that was not happening.
The gate is now v1.15.0 and says the addon cannot finish loading, with the
installed version in the message the way the Nampower branch does it.
ClassicAPI.lua gains GetVersion() to decode CLASSIC_API_VERSION, keeping
the X*10000 + Y*100 + Z encoding inside that module, and its header notes
that the minimum has to rise with any ClassicAPI call adopted at file
scope -- that is the property that turns a version mismatch into a silent
half-load rather than a missing feature.

C_Macro.SetMacroDisplay also ships in v1.15.0, which retires the last
pre-ClassicAPI display path:

  useClassicAPIDisplay folds into ClassicAPIMacroDisplay. The two were
  always equal -- set together at load, cleared together in
  ReleaseDisplays -- and one of them is a handshake ClassicAPI reads, so
  a second name for the same state could only ever drift.

  TestForAllActiveActions loses the per-slot ACTIONBAR_SLOT_CHANGED
  fan-out. Publishing repaints every slot holding the macro through the
  client's own notifier; the fan-out was what ran when SetMacroDisplay
  was unavailable, and its one remaining reachable state was after
  DisableAddon, where ReleaseDisplays has already handed all 36 macros
  back and ClassicAPI repaints them itself.

The load-time feature detect stays, with a new reason: below the minimum
the addon is already broken, so a nil SetMacroDisplay now means ClassicAPI
is absent outright -- the case the requirement check warns about and then
keeps running.
2026-09-11 13:44:09 -05:00
Brues c8bc4c134d Filter UNIT_AURA by unit token and drop the unit streams in realtime mode
Two handlers opened by testing arg1 against a fixed set of unit tokens, so
every other unit's copy of UNIT_AURA reached Lua only to be compared away
-- in a raid that is the bulk of them. ClassicAPI's RegisterUnitEvent
pushes that filter into the client, so the handler only runs for the units
named. Filtering is per (frame, event), so the other events sharing the
libdebuff frame are unaffected.

  Utility.lua      UNIT_AURA -> "target"            (libdebuff seeding)
  ComboPointTracker UNIT_AURA -> "target", "player"

Extensions register through CleveRoids.RegisterEvent, so ExtensionsManager
gains a RegisterUnitEvent alongside it.

Separately, the main frame's unit state streams (UNIT_AURA / UNIT_HEALTH /
UNIT_POWER, or the _GUID variants under Nampower v2.39+) are now registered
only in event-driven mode. Their handlers are wholly wrapped in
`realtime == 0`, so with realtime on every one of them crossed into Lua and
returned immediately while the OnUpdate did the refreshing. Registration is
applied at load, again at VARIABLES_LOADED once the saved setting is
readable, and whenever /cleveroid realtime flips it.

Those three stay plain RegisterEvent calls: their handlers ignore the unit
and refresh every macro, and a conditional may name any unit
([@party3,hp:<50], @focus, @mouseover), so narrowing the token set would
leave those icons stale. The _GUID variants could not be filtered anyway --
their first argument is a GUID, not a unit token.
2026-09-11 00:42:42 -05:00
Brues 5213bd5474 Migrate to ClassicAPI for macro display, auras and timing
Squash of the classicapi_next branch (10 commits). ClassicAPI is now a hard
requirement, and the Lua-side reimplementations it supersedes are gone:
net -1087 lines across 12 files.

Macro display: resolved macro actions are published through
C_Macro.SetMacroDisplay, so the client draws macro buttons and the
action-bar function overrides this addon used to install are removed.
Ownership is per macro -- ClassicAPI keeps the macros we never claim.

Auras and timing: ClassicAPI is the source of truth for non-player aura
timing via the positional C_UnitAuras.UnitAura, replacing the write-only
buff tracking tables and the overflow-slot fallback.

Cleanup: the hasPfUI76 flag and the pfUI 7.6 branches it gated, two
silently-shadowed code paths, and per-call allocations in the event and
publish paths (SPELL_CAST_EVENT merged). Packaging moves to
brues-code/packager@vCAPI.

Macro syntax: a clause may carry a leading run of [group] blocks sharing
one action, Blizzard-style -- groups are OR'd, first pass wins, and []
always passes. The ;-separated form is unchanged and mixes freely. @focus
clauses with no focus set now fail quietly instead of printing
"Invalid target" on the way past.
2026-09-11 00:30:36 -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
Brues 86fb0254e6 Show autoattack icon and glow 2026-06-30 12:34:34 -05:00
Brues fc640abb07 Render known-spell action tooltips via spellbook slot, not spellID
GameTooltip.SetAction used GameTooltip:SetSpellByID for spell actions, which
renders the static DBC tooltip. For spells we resolved from the player's own
spellbook, use GameTooltip:SetSpell(spellSlot, bookType) instead so the
tooltip shows live player-accurate data (mana cost, cooldown, range coloring,
reagent counts). spellSlot/bookType/id all come from the same spellbook index
entry, so it renders the identical spell and rank. SetSpellByID remains as a
defensive fallback for entries without a slot. Applied to both the direct and
nested-macro tooltip paths.
2026-06-23 12:41:17 -05:00
Brues 3384765faa dont need pcall around registering events 2026-06-22 13:04:05 -05:00
Brues ceaf7c2c43 Refresh [moving] macro icons on movement via ClassicAPI
Register PLAYER_STARTED_MOVING (ClassicAPI, edge-detected off the WASD/
autorun key state) and queue an action update so [moving]/[nomoving] macro
icons repaint when movement starts.

PLAYER_STOPPED_MOVING is deliberately NOT used -- it's key-release based and
misses real stops (running into geometry, click-to-move, roots, knockback).
Instead, STARTED kicks off a 0.1s C_Timer.NewTicker that watches
IsPlayerMoving() (the same speed>0/falling signal [moving] evaluates) and, on
the actual stop, refreshes once and cancels itself. No timer exists while
stopped, so there's no idle cost; guarded on isShuttingDown and event-driven
mode (realtime==0).
2026-06-22 12:58:59 -05:00
Brues 963ad21297 Show resolved icon for question-mark macros in the macro list
Macros with no chosen icon (e.g. "#showtooltip Shoot") display Blizzard's
default question mark in the macro UI. After each MacroFrame_Update, swap
that placeholder for the icon the action bar would show, resolved from the
macro's #showtooltip/first action via GetMacroByIndex.

- Only replaces icons that are currently the question mark, so user-chosen
  icons are never touched.
- Covers both the list buttons (MacroButtonNIcon) and the selected-macro
  detail icon.
- Purely cosmetic: never changes the saved icon; Blizzard repaints the
  default on the next refresh if resolution fails or the macro changes.
- Installed in MacroErrorUI's existing hook path (gated on Blizzard_MacroUI
  load and the SuperMacro guard).
2026-06-22 00:35:19 -05:00
Brues ae09b23afd Resolve item sets via ClassicAPI; drop Reliquary dependency
Item-set membership and set info now read ItemSet.dbc directly through
ClassicAPI instead of the nampower/Reliquary glue, so set features no
longer require the Reliquary DLL to be installed.

- ClassicAPI.lua: add GetItemSetIDByID and GetItemSetInfo wrappers.
- Utility.lua: repoint ResolveSetItems, CountEquippedSetItemsBySetId,
  GetEquippedItemSetInfo, and GetEquippedSetPieceCount at the ClassicAPI
  accessors. Set-name matching now uses the DBC name (localized; identical
  to the old enUS path on English clients).
- NampowerAPI.lua: remove the now-unused GetItemSet/GetItemSetId/
  GetItemSetItems/GetItemSetBonuses, the dead GetSpellEffectRadius (no
  callers, only radius source was Reliquary), and the orphaned RQ_SafeCall.
- Init.lua: drop hasReliquary detection and its startup feature line.
- Conditionals.lua: update the [set:] comment (no longer Reliquary-gated).
2026-06-21 14:29:28 -05:00
Brues a8bf7fbc0f Identify equipped weapon type by item subclass, not localized name
HasWeaponEquipped matched the localized GetItemInfo subtype string, which
required a hand-maintained translation of all 13 weapon/shield subtypes per
locale plus brittle "^Fist"/last-word string parsing.

Use the locale-independent class/subclass IDs from C_Item.GetItemInfoInstant
instead:
- Init.lua: WeaponTypeNames entries now carry numeric class + subClass set
  (Axes/Swords/Maces span their 1H+2H subclasses; Shields are armor class 4).
- Conditionals.lua: HasWeaponEquipped resolves the slot's itemID via
  ClassicAPI GetInventoryItemID, then compares classID/subClassID -- no link
  parsing, no string munging.
- Localization.lua: drop the now-unused weapon-subtype strings from every
  locale block (112 lines).
2026-06-21 14:16:56 -05:00
Brues f0f57330c5 Drop dead localization entries
Remove localization data with no consumers anywhere in the addon:
- ItemTypes tables (Consumable/Reagent/Projectile/Trade Goods) - unused
- SpellRank patterns - unused (the live GetSpellRank is a BuffLib method)
- Spells keys Revenge, Overpower, Riposte, Surprise Attack, Lacerate,
  Baited Shot, Counterattack, Arcane Surge - unused; only Shadowform,
  Stealth, Prowl, Shadowmeld are referenced.

133 lines removed across all locale blocks; no behavior change.
2026-06-21 14:12:33 -05:00
Brues f21f90d0f1 Adopt ClassicAPI accessors: cursor, item IDs, spell mechanic
Lean on ClassicAPI's backported readers instead of link-string parsing
and a hand-maintained mechanic table.

- ClassicAPI.lua: add wrappers GetCursorInfo/CursorHoldsItemID,
  GetContainerItemID, GetInventoryItemID, and GetSpellMechanicByID.
- Core.lua: in the manual equip fallback, verify the cursor holds the
  intended item (CursorHoldsItemID) before EquipCursorItem, aborting only
  on a definitive mismatch so empty/unknown cursors behave as before.
- NampowerAPI.lua / Utility.lua: replace GetContainerItemLink /
  GetInventoryItemLink + "item:(%d+)" parsing with direct C_Container/
  inventory ID lookups at the sites that only need the itemID; route the
  equipped-set scans through the GetEquippedItemID helper.
- Conditionals.lua / Utility.lua: replace the 785-entry CCSpellMechanics
  fallback table with C_Spell.GetSpellMechanicByID in GetSpellMechanic and
  GetSpellCCType (BuffLib / nampower paths unchanged); delete the table.
2026-06-21 14:02:49 -05:00
Brues d435d90871 useless comment 2026-06-21 03:38:50 -05:00
34 changed files with 3487 additions and 5512 deletions
+1 -3
View File
@@ -18,8 +18,6 @@ jobs:
fetch-depth: 0
- name: Package and release to GitHub
uses: BigWigsMods/packager@v2
uses: brues-code/packager@vCAPI
env:
# Only GITHUB_OAUTH is set, so the packager attaches the zip to a
# GitHub Release and uploads nothing to CurseForge/WoWInterface/Wago.
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
+283 -14
View File
@@ -3,9 +3,17 @@
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.15.8+, which scoped GetMouseButtonClicked to the
click dispatch; v1.15.0 added frame:RegisterUnitEvent), 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.
The minimum is not advisory: Utility.lua calls frame:RegisterUnitEvent at file
scope, so an older ClassicAPI aborts that chunk and leaves most of the addon
undefined. Raise the Core.lua minimum in step with any new API adopted at file
scope.
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
@@ -35,6 +43,14 @@ function API.GetVersionNumber()
return CLASSIC_API_VERSION or 0
end
-- Returns the loaded version as major, minor, patch (0, 0, 0 if absent).
function API.GetVersion()
local v = CLASSIC_API_VERSION or 0
local major = math.floor(v / 10000)
local minor = math.floor(v / 100) - major * 100
return major, minor, v - math.floor(v / 100) * 100
end
-- True if the ClassicAPI client mod is loaded at all.
function API.IsAvailable()
return CLASSIC_API_VERSION ~= nil
@@ -52,16 +68,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, below 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 +93,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
--------------------------------------------------------------------------------
@@ -119,6 +155,194 @@ function API.GetActionInfo(slot)
return GetActionInfo(slot)
end
--------------------------------------------------------------------------------
-- Container
--------------------------------------------------------------------------------
-- Base itemID in (bagID, slot), or nil for an empty/invalid slot. Same value
-- the "item:(%d+)" parse of GetContainerItemLink yields, but resolved straight
-- from the CGItem -- no link string built, no Lua pattern match.
function API.GetContainerItemID(bagID, slot)
return C_Container.GetContainerItemID(bagID, slot)
end
-- Base itemID equipped in `unit`'s 1-based inventory `slot` (1-19), or nil for
-- an empty slot / NPC unit. Same arg shape as GetInventoryItemLink and the same
-- value its "item:(%d+)" parse yields, resolved straight from the item instance.
function API.GetInventoryItemID(unit, slot)
return GetInventoryItemID(unit, slot)
end
--------------------------------------------------------------------------------
-- Item Set
--------------------------------------------------------------------------------
-- ItemSet.dbc ID that `itemID` belongs to, or nil if it isn't part of a set
-- (or isn't cached yet). Reads the item's m_itemSet field directly -- no
-- Reliquary, no per-set ItemID[] scan.
function API.GetItemSetIDByID(itemID)
return C_Item.GetItemSetIDByID(itemID)
end
-- Table describing an ItemSet.dbc row, or nil if `setID` doesn't resolve:
-- { setID, name (localized), requiredSkill, requiredSkillRank,
-- items = { itemID, ... }, bonuses = { { spellID, threshold }, ... } }
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).
-- 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
--------------------------------------------------------------------------------
-- WoW SpellMechanic enum ID for a spell, read straight from Spell.dbc -- covers
-- every spell the client knows, not just the spellbook (1=Charm, 5=Fear,
-- 7=Root, 12=Stun, 17=Polymorph, ...). Returns (mechanicID, enUS name); the ID
-- is 0 for a known spell with no mechanic, and the whole call is nil for an
-- invalid spell ID. Replaces hand-maintained spellID -> mechanic tables.
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.
function API.UnitHealthMissing(unit)
return UnitHealthMissing(unit)
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).
function API.UnitPower(unit, powerType)
return UnitPower(unit, powerType)
end
function API.UnitPowerMax(unit, powerType)
return UnitPowerMax(unit, powerType)
end
-- Power deficit (max - current) for the type / primary power, in one call.
function API.UnitPowerMissing(unit, powerType)
return UnitPowerMissing(unit, powerType)
end
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
function API.UnitPowerType(unit)
return UnitPowerType(unit)
end
--------------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------------
@@ -133,6 +357,51 @@ 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
--------------------------------------------------------------------------------
function API.GetCursorInfo()
return GetCursorInfo()
end
-- Tri-state check of whether the cursor holds the item with `itemID`:
-- true -> cursor holds exactly that item
-- false -> cursor holds a DIFFERENT item
-- nil -> can't tell (cursor empty / not an item / itemID unknown)
-- Callers should only act on an explicit `false`, leaving the nil case to the
-- existing CursorHasItem() behavior.
function API.CursorHoldsItemID(itemID)
if not itemID then return nil end
local kind, id = GetCursorInfo()
if kind ~= "item" then return nil end
if not id then return nil end
return id == itemID
end
--------------------------------------------------------------------------------
-- NamePlate
--------------------------------------------------------------------------------
+39 -146
View File
@@ -15,22 +15,34 @@ CleveRoids.ComboPointTracking = CleveRoids.ComboPointTracking or {}
-- Structure: CleveRoids_ComboDurations[spellID][comboPoints] = duration
CleveRoids_ComboDurations = CleveRoids_ComboDurations or {}
-- Storage for last Rip cast (for Carnage talent mechanic)
-- Carnage talent: When Ferocious Bite procs Carnage, it refreshes Rip and Rake to their original duration
-- Detection: When combo points don't drop to 0 after FB (they stay at 1 = Carnage proc)
-- Talent Position: Tab 2 (Feral Combat), Talent 17
-- Rank 1: 10% per CP, Rank 2: 20% per CP
CleveRoids.lastRipCast = CleveRoids.lastRipCast or {
duration = nil,
targetGUID = nil,
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
CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
duration = nil,
targetGUID = nil,
timestamp = 0
}
-- 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
@@ -57,10 +69,8 @@ CleveRoids.ComboScalingSpellsByID = {
[9896] = { base = 10, increment = 2, name = "Rip" }, -- Rank 6
}
-- Ferocious Bite spell IDs (for Carnage talent mechanic)
-- Carnage talent: When FB procs Carnage, refreshes Rip and Rake back to their original duration
-- Proc detection: combo points stay at 1 after FB instead of dropping to 0
-- Talent Position: Tab 2 (Feral Combat), Talent 17
-- Ferocious Bite spell IDs. A combo-point finisher, so the cast-time combo
-- snapshot has to know it.
CleveRoids.FerociousBiteSpellIDs = {
[22557] = true, -- Rank 1
[22568] = true, -- Rank 2
@@ -70,33 +80,6 @@ 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)
}
-- Combined table for all bleed spells that need immunity detection
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
-- NOTE: These are the DEBUFF spell IDs (what appears on target), not cast spell IDs
@@ -132,14 +115,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 +132,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
@@ -172,17 +145,6 @@ CleveRoids.ImmolateSpellIDs = {
[25309] = true, -- Rank 8
}
-- =============================================================================
-- WARLOCK: Dark Harvest Duration Acceleration (TWoW Custom)
-- 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
}
-- =============================================================================
-- DRUID: Rake Debuff Cap Boss Whitelist
-- These bosses are likely to hit the 48 debuff cap, causing Rake to get pushed off
@@ -489,7 +451,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 +510,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 +625,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
@@ -772,7 +714,7 @@ function Extension.OnLoad()
Extension.RegisterEvent("SPELLCAST_FAILED", "OnSpellcastFailed")
Extension.RegisterEvent("SPELLCAST_INTERRUPTED", "OnSpellcastInterrupted")
Extension.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
Extension.RegisterEvent("UNIT_AURA", "OnUnitAura")
Extension.RegisterUnitEvent("UNIT_AURA", "OnUnitAura", "target", "player")
Extension.RegisterEvent("PLAYER_COMBO_POINTS", "OnComboPointsChanged")
-- PERFORMANCE OPTIMIZATION: Removed OnUpdate polling for combo points
@@ -792,63 +734,14 @@ function Extension.OnTargetChanged()
CleveRoids.UpdateComboPoints()
end
-- Registered as a unit event for target and player, so arg1 is always one of
-- those two -- no token check needed.
function Extension.OnUnitAura()
if arg1 == "target" or arg1 == "player" then
CleveRoids.UpdateComboPoints()
end
CleveRoids.UpdateComboPoints()
end
function Extension.OnComboPointsChanged()
CleveRoids.UpdateComboPoints()
-- CARNAGE PROC DETECTION (Cursive-style)
-- When Ferocious Bite is used, combo points should drop to 0
-- If Carnage procs, combo points will be 1 instead (the Carnage-granted combo point)
-- Check: After Ferocious Bite (within 0.5s), if combo points > 0, Carnage procced
if CleveRoids.lastFerociousBiteTime and CleveRoids.lastFerociousBiteTargetGUID then
local timeSinceBite = GetTime() - CleveRoids.lastFerociousBiteTime
if timeSinceBite < 0.5 then
local currentCP = CleveRoids.GetComboPoints()
if currentCP > 0 then
-- Carnage procced! Combo points didn't drop to 0 (or rose back to 1)
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r PROC DETECTED! CP=%d after Ferocious Bite (%.2fs ago)",
currentCP, timeSinceBite)
)
end
-- Apply the Carnage refresh to Rip and Rake
local targetGUID = CleveRoids.lastFerociousBiteTargetGUID
local targetName = CleveRoids.lastFerociousBiteTargetName or "Unknown"
local biteSpellID = CleveRoids.lastFerociousBiteSpellID
-- Call the Carnage refresh function in Utility.lua
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyCarnageRefresh then
CleveRoids.libdebuff.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
end
-- Clear the tracking to prevent multiple refreshes
CleveRoids.lastFerociousBiteTime = nil
CleveRoids.lastFerociousBiteTargetGUID = nil
CleveRoids.lastFerociousBiteTargetName = nil
CleveRoids.lastFerociousBiteSpellID = nil
end
else
-- Time window expired, clear tracking
if CleveRoids.lastFerociousBiteTime then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r No proc - time window expired (%.2fs)", timeSinceBite)
)
end
CleveRoids.lastFerociousBiteTime = nil
CleveRoids.lastFerociousBiteTargetGUID = nil
CleveRoids.lastFerociousBiteTargetName = nil
CleveRoids.lastFerociousBiteSpellID = nil
end
end
end
end
-- Event handlers
+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
+701 -778
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)
+990 -1529
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.")
+103 -38
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)
@@ -1164,7 +1180,7 @@ local function ReportAllMacroErrors()
-- Collect body (syntax) errors per macro. Macro names are no longer
-- restricted (slot/index-based identification), so no name checks here.
for i = 1, 36 do
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
local nameOk, name = pcall(GetMacroInfo, i)
if nameOk and name and name ~= "" then
local errors = {}
@@ -1274,6 +1290,56 @@ end
-- Hook Installation
-- ============================================================================
-- ============================================================================
-- Dynamic macro-list icons
-- Blizzard shows the default question-mark icon for macros with no chosen icon
-- (e.g. "#showtooltip Shoot"). Replace it with the icon the action bar would
-- show -- resolved from the macro's #showtooltip/first action -- but ONLY when
-- the saved icon is the question mark, so user-chosen icons are never touched.
-- Purely cosmetic: Blizzard repaints from GetMacroInfo on the next update, so
-- if resolution fails or the macro changes, the default simply returns.
-- ============================================================================
local QUESTION_MARK = string.lower(CleveRoids.unknownTexture or "Interface\\Icons\\INV_Misc_QuestionMark")
local function IsQuestionMark(iconTexture)
local tex = iconTexture and iconTexture:GetTexture()
return type(tex) == "string" and string.lower(tex) == QUESTION_MARK
end
-- Resolved tooltip texture for a Blizzard macro index, or nil when it resolves
-- to nothing better than the question mark (no #showtooltip, unresolved spell).
local function ResolveMacroIcon(macroIndex)
if not macroIndex or macroIndex < 1 then return nil end
local ok, macro = pcall(CleveRoids.GetMacroByIndex, macroIndex)
if not ok or not macro or not macro.actions or not macro.actions.tooltip then return nil end
local tex = macro.actions.tooltip.texture
if type(tex) == "string" and string.lower(tex) ~= QUESTION_MARK then
return tex
end
return nil
end
local function FixMacroListIcons()
if not MacroFrame or not MacroFrame:IsVisible() then return end
local base = MacroFrame.macroBase or 0
for i = 1, (MAX_MACROS or 18) do
local icon = getglobal("MacroButton" .. i .. "Icon")
if icon and IsQuestionMark(icon) then
local tex = ResolveMacroIcon(base + i)
if tex then icon:SetTexture(tex) end
end
end
-- Large icon for the currently-selected macro (details pane).
if MacroFrame.selectedMacro and MacroFrameSelectedMacroButtonIcon
and IsQuestionMark(MacroFrameSelectedMacroButtonIcon) then
local tex = ResolveMacroIcon(MacroFrame.selectedMacro)
if tex then MacroFrameSelectedMacroButtonIcon:SetTexture(tex) end
end
end
local function InstallHooks()
if hooked then return end
if not MacroFrameText or not MacroFrame then return end
@@ -1325,6 +1391,15 @@ local function InstallHooks()
end
end)
-- After every Blizzard macro-list refresh, swap question-mark icons for the
-- dynamically-resolved ones. Post-hook so Blizzard has already set the
-- default texture we test against.
if hooksecurefunc and type(MacroFrame_Update) == "function" then
hooksecurefunc("MacroFrame_Update", function()
pcall(FixMacroListIcons)
end)
end
hooked = true
end
@@ -1332,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
@@ -1345,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
+18
View File
@@ -66,6 +66,10 @@ function CleveRoids.RegisterExtension(name)
CleveRoids.RegisterEvent(name, eventName, callbackName)
end
extension.RegisterUnitEvent = function(eventName, callbackName, ...)
CleveRoids.RegisterUnitEvent(name, eventName, callbackName, unpack(arg))
end
extension.Hook = function(functionName, callbackName, dontCallOriginal)
CleveRoids.RegisterHook(name, functionName, callbackName, dontCallOriginal)
end
@@ -146,6 +150,20 @@ function CleveRoids.RegisterEvent(extensionName, eventName, callbackName)
extension.internal.frame:RegisterEvent(eventName)
end
-- Registers a callback for a UNIT_* event, filtered to the given unit tokens.
-- The callback then only runs for those units: the client drops every other
-- unit's copy, instead of all of them reaching Lua to be compared away. Use
-- this over RegisterEvent whenever the handler starts by testing arg1.
-- extensionName: The name of the extension trying to register the callback
-- eventName: The UNIT_* event to register
-- callbackName: The name of the callback that gets called when the event fires
-- ...: the unit tokens to accept (e.g. "player", "target")
function CleveRoids.RegisterUnitEvent(extensionName, eventName, callbackName, ...)
local extension = CleveRoids.Extensions[extensionName]
extension.internal.eventHandlers[eventName] = callbackName
extension.internal.frame:RegisterUnitEvent(eventName, unpack(arg))
end
-- Hooks the given function by it's name
-- extensionName: The name of the extension trying to register the callback
-- functionName: The name of the function that'll be hooked
+64 -34
View File
@@ -19,13 +19,40 @@ CleveRoids.mouseOverResolvers = {}
CleveRoids.mouseoverUnit = CleveRoids.mouseoverUnit or nil
CleveRoids.mouseOverUnit = nil
-- Every macro slot the client can hold: 18 account-wide (1-18) followed by 18
-- character-specific (19-36). This is the index space GetMacroInfo and
-- C_Macro.SetMacroDisplay address, and it is fixed -- GetNumMacros() returns how
-- many of each tab are *used*, which cannot be summed into a range, because the
-- character block starts at 19 no matter how few account macros exist. Blizzard's
-- own MAX_MACROS is no help either: it lives in the load-on-demand Blizzard_MacroUI
-- and is nil until the player opens the macro window.
CleveRoids.MAX_MACRO_SLOTS = 36
-- Environment flags
CleveRoids.hasSuperwow = SetAutoloot and true or false
CleveRoids.hasTurtle = (type(_G.TURTLE_WOW_VERSION) ~= "nil")
CleveRoids.hasReliquary = (RQ_GetVersion ~= 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.
--
-- ClassicAPI stands down from macro display entirely when it sees this addon loaded;
-- ClassicAPIMacroDisplay is what tells it we drive it instead, and ReleaseDisplays
-- clears it to hand every macro back. A fork that leaves the flag unset keeps the old
-- behavior -- both must never drive the same buttons. It doubles as the internal
-- "may we call C_Macro.SetMacroDisplay" guard, so the two can never disagree.
--
-- Feature-detect rather than version-check: SetMacroDisplay ships in ClassicAPI
-- v1.15.0, below this addon's minimum, so a nil here means the client mod is
-- missing outright -- the case Core.lua's requirement check warns about but
-- keeps running.
CleveRoids.ClassicAPIMacroDisplay =
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
CleveRoids.ParsedMsg = {}
CleveRoids.ExpandedGroups = {}
CleveRoids.Items = {}
CleveRoids.Spells = {}
CleveRoids.PetSpells = {}
@@ -44,13 +71,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
@@ -125,6 +150,7 @@ CleveRoids.dynamicCmds = {
["/castpet"] = true,
["/castsequence"] = true,
["/use"] = true,
["/feedpet"] = true,
["/equip"] = true,
["/equipmh"] = true,
["/equipoh"] = true,
@@ -187,11 +213,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,
@@ -242,28 +276,32 @@ CleveRoids.auraTextures = {
-- I need to make a 2h modifier
-- Maps easy to use weapon type names (e.g. Axes, Shields) to their inventory slot name and their localized tooltip name
-- Maps easy-to-use weapon type names (e.g. Axes, Shields) to their inventory
-- slot plus the locale-independent item class/subclass IDs that identify them
-- (read via C_Item.GetItemInfoInstant). class 2 = Weapon, 4 = Armor (shields).
-- subClass is a set because the logical "Axes"/"Swords"/"Maces" types span both
-- the one-handed and two-handed weapon subclasses.
CleveRoids.WeaponTypeNames = {
Daggers = { slot = "MainHandSlot", name = CleveRoids.Localized.Dagger },
Fists = { slot = "MainHandSlot", name = CleveRoids.Localized.FistWeapon },
Axes = { slot = "MainHandSlot", name = CleveRoids.Localized.Axe },
Swords = { slot = "MainHandSlot", name = CleveRoids.Localized.Sword },
Staves = { slot = "MainHandSlot", name = CleveRoids.Localized.Staff },
Maces = { slot = "MainHandSlot", name = CleveRoids.Localized.Mace },
Polearms = { slot = "MainHandSlot", name = CleveRoids.Localized.Polearm },
Daggers = { slot = "MainHandSlot", class = 2, subClass = { [15] = true } },
Fists = { slot = "MainHandSlot", class = 2, subClass = { [13] = true } },
Axes = { slot = "MainHandSlot", class = 2, subClass = { [0] = true, [1] = true } },
Swords = { slot = "MainHandSlot", class = 2, subClass = { [7] = true, [8] = true } },
Staves = { slot = "MainHandSlot", class = 2, subClass = { [10] = true } },
Maces = { slot = "MainHandSlot", class = 2, subClass = { [4] = true, [5] = true } },
Polearms = { slot = "MainHandSlot", class = 2, subClass = { [6] = true } },
-- OH
Daggers2 = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.Dagger },
Fists2 = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.FistWeapon },
Axes2 = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.Axe },
Swords2 = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.Sword },
Maces2 = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.Mace },
Shields = { slot = "SecondaryHandSlot", name = CleveRoids.Localized.Shield },
Daggers2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [15] = true } },
Fists2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [13] = true } },
Axes2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [0] = true, [1] = true } },
Swords2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [7] = true, [8] = true } },
Maces2 = { slot = "SecondaryHandSlot", class = 2, subClass = { [4] = true, [5] = true } },
Shields = { slot = "SecondaryHandSlot", class = 4, subClass = { [6] = true } },
-- ranged
Guns = { slot = "RangedSlot", name = CleveRoids.Localized.Gun },
Crossbows = { slot = "RangedSlot", name = CleveRoids.Localized.Crossbow },
Bows = { slot = "RangedSlot", name = CleveRoids.Localized.Bow },
Thrown = { slot = "RangedSlot", name = CleveRoids.Localized.Thrown },
Wands = { slot = "RangedSlot", name = CleveRoids.Localized.Wand },
Guns = { slot = "RangedSlot", class = 2, subClass = { [3] = true } },
Crossbows = { slot = "RangedSlot", class = 2, subClass = { [18] = true } },
Bows = { slot = "RangedSlot", class = 2, subClass = { [2] = true } },
Thrown = { slot = "RangedSlot", class = 2, subClass = { [16] = true } },
Wands = { slot = "RangedSlot", class = 2, subClass = { [19] = true } },
}
-- Detect available features
@@ -287,14 +325,6 @@ local function PrintFeatures()
end
end
if CleveRoids.hasUnitXP then table.insert(features, "UnitXP") end
if CleveRoids.hasReliquary then
local ok, major, minor, patch = pcall(RQ_GetVersion)
if ok and major then
table.insert(features, string.format("Reliquary v%d.%d.%d", major, minor, patch))
else
table.insert(features, "Reliquary")
end
end
if CleveRoids.hasTurtle then table.insert(features, "Turtle") end
if table.getn(features) > 0 then
-245
View File
@@ -8,26 +8,9 @@ CleveRoids.Locale = GetLocale()
CleveRoids.Localized = {}
if CleveRoids.Locale == "enUS" or CleveRoids.Locale == "enGB" then
-- place item in backpack slot 1 and run:
-- /script local l=GetContainerItemLink(0,1);local _,_,id=string.find(l,"item:(%d+)");local n,_,_,_,t,st=GetItemInfo(id);DEFAULT_CHAT_FRAME:AddMessage("\n\nID: ["..id.."]\nName: ["..n.."]\nType: ["..t.."]\nSub Type: ["..st.."]\n\n");
CleveRoids.Localized.Shield = "Shields"
CleveRoids.Localized.Bow = "Bows"
CleveRoids.Localized.Crossbow = "Crossbows"
CleveRoids.Localized.Gun = "Guns"
CleveRoids.Localized.Thrown = "Thrown"
CleveRoids.Localized.Wand = "Wands"
CleveRoids.Localized.Sword = "Swords"
CleveRoids.Localized.Staff = "Staves"
CleveRoids.Localized.Polearm = "Polearms"
CleveRoids.Localized.Mace = "Maces"
CleveRoids.Localized.FistWeapon = "Fist Weapons"
CleveRoids.Localized.Dagger = "Daggers"
CleveRoids.Localized.Axe = "Axes"
CleveRoids.Localized.Attack = "Attack"
CleveRoids.Localized.AutoShot = "Auto Shot"
CleveRoids.Localized.Shoot = "Shoot"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
-- target creature and run:
-- /script local ct, uc = UnitCreatureType("target"),UnitClassification("target"); DEFAULT_CHAT_FRAME:AddMessage("\n\nUnitCreatureType: ["..ct.."]\nUnitClassificationType: ["..uc.."]\n\n");
@@ -50,43 +33,11 @@ if CleveRoids.Locale == "enUS" or CleveRoids.Locale == "enGB" then
["Stealth"] = "Stealth",
["Prowl"] = "Prowl",
["Shadowmeld"] = "Shadowmeld",
["Revenge"] = "Revenge",
["Overpower"] = "Overpower",
["Riposte"] = "Riposte",
["Surprise Attack"] = "Surprise Attack",
["Lacerate"] = "Lacerate",
["Baited Shot"] = "Baited Shot",
["Counterattack"] = "Counterattack",
["Arcane Surge"] = "Arcane Surge",
}
-- place item in backpack slot 1 and run:
-- /script local l=GetContainerItemLink(0,1);local _,_,id=string.find(l,"item:(%d+)");local n,_,_,_,t,st=GetItemInfo(id);DEFAULT_CHAT_FRAME:AddMessage("\n\nID: ["..id.."]\nName: ["..n.."]\nType: ["..t.."]\nSub Type: ["..st.."]\n\n");
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "Consumable",
["Reagent"] = "Reagent",
["Projectile"] = "Projectile",
["Trade Goods"] = "Trade Goods",
}
elseif CleveRoids.Locale == "deDE" then
CleveRoids.Localized.Shield = "Schilde"
CleveRoids.Localized.Bow = "Bögen"
CleveRoids.Localized.Crossbow = "Armbrüste"
CleveRoids.Localized.Gun = "Waffen"
CleveRoids.Localized.Thrown = "Geworfen"
CleveRoids.Localized.Wand = "Zauberstäbe"
CleveRoids.Localized.Sword = "Schwerter"
CleveRoids.Localized.Staff = "Dauben"
CleveRoids.Localized.Polearm = "Stangenwaffen"
CleveRoids.Localized.Mace = "Streitkolben"
CleveRoids.Localized.FistWeapon = "Faustwaffen"
CleveRoids.Localized.Dagger = "Dolche"
CleveRoids.Localized.Axe = "Äxte"
CleveRoids.Localized.Attack = "Angriff"
CleveRoids.Localized.AutoShot = "Automatischer Schuss"
CleveRoids.Localized.Shoot = "Schießen"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "Wildtier",
@@ -108,42 +59,11 @@ elseif CleveRoids.Locale == "deDE" then
["Stealth"] = "Verstohlenheit",
["Prowl"] = "Schleichen",
["Shadowmeld"] = "Schattenmimik",
["Revenge"] = "Rache",
["Overpower"] = "Überwältigen",
["Riposte"] = "Riposte",
["Surprise Attack"] = "Überraschungsangriff",
["Lacerate"] = "Zerfleischen",
["Baited Shot"] = "Köderschuss",
["Counterattack"] = "Gegenangriff",
["Arcane Surge"] = "Arkane Woge",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "Verbrauchsmaterial",
["Reagent"] = "Reagens",
["Projectile"] = "Projektil",
["Trade Goods"] = "Handwerkswaren",
}
elseif CleveRoids.Locale == "frFR" then
CleveRoids.Localized.Shield = "Boucliers"
CleveRoids.Localized.Bow = "Arcs"
CleveRoids.Localized.Crossbow = "Arbalètes"
CleveRoids.Localized.Gun = "Armes à feu"
CleveRoids.Localized.Thrown = "Thrown"
CleveRoids.Localized.Wand = "Wands"
CleveRoids.Localized.Sword = "Swords"
CleveRoids.Localized.Staff = "Staves"
CleveRoids.Localized.Polearm = "Polearms"
CleveRoids.Localized.Mace = "Maces"
CleveRoids.Localized.FistWeapon = "Fist Weapons"
CleveRoids.Localized.Dagger = "Daggers"
CleveRoids.Localized.Axe = "Axes"
CleveRoids.Localized.Attack = "Attack"
CleveRoids.Localized.AutoShot = "Auto Shot"
CleveRoids.Localized.Shoot = "Shoot"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "Bête",
@@ -164,41 +84,11 @@ elseif CleveRoids.Locale == "frFR" then
["Stealth"] = "Camouflage",
["Prowl"] = "Rôder",
["Shadowmeld"] = "Camouflage dans l'ombre",
["Revenge"] = "Vengeance",
["Overpower"] = "Fulgurance",
["Riposte"] = "Riposte",
["Surprise Attack"] = "Attaque surprise",
["Lacerate"] = "Lacérer",
["Baited Shot"] = "Tir appâté",
["Counterattack"] = "Contre-attaque",
["Arcane Surge"] = "Éruption darcanes",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "Consommable",
["Reagent"] = "Reagent",
["Projectile"] = "Projectile",
["Trade Goods"] = "Artisanat",
}
elseif CleveRoids.Locale == "koKR" then
CleveRoids.Localized.Shield = "Shields"
CleveRoids.Localized.Bow = "Bows"
CleveRoids.Localized.Crossbow = "Crossbows"
CleveRoids.Localized.Gun = "Guns"
CleveRoids.Localized.Thrown = "Thrown"
CleveRoids.Localized.Wand = "Wands"
CleveRoids.Localized.Sword = "Swords"
CleveRoids.Localized.Staff = "Staves"
CleveRoids.Localized.Polearm = "Polearms"
CleveRoids.Localized.Mace = "Maces"
CleveRoids.Localized.FistWeapon = "Fist Weapons"
CleveRoids.Localized.Dagger = "Daggers"
CleveRoids.Localized.Axe = "Axes"
CleveRoids.Localized.Attack = "Attack"
CleveRoids.Localized.AutoShot = "Auto Shot"
CleveRoids.Localized.Shoot = "Shoot"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "야수",
@@ -219,41 +109,11 @@ elseif CleveRoids.Locale == "koKR" then
["Stealth"] = "은신",
["Prowl"] = "숨기",
["Shadowmeld"] = "그림자 숨기",
["Revenge"] = "복수",
["Overpower"] = "제압",
["Riposte"] = "반격",
["Surprise Attack"] = "기습",
["Lacerate"] = "괴롭히다",
["Baited Shot"] = "베이티드 샷",
["Counterattack"] = "역습",
["Arcane Surge"] = "비전 쇄도",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "소모품",
["Reagent"] = "재료",
["Projectile"] = "발사체",
["Trade Goods"] = "거래 용품",
}
elseif CleveRoids.Locale == "zhCN" then
CleveRoids.Localized.Shield = "盾牌"
CleveRoids.Localized.Bow = ""
CleveRoids.Localized.Crossbow = ""
CleveRoids.Localized.Gun = "枪械"
CleveRoids.Localized.Thrown = "投掷武器"
CleveRoids.Localized.Wand = "魔杖"
CleveRoids.Localized.Sword = ""
CleveRoids.Localized.Staff = "法杖"
CleveRoids.Localized.Polearm = "长柄武器"
CleveRoids.Localized.Mace = ""
CleveRoids.Localized.FistWeapon = "拳套"
CleveRoids.Localized.Dagger = "匕首"
CleveRoids.Localized.Axe = ""
CleveRoids.Localized.Attack = "攻击"
CleveRoids.Localized.AutoShot = "自动射击"
CleveRoids.Localized.Shoot = "射击"
CleveRoids.Localized.SpellRank = "%(等级 %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "野兽",
@@ -274,41 +134,11 @@ elseif CleveRoids.Locale == "zhCN" then
["Stealth"] = "潜行",
["Prowl"] = "潜行",
["Shadowmeld"] = "影遁",
["Revenge"] = "复仇",
["Overpower"] = "压制",
["Riposte"] = "还击",
["Surprise Attack"] = "偷袭",
["Lacerate"] = "划破",
["Baited Shot"] = "诱饵射击",
["Counterattack"] = "反击",
["Arcane Surge"] = "奥术涌动",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "消耗品",
["Reagent"] = "材料",
["Projectile"] = "弹药",
["Trade Goods"] = "商品",
}
elseif CleveRoids.Locale == "zhTW" then
CleveRoids.Localized.Shield = "盾牌"
CleveRoids.Localized.Bow = "長弓"
CleveRoids.Localized.Crossbow = ""
CleveRoids.Localized.Gun = "槍械"
CleveRoids.Localized.Thrown = "投擲武器"
CleveRoids.Localized.Wand = "魔杖"
CleveRoids.Localized.Sword = ""
CleveRoids.Localized.Staff = "法杖"
CleveRoids.Localized.Polearm = "長柄武器"
CleveRoids.Localized.Mace = ""
CleveRoids.Localized.FistWeapon = "拳套"
CleveRoids.Localized.Dagger = "匕首"
CleveRoids.Localized.Axe = ""
CleveRoids.Localized.Attack = "攻擊"
CleveRoids.Localized.AutoShot = "自動射擊"
CleveRoids.Localized.Shoot = "射擊"
CleveRoids.Localized.SpellRank = "%(等級 %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "野獸",
@@ -329,41 +159,11 @@ elseif CleveRoids.Locale == "zhTW" then
["Stealth"] = "隱形",
["Prowl"] = "徘徊",
["Shadowmeld"] = "影遁",
["Revenge"] = "復仇",
["Overpower"] = "壓倒",
["Riposte"] = "還擊",
["Surprise Attack"] = "偷襲",
["Lacerate"] = "劃破",
["Baited Shot"] = "誘餌射擊",
["Counterattack"] = "反擊",
["Arcane Surge"] = "奧術湧動",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "消耗品",
["Reagent"] = "材料",
["Projectile"] = "彈藥",
["Trade Goods"] = "貿易貨物",
}
elseif CleveRoids.Locale == "ruRU" then
CleveRoids.Localized.Shield = "Shields"
CleveRoids.Localized.Bow = "Bows"
CleveRoids.Localized.Crossbow = "Crossbows"
CleveRoids.Localized.Gun = "Guns"
CleveRoids.Localized.Thrown = "Thrown"
CleveRoids.Localized.Wand = "Wands"
CleveRoids.Localized.Sword = "Swords"
CleveRoids.Localized.Staff = "Staves"
CleveRoids.Localized.Polearm = "Polearms"
CleveRoids.Localized.Mace = "Maces"
CleveRoids.Localized.FistWeapon = "Fist Weapons"
CleveRoids.Localized.Dagger = "Daggers"
CleveRoids.Localized.Axe = "Axes"
CleveRoids.Localized.Attack = "Attack"
CleveRoids.Localized.AutoShot = "Auto Shot"
CleveRoids.Localized.Shoot = "Shoot"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "Животное",
@@ -384,41 +184,11 @@ elseif CleveRoids.Locale == "ruRU" then
["Stealth"] = "Незаметность",
["Prowl"] = "Крадущийся зверь",
["Shadowmeld"] = "Слияние с тенью",
["Revenge"] = "Реванш",
["Overpower"] = "Превосходство",
["Riposte"] = "Ответный удар",
["Surprise Attack"] = "Внезапная атака",
["Lacerate"] = "Разрыв",
["Baited Shot"] = "Выстрел с наживкой",
["Counterattack"] = "Контратака",
["Arcane Surge"] = "Чародейский выброс",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "Расходный материал",
["Reagent"] = "Reagent",
["Projectile"] = "Projectile",
["Trade Goods"] = "Хозяйственные товары",
}
elseif CleveRoids.Locale == "esES" then
CleveRoids.Localized.Shield = "Shields"
CleveRoids.Localized.Bow = "Bows"
CleveRoids.Localized.Crossbow = "Crossbows"
CleveRoids.Localized.Gun = "Guns"
CleveRoids.Localized.Thrown = "Thrown"
CleveRoids.Localized.Wand = "Wands"
CleveRoids.Localized.Sword = "Swords"
CleveRoids.Localized.Staff = "Staves"
CleveRoids.Localized.Polearm = "Polearms"
CleveRoids.Localized.Mace = "Maces"
CleveRoids.Localized.FistWeapon = "Fist Weapons"
CleveRoids.Localized.Dagger = "Daggers"
CleveRoids.Localized.Axe = "Axes"
CleveRoids.Localized.Attack = "Attack"
CleveRoids.Localized.AutoShot = "Auto Shot"
CleveRoids.Localized.Shoot = "Shoot"
CleveRoids.Localized.SpellRank = "%(Rank %d+%)"
CleveRoids.Localized.CreatureTypes = {
["Beast"] = "Bestia",
@@ -439,21 +209,6 @@ elseif CleveRoids.Locale == "esES" then
["Stealth"] = "Sigilo",
["Prowl"] = "Acechar",
["Shadowmeld"] = "Fusión con las sombras",
["Revenge"] = "Revancha",
["Overpower"] = "Abrumar",
["Riposte"] = "Estocada",
["Surprise Attack"] = "Ataque sorpresa",
["Lacerate"] = "Lacerar",
["Baited Shot"] = "Disparo con cebo",
["Counterattack"] = "Contraataque",
["Arcane Surge"] = "Oleada Arcana",
}
CleveRoids.Localized.ItemTypes = {
["Consumable"] = "Consumible",
["Reagent"] = "Reagent",
["Projectile"] = "Projectile",
["Trade Goods"] = "Objetos comerciables",
}
end
+42 -27
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*)")
@@ -1071,17 +1096,7 @@ function CleveRoids.ValidateAllMacros()
local results = {}
local totalErrors = 0
-- Account-wide macros are indexed from 1 up to GetNumMacros().
-- Character-specific macros occupy the slots immediately following the account-wide ones.
-- In Classic clients, the macro UI has 18 General (Account) slots and 18 Character-Specific slots.
local numAccountMacros = GetNumMacros()
-- The WoW API GetMacroInfo(index) supports indexing up to 36 (1-18 for General, 19-36 for Character)
-- in Classic clients, even though the total is GetNumMacros() + GetNumCharacterMacros() in Retail.
-- To ensure we check all 36 possible slots:
local totalSlots = 36
for i = 1, totalSlots do
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
local nameSuccess, name = pcall(GetMacroInfo, i)
-- Check if GetMacroInfo returned a name (i.e., the slot is used)
+64 -234
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,16 +1489,13 @@ function API.GetEquippedItems(unitToken)
end
local items = {}
for slot = 0, 18 do
local link = GetInventoryItemLink("player", slot + 1)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
if itemId then
items[slot] = {
itemId = tonumber(itemId),
-- Other fields not available without native API
}
end
for slot = 1, 19 do
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot)
if itemId then
items[slot] = {
itemId = itemId,
-- Other fields not available without native API
}
end
end
@@ -1524,14 +1518,11 @@ function API.GetEquippedItem(unitToken, slot)
return nil
end
local link = GetInventoryItemLink("player", slot + 1) -- 1-indexed
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
if itemId then
return {
itemId = tonumber(itemId),
}
end
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", slot) -- both are 1-indexed
if itemId then
return {
itemId = itemId,
}
end
return nil
@@ -1560,16 +1551,13 @@ function API.GetBagItems(bagIndex)
local bagContents = {}
local numSlots = GetContainerNumSlots(bagIndex) or 0
for slot = 1, numSlots do
local link = GetContainerItemLink(bagIndex, slot)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
local itemId = CleveRoids.ClassicAPI.GetContainerItemID(bagIndex, slot)
if itemId then
local _, count = GetContainerItemInfo(bagIndex, slot)
if itemId then
bagContents[slot] = {
itemId = tonumber(itemId),
stackCount = count or 1,
}
end
bagContents[slot] = {
itemId = itemId,
stackCount = count or 1,
}
end
end
return bagContents
@@ -1582,16 +1570,13 @@ function API.GetBagItems(bagIndex)
if numSlots > 0 then
bags[bag] = {}
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
local itemId = CleveRoids.ClassicAPI.GetContainerItemID(bag, slot)
if itemId then
local _, count = GetContainerItemInfo(bag, slot)
if itemId then
bags[bag][slot] = {
itemId = tonumber(itemId),
stackCount = count or 1,
}
end
bags[bag][slot] = {
itemId = itemId,
stackCount = count or 1,
}
end
end
end
@@ -1609,16 +1594,13 @@ function API.GetBagItem(bagIndex, slot)
end
-- Fallback: manual lookup
local link = GetContainerItemLink(bagIndex, slot)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
local itemId = CleveRoids.ClassicAPI.GetContainerItemID(bagIndex, slot)
if itemId then
local _, count = GetContainerItemInfo(bagIndex, slot)
if itemId then
return {
itemId = tonumber(itemId),
stackCount = count or 1,
}
end
return {
itemId = itemId,
stackCount = count or 1,
}
end
return nil
@@ -1690,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
@@ -1779,14 +1761,13 @@ function API.FindBagItem(itemIdOrName)
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag) or 0
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
if checkId then
local _, _, currentId = string.find(link, "item:(%d+)")
if currentId and tonumber(currentId) == checkId then
return bag, slot
end
elseif checkName then
if checkId then
if CleveRoids.ClassicAPI.GetContainerItemID(bag, slot) == checkId then
return bag, slot
end
elseif checkName then
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, currentName = string.find(link, "|h%[(.-)%]|h")
if currentName and string.lower(currentName) == checkName then
return bag, slot
@@ -2103,26 +2084,28 @@ function API.GetTrinkets(copy)
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag) or 0
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
local _, _, itemId = string.find(link, "item:(%d+)")
if itemId then
local numItemId = tonumber(itemId)
local invType = API.GetItemInventoryType(numItemId)
if invType == 12 then -- Trinket
local _, _, name = string.find(link, "|h%[(.-)%]|h")
local texture = GetContainerItemInfo(bag, slot)
local itemLevel = API.GetItemLevel(numItemId)
trinkets[index] = {
itemId = numItemId,
trinketName = name or "Unknown",
texture = texture,
itemLevel = itemLevel,
bagIndex = bag,
slotIndex = slot,
}
index = index + 1
local numItemId = CleveRoids.ClassicAPI.GetContainerItemID(bag, slot)
if numItemId then
local invType = API.GetItemInventoryType(numItemId)
if invType == 12 then -- Trinket
-- Only build the link string for actual trinkets, to read the name.
local link = GetContainerItemLink(bag, slot)
local name
if link then
local _
_, _, name = string.find(link, "|h%[(.-)%]|h")
end
local texture = GetContainerItemInfo(bag, slot)
local itemLevel = API.GetItemLevel(numItemId)
trinkets[index] = {
itemId = numItemId,
trinketName = name or "Unknown",
texture = texture,
itemLevel = itemLevel,
bagIndex = bag,
slotIndex = slot,
}
index = index + 1
end
end
end
@@ -2183,18 +2166,13 @@ function API.GetTrinketCooldown(slot)
end
-- Get item ID from equipped slot
local link = GetInventoryItemLink("player", equipSlot)
if not link then
return -1
end
local _, _, itemId = string.find(link, "item:(%d+)")
local itemId = CleveRoids.ClassicAPI.GetInventoryItemID("player", equipSlot)
if not itemId then
return -1
end
-- Get cooldown info
return API.GetItemCooldownInfo(tonumber(itemId))
return API.GetItemCooldownInfo(itemId)
end
-- Use an equipped trinket
@@ -3406,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.
@@ -3523,144 +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
--------------------------------------------------------------------------------
-- RELIQUARY DBC FUNCTIONS (optional DLL)
--------------------------------------------------------------------------------
-- Safe Reliquary call wrapper — returns nil on missing DLL or lookup failure
local function RQ_SafeCall(func, ...)
if not func then return nil end
local ok, result = pcall(func, unpack(arg))
if ok then return result end
return nil
end
-- Get item set data from DBC by set ID
-- Returns: { name, itemId_1..17, setSpellId_1..8, setThreshold_1..8 } or nil
function API.GetItemSet(setId)
if not setId or not _G.RQ_GetItemSet then return nil end
return RQ_SafeCall(_G.RQ_GetItemSet, setId)
end
-- Get the set ID for an item (via Nampower's GetItemStatsField)
-- Returns: setId (number) or nil if item has no set
function API.GetItemSetId(itemId)
if not itemId then return nil end
local setId = API.GetItemField(itemId, "itemSet")
if setId and setId ~= 0 then return setId end
return nil
end
-- Get all item IDs belonging to a set (from Reliquary DBC)
-- Returns: { itemId1, itemId2, ... } or nil
function API.GetItemSetItems(setId)
local setData = API.GetItemSet(setId)
if not setData then return nil end
local items = {}
for i = 1, 17 do
local id = setData["itemId_" .. i]
if id then
id = tonumber(id)
if id and id ~= 0 then
table.insert(items, id)
end
end
end
if table.getn(items) > 0 then return items end
return nil
end
-- Get set bonus spell/threshold pairs from DBC
-- Returns: { { spellId = N, threshold = N }, ... } or nil
function API.GetItemSetBonuses(setId)
local setData = API.GetItemSet(setId)
if not setData then return nil end
local bonuses = {}
for i = 1, 8 do
local spellId = setData["setSpellId_" .. i]
local threshold = setData["setThreshold_" .. i]
if spellId and threshold then
spellId = tonumber(spellId)
threshold = tonumber(threshold)
if spellId and spellId ~= 0 and threshold and threshold > 0 then
table.insert(bonuses, { spellId = spellId, threshold = threshold })
end
end
end
if table.getn(bonuses) > 0 then return bonuses end
return nil
end
-- Get spell effect radius in yards from DBC
-- spellId: the spell to look up
-- effectIndex: 1, 2, or 3 (which effect slot, default 1)
-- Returns: radius (number) or nil
function API.GetSpellEffectRadius(spellId, effectIndex)
effectIndex = effectIndex or 1
if not spellId then return nil end
-- Get the radius index from the spell's effect via Nampower
local radiusField = "effectRadiusIndex"
local rec = API.GetSpellRecord(spellId)
if not rec then return nil end
-- effectRadiusIndex is an array field — access by effect index
local radiusIndex = nil
if rec.effectRadiusIndex then
if type(rec.effectRadiusIndex) == "table" then
radiusIndex = rec.effectRadiusIndex[effectIndex]
else
-- Single value (effect 1 only)
if effectIndex == 1 then
radiusIndex = rec.effectRadiusIndex
end
end
end
if not radiusIndex or radiusIndex == 0 then return nil end
-- Try Reliquary for the SpellRadius DBC lookup
if _G.RQ_GetSpellRadius then
local radiusData = RQ_SafeCall(_G.RQ_GetSpellRadius, radiusIndex)
if radiusData and radiusData.radius then
local radius = tonumber(radiusData.radius)
if radius and radius > 0 then return radius end
end
end
return nil
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+3 -2
View File
@@ -10,7 +10,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
|-----|:--------:|---------|
| [Nampower](https://github.com/brues-code/nampower/releases) (v3.0.0+) | ✅ | Spell queueing, DBC data, auto-attack events |
| [UnitXP_SP3](https://codeberg.org/konaka/UnitXP_SP3/releases) | ✅ | Distance checks, `[multiscan]` enemy scanning |
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed |
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) (v1.15.8+) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed, unit-filtered events |
## Installation
@@ -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
+626 -1825
View File
File diff suppressed because it is too large Load Diff
+188
View File
@@ -0,0 +1,188 @@
# CC, Learned Immunity and Diminishing Returns
How SuperCleveRoidMacros classifies crowd control, what it learns about NPC
immunity, and why diminishing returns only ever acts as a brake on that
learning. Source citations are into `C:\Git\vmangos` and `C:\Git\nampower`.
## Three identities, never conflated
```text
ccType Exact DBC mechanic. Backs [cc:*] and CC landing verification.
Conditionals.lua, CleveRoids.CCMechanics
immunityType Exact DBC mechanic, recorded against an NPC name when a CC
spell comes back IMMUNE.
Utility.lua, MECHANIC_TO_IMMUNITY_TYPE / GetSpellImmunityType
DR type The real Vanilla diminishing-return pool. Internal only.
Utility.lua, GetSpellImmunityDRType
```
The first two are the same taxonomy used for different purposes; the third is
a separate axis and is never exposed to macros. Folding any pair together is
what produced the bugs this design replaces: a target that resisted Gouge got
written down as stun-immune, and every later Kidney Shot was suppressed
against a target that had never resisted one.
## `[cc:*]` follows the DBC mechanic exactly
One conditional, one mechanic. Several names may alias a single mechanic, but
no name spans two:
```text
[cc:stun] -> mechanic 12 only
[cc:freeze] -> mechanic 13 only
[cc:knockout] -> mechanic 14 only
[cc:sap] -> mechanic 30 only
[cc:daze] -> mechanic 27 only
```
Bare `[cc]` and `[cc:any]` still mean "any loss-of-control effect" and are
driven by `CleveRoids.CCTypesLossOfControl`, which is deliberately independent
of the per-mechanic table. An aggregate does not require redefining the parts.
Current aliases:
```text
slow -> snare
disoriented -> disorient
grip -> fumble (mechanic 6 is DBC Fumble)
incap / incapacitate / incapacitated -> knockout (immunity input only)
```
Aliases exist so published macros keep working. They resolve to the canonical
mechanic name before anything is stored, so SavedVariables only ever hold
canonical keys.
## Vanilla DBC mechanic taxonomy
The identities SCRM names. Where a mechanic is named at all, it is named as
itself:
| DBC mechanic | ID | Canonical `ccType` |
|---|---:|---|
| Charm | 1 | `charm` |
| Disoriented | 2 | `disorient` |
| Disarm | 3 | `disarm` |
| Distract | 4 | `distract` |
| Fear | 5 | `fear` |
| Fumble | 6 | `fumble` |
| Root | 7 | `root` |
| Pacify | 8 | `pacify` |
| Silence | 9 | `silence` |
| Sleep | 10 | `sleep` |
| Snare | 11 | `snare` |
| Stun | 12 | `stun` |
| Freeze | 13 | `freeze` |
| Knockout | 14 | `knockout` |
| Bleed | 15 | `bleed` |
| Polymorph | 17 | `polymorph` |
| Banish | 18 | `banish` |
| Shackle | 20 | `shackle` |
| Turn | 23 | `turn` |
| Horror | 24 | `horror` |
| Interrupt | 26 | `interrupt` |
| Daze | 27 | `daze` |
| Sapped | 30 | `sap` |
Mechanics 16, 19, 21, 22, 25, 28 and 29 (Bandage, Shield, Mount, Persuade,
Invulnerability, Discovery, Immune Shield) are real DBC values but are not
crowd control, so they get no conditional.
## Diminishing returns
DR matters here for exactly one reason: a target at DR level 4 returns
`IMMUNE`, and that must not be recorded as permanent immunity. Nothing else in
the addon consumes DR state, and there is no player-facing DR feature.
### Only three pools can diminish a creature
`GetDiminishingReturnsGroupType` in
`vmangos/src/game/Spells/SpellEntry.h:52-79` returns `DRTYPE_ALL` for exactly
three groups:
```text
DIMINISHING_CONTROL_STUN
DIMINISHING_TRIGGER_STUN
DIMINISHING_KIDNEYSHOT
```
Every other staged group — sleep, both roots, fear, Warlock fear, charm,
polymorph, silence, disarm, Death Coil, freeze, banish, knockout — is
`DRTYPE_PLAYER`, and `Unit.cpp:7881` applies those only when the victim is a
player. So on an NPC target they can never generate a DR immunity, and they
must never hold back immunity learning.
This is why `GetSpellImmunityDRType` returns nil for everything that is not
mechanic 12. It is not an approximation; it is the complete `DRTYPE_ALL` set.
### Classifying a stun into its pool
`SpellEntry::GetDiminishingReturnsGroup(bool triggered)`
(`vmangos/src/game/Spells/SpellEntry.cpp:281-432`) resolves the pool, and the
`triggered` argument means the runtime fact of whether an aura triggered the
cast — this is not a pure DBC property. SCRM reconstructs it from nampower
`SPELL_CAST_EVENT` correlation, which only fires for client-initiated casts:
a spell ID with a recent `CleveRoids.pendingCasts` entry was cast deliberately,
one without reached `SPELL_GO`/`SPELL_MISS` as a proc.
```text
Kidney Shot -> stun_kidneyshot
Charge / Intercept stun -> stun_control
other mechanic-12, client cast -> stun_control
other mechanic-12, proc -> stun_trigger
```
Kidney Shot is matched on rogue family bit 21 (`0x00200000`,
`SpellClassMask.h:228`) rather than a bare ID list, so custom ranks that keep
their DBC family data still classify correctly. Charge (7922) and Intercept
(20253, 20614, 20615) are internally triggered but explicitly returned as
controlled stun by `SpellEntry.cpp:389-399`; they are the only such exceptions
in 1.12.
### Pools that exist but do not apply
Recorded so nobody re-adds them to the safeguard. Knockout and Sap are
distinct mechanics that share `DIMINISHING_KNOCKOUT`
(`SpellEntry.cpp:424-425`), and Horror maps to `DIMINISHING_DEATHCOIL`
(`SpellEntry.cpp:428-429`). Both are `DRTYPE_PLAYER`. `DIMINISHING_LIMITONLY`
is a PvP duration cap rather than a staged pool, and `DIMINISHING_NONE` is not
a pool at all.
### The safeguard itself
`recentCCHits[targetGUID][drType]` counts landed CC per DR pool, so the three
stun pools keep separate histories. A hit more than `DR_RESET_WINDOW` (20s)
after the previous one in that pool restarts the count at 1, matching the DR
decay the skip check uses. Without that reset the counter only ever climbed,
so three stuns on a target permanently disqualified it from ever teaching the
addon anything again.
An `IMMUNE` result is treated as DR, and discarded, only while the pool holds
three or more hits inside the window. Otherwise it is learned.
## nampower `SPELL_MISS` arguments
`TriggerSpellMissEvent` (`nampower/spellevents.cpp:888-917`) signals both
`SPELL_MISS_SELF` and `SPELL_MISS_OTHER` with the same layout:
```text
arg1 = casterGuid arg2 = targetGuid arg3 = spellId arg4 = missInfo
```
`SPELL_MISS_SELF` carries `casterGuid` too, even though it is by definition
the player — the event is chosen by comparing the caster to the active player
GUID, not by changing the payload. Reading `arg1` as the spell ID hands a GUID
string to the immunity recorder and silently loses every miss.
## Known limitation: mouseover casts
`ProcessSpellMissSelf` refuses to learn permanent immunity when it cannot
resolve a queryable unit, because the temporary-immunity-buff check needs one
— an NPC under Divine Shield must not be recorded as permanently immune. A CC
cast at a mouseover that is not the current target can therefore return
`IMMUNE` without being learned.
This is the safeguard working as designed, not a known bug. Do not loosen it
without a reproduction showing a real failure, since the failure mode on the
other side is permanent bad data in `CleveRoids_ImmunityData`.
+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 +