12 Commits
Author SHA1 Message Date
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
5 changed files with 307 additions and 635 deletions
+41 -69
View File
@@ -273,24 +273,17 @@ local function BuildEquipmentCache()
end end
end end
-- Fallback: manual slot enumeration -- Fallback: manual slot enumeration via ClassicAPI (id + decorated name),
-- no link string built. C_Item.GetItemName carries random-suffix decoration
-- and falls back to the base name internally, so it replaces the old
-- bracket-name / GetItemInfo two-step in a single call.
for slot = 1, 19 do for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot) local id = GetInventoryItemID("player", slot)
if link then if id then
local _, _, id = string_find(link, "item:(%d+)") _equippedItemIDs[slot] = id
local _, _, nameInBrackets = string_find(link, "%[(.+)%]") local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if name then
if id then _equippedItemNames[slot] = string_lower(name)
_equippedItemIDs[slot] = tonumber(id)
end
if nameInBrackets then
_equippedItemNames[slot] = string_lower(nameInBrackets)
elseif id then
-- Fallback: resolve via GetItemInfo
local itemName = GetItemInfo(tonumber(id))
if itemName then
_equippedItemNames[slot] = string_lower(itemName)
end
end end
end end
end end
@@ -350,8 +343,7 @@ function CleveRoids.FindItemLocation(item)
if numericItem then if numericItem then
-- Check if it's an equipment slot (1-19) -- Check if it's an equipment slot (1-19)
if numericItem >= 1 and numericItem <= 19 then if numericItem >= 1 and numericItem <= 19 then
local link = GetInventoryItemLink("player", numericItem) if GetInventoryItemID("player", numericItem) then
if link then
return { type = "inventory", inventoryID = numericItem } return { type = "inventory", inventoryID = numericItem }
end end
return nil return nil
@@ -3466,11 +3458,8 @@ function CleveRoids.ValidateCooldown(args, ignoreGCD)
-- If this is a numeric slot (1-19), resolve to the equipped item's name -- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name) local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum) local itemName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if link then if itemName then name = itemName end
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then name = itemName end
end
end end
args = {name = name} args = {name = name}
else else
@@ -3481,11 +3470,8 @@ function CleveRoids.ValidateCooldown(args, ignoreGCD)
-- If this is a numeric slot (1-19), resolve to the equipped item's name -- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name) local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum) local itemName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if link then if itemName then name = itemName end
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then name = itemName end
end
end end
args.name = name args.name = name
else else
@@ -4869,13 +4855,11 @@ function CleveRoids.HasItem(item)
if type(item) == "string" and item ~= "" then if type(item) == "string" and item ~= "" then
local itemLower = string.lower(item) local itemLower = string.lower(item)
-- Check equipped slots for substring match -- Check equipped slots for substring match (decorated name, no link build)
for slot = 0, 19 do for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot) local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if link then if name and string.find(string.lower(name), itemLower, 1, true) then
if string.find(string.lower(link), itemLower, 1, true) then return true
return true
end
end end
end end
@@ -4884,11 +4868,9 @@ function CleveRoids.HasItem(item)
local size = GetContainerNumSlots(bag) local size = GetContainerNumSlots(bag)
if size and size > 0 then if size and size > 0 then
for slotIndex = 1, size do for slotIndex = 1, size do
local link = GetContainerItemLink(bag, slotIndex) local name = C_Item.GetItemName({ bagID = bag, slotIndex = slotIndex })
if link then if name and string.find(string.lower(name), itemLower, 1, true) then
if string.find(string.lower(link), itemLower, 1, true) then return true
return true
end
end end
end end
end end
@@ -4933,14 +4915,12 @@ function CleveRoids.GetItemCooldown(item)
local itemLower = string.lower(item) local itemLower = string.lower(item)
local start, dur, en local start, dur, en
-- Check equipped slots for substring match -- Check equipped slots for substring match (decorated name, no link build)
for slot = 0, 19 do for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot) local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if link then if name and string.find(string.lower(name), itemLower, 1, true) then
if string.find(string.lower(link), itemLower, 1, true) then start, dur, en = GetInventoryItemCooldown("player", slot)
start, dur, en = GetInventoryItemCooldown("player", slot) return _norm(start, dur, en)
return _norm(start, dur, en)
end
end end
end end
@@ -4949,12 +4929,10 @@ function CleveRoids.GetItemCooldown(item)
local size = GetContainerNumSlots(bag) local size = GetContainerNumSlots(bag)
if size and size > 0 then if size and size > 0 then
for slotIndex = 1, size do for slotIndex = 1, size do
local link = GetContainerItemLink(bag, slotIndex) local name = C_Item.GetItemName({ bagID = bag, slotIndex = slotIndex })
if link then if name and string.find(string.lower(name), itemLower, 1, true) then
if string.find(string.lower(link), itemLower, 1, true) then start, dur, en = GetContainerItemCooldown(bag, slotIndex)
start, dur, en = GetContainerItemCooldown(bag, slotIndex) return _norm(start, dur, en)
return _norm(start, dur, en)
end
end end
end end
end end
@@ -5887,13 +5865,10 @@ CleveRoids.Keywords = {
local itemName = name local itemName = name
local slotNum = tonumber(name) local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then if slotNum and slotNum >= 1 and slotNum <= 19 then
-- Resolve slot number to item name -- Resolve slot number to item name (decorated, no link build)
local link = GetInventoryItemLink("player", slotNum) local extractedName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if link then if extractedName then
local _, _, extractedName = string.find(link, "%[(.+)%]") itemName = extractedName
if extractedName then
itemName = extractedName
end
end end
end end
@@ -5925,13 +5900,10 @@ CleveRoids.Keywords = {
local itemName = name local itemName = name
local slotNum = tonumber(name) local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then if slotNum and slotNum >= 1 and slotNum <= 19 then
-- Resolve slot number to item name -- Resolve slot number to item name (decorated, no link build)
local link = GetInventoryItemLink("player", slotNum) local extractedName = C_Item.GetItemName({ equipmentSlotIndex = slotNum })
if link then if extractedName then
local _, _, extractedName = string.find(link, "%[(.+)%]") itemName = extractedName
if extractedName then
itemName = extractedName
end
end end
end end
+4
View File
@@ -67,6 +67,10 @@ SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
SLASH_CANCELFORM1 = "/cancelform"
SlashCmdList.CANCELFORM = CleveRoids.DoCancelForm
SLASH_UNQUEUE1 = "/unqueue" SLASH_UNQUEUE1 = "/unqueue"
SlashCmdList.UNQUEUE = SpellStopCasting SlashCmdList.UNQUEUE = SpellStopCasting
+192 -462
View File
@@ -46,10 +46,8 @@ CleveRoids.spellNameCache = {}
local GetTime = GetTime local GetTime = GetTime
local UnitExists = UnitExists local UnitExists = UnitExists
local UnitAffectingCombat = UnitAffectingCombat local UnitAffectingCombat = UnitAffectingCombat
local GetContainerItemLink = GetContainerItemLink
local GetContainerItemInfo = GetContainerItemInfo local GetContainerItemInfo = GetContainerItemInfo
local GetContainerNumSlots = GetContainerNumSlots local GetContainerNumSlots = GetContainerNumSlots
local GetInventoryItemLink = GetInventoryItemLink
local GetItemInfo = GetItemInfo local GetItemInfo = GetItemInfo
local PickupContainerItem = PickupContainerItem local PickupContainerItem = PickupContainerItem
local PickupInventoryItem = PickupInventoryItem local PickupInventoryItem = PickupInventoryItem
@@ -63,7 +61,6 @@ local ipairs = ipairs
local type = type local type = type
local tonumber = tonumber local tonumber = tonumber
local tostring = tostring local tostring = tostring
local string_find = string.find
local string_lower = string.lower local string_lower = string.lower
local string_gsub = string.gsub local string_gsub = string.gsub
local table_insert = table.insert local table_insert = table.insert
@@ -577,118 +574,34 @@ function CleveRoids.QueueActionUpdate()
end end
end end
local function _StripColor(s) -- Count of `itemId` in *bags only* (not bank), matched by itemID throughout --
if not s then return s end -- locale-independent, no hardcoded item names.
if string.sub(s,1,2) == "|c" then return string.sub(s,11,-3) end -- PERFORMANCE: uses the CleveRoids.Items cache (itemId -> name -> data) for an
return s -- O(1) lookup when available, falling back to a bag scan on a cache miss.
end function CleveRoids.GetReagentCount(itemId)
if not itemId then return 0 end
local _ReagentBySpell = {
["Vanish"] = "Flash Powder", -- 5140
["Blind"] = "Blinding Powder", -- 5530
}
-- Expose for use in Generic.lua (IndexSpells fallback)
CleveRoids.ReagentBySpell = _ReagentBySpell
-- Minimal map for rogue reagents; extend as needed
local _ReagentIdByName = {
["Flash Powder"] = 5140, -- Vanish
["Blinding Powder"] = 5530, -- Blind
}
-- Lazy bag-scan tooltip (only if we need to scan a bag slot by name)
local function CRM_GetBagScanTip()
local tip = _G.CleveRoidsBagScanTip
if tip then return tip end
local ok, created = pcall(CreateFrame, "GameTooltip", "CleveRoidsBagScanTip", UIParent, "GameTooltipTemplate")
if ok and created then
tip = created
else
tip = CreateFrame("GameTooltip", "CleveRoidsBagScanTip", UIParent)
local L1 = tip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
local R1 = tip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
tip:AddFontStrings(L1, R1)
for i=2,10 do
tip:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
tip:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
end
end
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
_G.CleveRoidsBagScanTip = tip
return tip
end
-- Return total in *bags only* (not bank), by id when possible; fallback to name
-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup when available
function CleveRoids.GetReagentCount(reagentName)
if not reagentName or reagentName == "" then return 0 end
-- Fast path: check cache first
local Items = CleveRoids.Items local Items = CleveRoids.Items
if Items then if Items then
local wantId = _ReagentIdByName[reagentName] local itemName = Items[itemId]
if itemName then
-- Try by ID first if we have a mapping local itemData = Items[itemName]
if wantId then if itemData and itemData.count and not itemData.inventoryID then
local itemName = Items[wantId] -- Item is in bags (not equipped), return cached count
if itemName then return itemData.count
local itemData = Items[itemName]
if itemData and itemData.count and not itemData.inventoryID then
-- Item is in bags (not equipped), return cached count
return itemData.count
end
end end
end end
-- Try by name
local itemData = Items[reagentName]
if type(itemData) == "string" then
itemData = Items[itemData] -- Resolve indirection
end
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
return itemData.count
end
-- Try lowercase
local lowerName = string.lower(reagentName)
local resolved = Items[lowerName]
if type(resolved) == "string" then
itemData = Items[resolved]
elseif type(resolved) == "table" then
itemData = resolved
end
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
return itemData.count
end
end end
-- Slow path fallback: full bag scan (only when cache miss) -- Slow path: scan bags by itemID (only on a cache miss)
local wantId = _ReagentIdByName[reagentName]
local total = 0 local total = 0
for bag = 0, 4 do for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0 local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do for slot = 1, slots do
local _, count = GetContainerItemInfo(bag, slot) -- Base itemID straight off the slot (no link string, no regex).
count = count or 0 if C_Container.GetContainerItemID(bag, slot) == itemId then
local link = GetContainerItemLink and GetContainerItemLink(bag, slot) local _, count = GetContainerItemInfo(bag, slot)
if link then total = total + (count or 0)
local _, _, idstr = string.find(link, "item:(%d+)")
local id = idstr and tonumber(idstr) or nil
if (wantId and id == wantId) or (not wantId and string.find(link, "%["..reagentName.."%]")) then
total = total + count
end
else
-- Fallback: scan bag slot tooltip for the name (expensive, only when no link)
local tip = CRM_GetBagScanTip()
tip:ClearLines()
tip:SetBagItem(bag, slot)
local left1 = _G[tip:GetName().."TextLeft1"]
local name = left1 and left1:GetText()
if name and name == reagentName then
total = total + count
end
end end
end end
end end
@@ -701,102 +614,55 @@ end
function CleveRoids.GetLiveItemCount(itemName) function CleveRoids.GetLiveItemCount(itemName)
if not itemName or itemName == "" then return 0 end if not itemName or itemName == "" then return 0 end
-- Escape Lua pattern special chars in item name to avoid crashes -- Escape Lua pattern special chars in item name to avoid crashes
local escaped = string.gsub(itemName, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") local wantLower = string.lower(itemName)
local pattern = "%[" .. escaped .. "%]"
local lowerPattern = string.lower(pattern)
local total = 0 local total = 0
for bag = 0, 4 do for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0 local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do for slot = 1, slots do
local link = GetContainerItemLink and GetContainerItemLink(bag, slot) local id = C_Container.GetContainerItemID(bag, slot)
if link and string.find(string.lower(link), lowerPattern) then if id then
local _, count = GetContainerItemInfo(bag, slot) local name = C_Item.GetItemNameByID(id)
total = total + (count or 0) if name and string.lower(name) == wantLower then
local _, count = GetContainerItemInfo(bag, slot)
total = total + (count or 0)
end
end end
end end
end end
return total return total
end end
function CleveRoids.GetSpellCost(spellSlot, bookType) -- Power cost and (first) reagent for a spellbook slot, read from Spell.dbc via
-- Fast path: existing fixed-slot read -- ClassicAPI -- no GameTooltip scan, no locale-dependent string parsing. Pass
CleveRoids.Frame:SetOwner(WorldFrame, "ANCHOR_NONE") -- spellId to skip the slot->id resolution when the caller already has it.
CleveRoids.Frame:SetSpell(spellSlot, bookType) -- cost: C_Spell.GetSpellPowerCost returns the *effective* (talent-modified)
-- cost the engine actually charges -- matches the old tooltip value,
local cost, reagent -- unlike GetSpellInfo().cost which is base-only.
local costText = CleveRoids.Frame.costFontString:GetText() -- reagent: C_Spell.GetSpellReagents returns { {itemID, count}, ... }; take the
if costText then -- first reagent's itemID (Spell.dbc covers reagent spells like Vanish).
_, _, cost = string.find(costText, "^(%d+)%s+[^yYsS]") -- Returns cost, reagentName (localized, for display), reagentId (for counting).
function CleveRoids.GetSpellCost(spellSlot, bookType, spellId)
if not spellId then
spellId = select(10, GetSpellInfo(spellSlot, bookType))
end end
local reagentText = CleveRoids.Frame.reagentFontString:GetText() local cost = 0
if reagentText then local reagent, reagentId
_, _, reagent = string.find(reagentText, "^Reagents?%s*:%s*(.*)")
end
reagent = _StripColor(reagent)
if reagent == "" then reagent = nil end
-- Fallback: scan all lines on a named tooltip (handles Vanish layout) if spellId then
if not reagent or not cost then local costs = C_Spell.GetSpellPowerCost(spellId)
local tip = CleveRoidsTooltipScan if costs and costs[1] and costs[1].cost then
if not tip then cost = costs[1].cost
-- belt & suspenders: create if somehow missing
local ok,_ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
if not ok or not CleveRoidsTooltipScan then
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
for i=2,32 do
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
end
end
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
tip = CleveRoidsTooltipScan
end end
tip:ClearLines() local reagents = C_Spell.GetSpellReagents(spellId)
tip:SetOwner(WorldFrame, "ANCHOR_NONE") if reagents and reagents[1] and reagents[1].itemID then
tip:SetSpell(spellSlot, bookType) reagentId = reagents[1].itemID
reagent = C_Item.GetItemNameByID(reagentId)
local base = tip:GetName() or "CleveRoidsTooltipScan"
local maxLines = (tip.NumLines and tip:NumLines()) or 32
for i = 1, maxLines do
local L = _G[base.."TextLeft"..i]
local R = _G[base.."TextRight"..i]
local lt = L and L:GetText() or ""
local rt = R and R:GetText() or ""
if not reagent then
if string.find(lt, "^[Rr]eagents?%s*:") then
reagent = _StripColor((rt ~= "" and rt) or (string.gsub(lt, "^[Rr]eagents?%s*:%s*", "")))
elseif string.find(rt, "^[Rr]eagents?%s*:") then
reagent = _StripColor((lt ~= "" and lt) or (string.gsub(rt, "^[Rr]eagents?%s*:%s*", "")))
end
if reagent == "" then reagent = nil end
end
if not cost and rt ~= "" then
local _, _, num = string.find(rt, "^(%d+)%s+(Mana|Energy|Rage|Focus)")
if num then cost = tonumber(num) end
end
if reagent and cost then break end
end end
end end
if not reagent or reagent == "" then return cost, (reagent and reagent ~= "" and tostring(reagent) or nil), reagentId
reagent = nil
local name = GetSpellName(spellSlot, bookType)
if name then
name = CleveRoids.StripRank(name)
reagent = _ReagentBySpell[name]
end
end
return (cost and tonumber(cost) or 0), (reagent and reagent ~= "" and tostring(reagent) or nil)
end end
function CleveRoids.GetProxyActionSlot(slot) function CleveRoids.GetProxyActionSlot(slot)
@@ -2689,11 +2555,13 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
CastSpellByName(castMsg) CastSpellByName(castMsg)
end end
else else
-- For other actions like UseContainerItem etc. -- For other actions like item use etc. Pass the resolved unit token so
-- item-use can target it directly (DoUse -> C_Item.UseItemByName); action
-- closures that only take (msg) simply ignore the extra arg.
if CleveRoids.equipDebugLog then if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r") CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r")
end end
action(msg) action(msg, conditionals.target)
end end
end end
@@ -3100,15 +2968,17 @@ end
-- PERFORMANCE: Module-level action to avoid closure allocation per call -- PERFORMANCE: Module-level action to avoid closure allocation per call
local function _startAttackAction() local function _startAttackAction()
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
-- Check both event-based flag AND action bar state for reliable detection -- Ground truth is the real action-bar state, NOT the cached autoAttack flag.
-- The flag can drift stale-true (e.g. set optimistically after AttackTarget below,
-- or a target dying without PLAYER_LEAVE_COMBAT firing), which would make us wrongly
-- believe we're already swinging and skip the attack. Use the ORIGINAL API here:
-- the overridden global IsCurrentAction just echoes the cached flag for the attack
-- slot, so it can't detect drift. Fall back to the flag only if the slot is unknown.
local isAttacking = CleveRoids.CurrentSpell.autoAttack local isAttacking = CleveRoids.CurrentSpell.autoAttack
if not isAttacking then local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
-- Fallback: check action bar state via IsCurrentAction if slot then
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack) isAttacking = CleveRoids.Hooks.IsCurrentAction(slot) and true or false
if slot and IsCurrentAction(slot) then CleveRoids.CurrentSpell.autoAttack = isAttacking
CleveRoids.CurrentSpell.autoAttack = true
isAttacking = true
end
end end
if not isAttacking and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then if not isAttacking and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then
CleveRoids.CurrentSpell.autoAttackLock = true CleveRoids.CurrentSpell.autoAttackLock = true
@@ -3194,6 +3064,36 @@ function CleveRoids.DoConditionalClearTarget(msg)
return false return false
end end
-- Resolve an EQUIPPED inventory slot (1-19) holding the item, or nil.
-- C_Item.UseItemByName only searches bags, so equipped-only items (trinkets,
-- weapons) must be fired via their slot. Prefer nampower's fast lookup; fall
-- back to a manual scan for clients without FindPlayerItemSlot (< v2.18).
local function FindEquippedItemSlot(msg, itemId)
local API = CleveRoids.NampowerAPI
if type(API) == "table" and API.FindItemFast then
local info = API.FindItemFast(itemId or msg)
if info and info.inventoryID then return info.inventoryID end
-- Found only in bags (or not at all) -> not an equipped-only item.
if info then return nil end
end
-- Fallback scan of equipped slots by ID or name, read straight from each
-- slot via ClassicAPI (GetInventoryItemID / C_Item.GetItemNameByID) -- no
-- link string built, no "item:"/"|h[..]|h" parse. Slots are 1-19.
local wantLower = (not itemId) and string_lower(msg) or nil
for slot = 1, 19 do
local id = GetInventoryItemID("player", slot)
if id then
if itemId then
if id == itemId then return slot end
else
local nm = C_Item.GetItemNameByID(id)
if nm and string_lower(nm) == wantLower then return slot end
end
end
end
return nil
end
-- Attempts to use or equip an item by a set of conditionals -- Attempts to use or equip an item by a set of conditionals
-- Also checks if a condition is a spell so that you can mix item and spell use -- Also checks if a condition is a spell so that you can mix item and spell use
-- msg: The raw message intercepted from a /use or /equip command -- msg: The raw message intercepted from a /use or /equip command
@@ -3205,184 +3105,42 @@ function CleveRoids.DoUse(msg)
local handled = false local handled = false
local action = function(msg) local action = function(msg, unit)
-- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor -- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor
if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end
if CursorHasItem and CursorHasItem() then ClearCursor() end if CursorHasItem and CursorHasItem() then ClearCursor() end
-- Try to interpret the message as a direct inventory slot ID first. -- Only pass a cast target when it actually exists, so a stale @unit doesn't
-- waste a consumable. Self-use items (potions/food/hearth) ignore it anyway.
local useUnit = (unit and UnitExists(unit)) and unit or nil
-- Direct equipped inventory slot ID (1-19): use it in place.
local slotId = tonumber(msg) local slotId = tonumber(msg)
if slotId and slotId >= 1 and slotId <= 19 then -- Character slots are 1-19 if slotId and slotId >= 1 and slotId <= 19 then
ClearCursor() -- extra safety before using equipped items ClearCursor() -- extra safety before using equipped items
UseInventoryItem(slotId) UseInventoryItem(slotId)
return return
end end
-- Try to interpret as item ID (numbers > 19) -- Equipped items (trinkets, weapons) live outside bags, so C_Item.UseItemByName
-- v2.18+: Use FindPlayerItemSlot directly for item IDs (no name resolution needed) -- can't reach them. Fire an equipped match through its slot first.
if slotId and slotId > 19 then local invSlot = FindEquippedItemSlot(msg, slotId)
local API = CleveRoids.NampowerAPI if invSlot then
-- v2.18+: Native lookup can find item directly by ID ClearCursor()
if API and API.features and API.features.hasFindPlayerItemSlot then if CleveRoids.equipDebugLog then
local itemInfo = API.FindItemFast(slotId) CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. invSlot .. ")|r")
if itemInfo then
ClearCursor()
if itemInfo.inventoryID then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 ID lookup]|r")
end
UseInventoryItem(itemInfo.inventoryID)
return
elseif itemInfo.bagID and itemInfo.slot then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 ID lookup]|r")
end
UseContainerItem(itemInfo.bagID, itemInfo.slot)
return
end
end
-- Item not found by ID - fail
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[UseLog] Item ID " .. slotId .. " not found in inventory [v2.18]|r")
end
return
end
-- Fallback: Resolve item ID to name for legacy lookup
local itemName = nil
if API and API.GetItemName then
itemName = API.GetItemName(slotId)
end
-- Fall back to GetItemInfo
if not itemName and GetItemInfo then
itemName = GetItemInfo(slotId)
end
if itemName then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] Resolved item ID " .. slotId .. " to '" .. itemName .. "'|r")
end
msg = itemName -- Replace ID with name for subsequent lookups
else
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[UseLog] Could not resolve item ID " .. slotId .. " - item not in cache|r")
end
-- Item not in client cache - can't resolve without seeing it first
return
end
end
-- v2.18+: Use native fast lookup (much faster than Lua cache + scan)
local API = CleveRoids.NampowerAPI
if API and API.features and API.features.hasFindPlayerItemSlot then
local itemInfo = API.FindItemFast(msg)
if itemInfo then
ClearCursor()
if itemInfo.inventoryID then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 native]|r")
end
UseInventoryItem(itemInfo.inventoryID)
return
elseif itemInfo.bagID and itemInfo.slot then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 native]|r")
end
UseContainerItem(itemInfo.bagID, itemInfo.slot)
return
end
end
-- v2.18 lookup didn't find item - fall through to legacy path
-- (might be partial match or different case that native doesn't handle)
end
-- PERFORMANCE: Try cache lookup first (O(1) instead of O(n) scan)
-- IMPORTANT: Validate cache hits to prevent stale data during combat
-- (IndexItems() is skipped during combat, so cache may have old bag/slot locations)
local location = CleveRoids.FindItemLocation(msg)
if location then
local cacheValid = false
local qname = string_lower(msg)
if location.type == "inventory" then
-- Validate: check if this slot actually contains the item we want
local link = GetInventoryItemLink("player", location.inventoryID)
if link then
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
if nm and string_lower(nm) == qname then
cacheValid = true
end
end
else
-- Validate: check if this bag slot actually contains the item we want
local link = GetContainerItemLink(location.bag, location.slot)
if link then
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
if nm and string_lower(nm) == qname then
cacheValid = true
end
end
end
if cacheValid then
ClearCursor()
if location.type == "inventory" then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. location.inventoryID .. ") [cached]|r")
end
UseInventoryItem(location.inventoryID)
else
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. location.bag .. "," .. location.slot .. ") [cached]|r")
end
UseContainerItem(location.bag, location.slot)
end
return
elseif CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - cache STALE, falling back to scan|r")
end
end
-- Slow path fallback: full scan for substring matches or cache miss
local qname = string_lower(msg)
-- Search equipped inventory slots first (for trinkets, etc.)
for slot = 0, 19 do
local link = GetInventoryItemLink("player", slot)
if link then
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
if nm and string_lower(nm) == qname then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. slot .. ")|r")
end
ClearCursor()
UseInventoryItem(slot)
return
end
end
end
-- Then search bags
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag) or 0
for bagSlot = 1, numSlots do
local link = GetContainerItemLink(bag, bagSlot)
if link then
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
if nm and string_lower(nm) == qname then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. bag .. "," .. bagSlot .. ")|r")
end
ClearCursor()
UseContainerItem(bag, bagSlot)
return
end
end
end end
UseInventoryItem(invSlot)
return
end end
-- Bag path: one ClassicAPI call finds the item in bags and dispatches by
-- type (potion/food/scroll/on-use), honoring `useUnit` for targeted-spell
-- items. itemIDs pass as numbers; names/links pass through unchanged.
if CleveRoids.equipDebugLog then if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - not found in equipped slots or bags|r") CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via C_Item.UseItemByName(" .. tostring(slotId or msg) .. ", " .. tostring(useUnit) .. ")|r")
end end
C_Item.UseItemByName(slotId or msg, useUnit)
end end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation -- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
@@ -3408,12 +3166,9 @@ local function FindItemInBagsByName(itemName)
for bag = 0, 4 do for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag) or 0 local numSlots = GetContainerNumSlots(bag) or 0
for slot = 1, numSlots do for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot) local name = C_Item.GetItemName({ bagID = bag, slotIndex = slot })
if link then if name and string_lower(name) == lowerName then
local _, _, name = string_find(link, "|h%[(.-)%]|h") return bag, slot
if name and string_lower(name) == lowerName then
return bag, slot
end
end end
end end
end end
@@ -3550,12 +3305,7 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
end end
-- Note what's currently in the target slot so we can invalidate its cache -- Note what's currently in the target slot so we can invalidate its cache
local oldSlotLink = GetInventoryItemLink("player", invslot) local oldSlotName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
local oldSlotName = nil
if oldSlotLink then
local _, _, name = string_find(oldSlotLink, "|h%[(.-)%]|h")
oldSlotName = name
end
-- Helper to invalidate displaced item's cache -- Helper to invalidate displaced item's cache
local function InvalidateDisplacedItem() local function InvalidateDisplacedItem()
@@ -3572,10 +3322,9 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11} local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11}
local checkSlot = pairedSlots[invslot] local checkSlot = pairedSlots[invslot]
if checkSlot then if checkSlot then
local link = GetInventoryItemLink("player", checkSlot) local slotItemName = C_Item.GetItemName({ equipmentSlotIndex = checkSlot })
if link then if slotItemName then
local _, _, slotItemName = string_find(link, "|h%[(.-)%]|h") if string_lower(slotItemName) == string_lower(msg) then
if slotItemName and string_lower(slotItemName) == string_lower(msg) then
-- Item found in paired slot - but prefer a bag copy if one exists -- Item found in paired slot - but prefer a bag copy if one exists
local bagCopyBag, bagCopySlot = FindItemInBagsByName(msg) local bagCopyBag, bagCopySlot = FindItemInBagsByName(msg)
if bagCopyBag then if bagCopyBag then
@@ -3627,17 +3376,14 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
-- Verify the item actually landed in the target slot -- Verify the item actually landed in the target slot
-- EquipItemByName may silently no-op for same-named items in paired slots -- EquipItemByName may silently no-op for same-named items in paired slots
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot) -- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
local newLink = GetInventoryItemLink("player", invslot) local newName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
if newLink then if newName and string_lower(newName) == string_lower(msg) then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h") if CleveRoids.Items then
if newName and string_lower(newName) == string_lower(msg) then CleveRoids.Items[msg] = nil
if CleveRoids.Items then CleveRoids.Items[string_lower(msg)] = nil
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end end
InvalidateDisplacedItem()
return true
end end
-- Verification failed - EquipItemByName didn't place item in target slot -- Verification failed - EquipItemByName didn't place item in target slot
if CleveRoids.equipDebugLog then if CleveRoids.equipDebugLog then
@@ -3679,10 +3425,9 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
local ok = pcall(EquipItemByName, item.name, invslot) local ok = pcall(EquipItemByName, item.name, invslot)
if ok then if ok then
-- Verify the item actually landed in the target slot (same guard as above) -- Verify the item actually landed in the target slot (same guard as above)
local newLink = GetInventoryItemLink("player", invslot) local newName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
if newLink then if newName then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h") if string_lower(newName) == string_lower(item.name) then
if newName and string_lower(newName) == string_lower(item.name) then
if CleveRoids.Items then if CleveRoids.Items then
CleveRoids.Items[item.name] = nil CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil CleveRoids.Items[string_lower(item.name)] = nil
@@ -3771,11 +3516,8 @@ local function _equipRing2Action(msg)
return CleveRoids.EquipBagItem(msg, 12) return CleveRoids.EquipBagItem(msg, 12)
end end
local function _unshiftAction() local function _cancelFormAction()
local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex() CancelShapeshiftForm()
if currentShapeshiftIndex ~= 0 then
CastShapeshiftForm(currentShapeshiftIndex)
end
end end
function CleveRoids.DoEquipMainhand(msg) function CleveRoids.DoEquipMainhand(msg)
@@ -3846,27 +3588,29 @@ function CleveRoids.DoEquipRing2(msg)
return false return false
end end
function CleveRoids.DoUnshift(msg) function CleveRoids.DoCancelForm(msg)
local handled local handled
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation -- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg) local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do for i = 1, table.getn(parts) do
handled = false handled = false
if CleveRoids.DoWithConditionals(parts[i], _unshiftAction, CleveRoids.FixEmptyTarget, false, _unshiftAction) then if CleveRoids.DoWithConditionals(parts[i], _cancelFormAction, CleveRoids.FixEmptyTarget, false, _cancelFormAction) then
handled = true handled = true
break break
end end
end end
if handled == nil then if handled == nil then
_unshiftAction() _cancelFormAction()
end end
return handled return handled
end end
CleveRoids.DoUnshift = CleveRoids.DoCancelForm
function CleveRoids.DoRetarget() function CleveRoids.DoRetarget()
if GetUnitName("target") == nil if UnitName("target") == nil
or UnitHealth("target") == 0 or UnitHealth("target") == 0
or not UnitCanAttack("player", "target") or not UnitCanAttack("player", "target")
then then
@@ -4191,6 +3935,17 @@ function CleveRoids.OnUpdate(self)
CR.lastUpdate = time CR.lastUpdate = time
-- LeafVillageAchievements/Legends also hook SendChatMessage and can bump ours
-- off the top of the chain (orphaning it / decorating #showtooltip). Reclaim
-- the top when they're present so #showtooltip stays filtered.
if not CR._watchChatHooks then
CR._watchChatHooks = (C_AddOns.IsAddOnLoaded("LeafVillageAchievements")
or C_AddOns.IsAddOnLoaded("LeafVillageLegends"))
end
if CR._watchChatHooks then
CR.EnsureSendChatMessageHook()
end
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED) -- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED)
-- PERFORMANCE: Skip check entirely if no pending update -- PERFORMANCE: Skip check entirely if no pending update
local pendingTime = CR.equipIndexPendingTime local pendingTime = CR.equipIndexPendingTime
@@ -4919,21 +4674,17 @@ function GetActionCount(slot)
count = CleveRoids.GetLiveItemCount(actionToCheck.item.name or actionToCheck.action) count = CleveRoids.GetLiveItemCount(actionToCheck.item.name or actionToCheck.action)
elseif actionToCheck.spell then elseif actionToCheck.spell then
local reagent = actionToCheck.spell.reagent local reagentId = actionToCheck.spell.reagentId
if not reagent or reagent == "" then if not reagentId then
reagent = nil
local ss, bt = actionToCheck.spell.spellSlot, actionToCheck.spell.bookType local ss, bt = actionToCheck.spell.spellSlot, actionToCheck.spell.bookType
if ss and bt then if ss and bt then
local _, r = CleveRoids.GetSpellCost(ss, bt) local _, _, rid = CleveRoids.GetSpellCost(ss, bt, actionToCheck.spell.id)
reagent = r actionToCheck.spell.reagentId = rid -- cache itemID so we don't re-derive every frame
reagentId = rid
end end
if (not reagent) and _ReagentBySpell and actionToCheck.spell.name then
reagent = _ReagentBySpell[actionToCheck.spell.name] -- e.g., Vanish → Flash Powder
end
actionToCheck.spell.reagent = reagent -- cache it so we don't re-scan every frame
end end
if reagent and reagent ~= "" then if reagentId then
count = CleveRoids.GetReagentCount(reagent) -- id-first bag scan, falls back to name/tooltip count = CleveRoids.GetReagentCount(reagentId) -- id-based bag scan
end end
end end
end end
@@ -4971,17 +4722,8 @@ function IsConsumableAction(slot)
end end
if actionToCheck.spell then if actionToCheck.spell and actionToCheck.spell.reagentId then
local reagent = actionToCheck.spell.reagent return 1
if (not reagent or reagent == "") and actionToCheck.spell.name then
reagent = _ReagentBySpell[actionToCheck.spell.name]
if reagent then
actionToCheck.spell.reagent = reagent
end
end
if reagent and reagent ~= "" then
return 1
end
end end
end end
@@ -5051,37 +4793,9 @@ if not CleveRoids.RunMacroHooked then
CleveRoids.RunMacroHooked = true CleveRoids.RunMacroHooked = true
end end
-- Robust named tooltip for scanning spells/items -- This single frame drives the addon's event loop and OnUpdate. (Spell cost /
if not CleveRoidsTooltipScan then -- reagent lookups now read Spell.dbc directly via ClassicAPI -- no tooltip scan.)
-- Try to create with the standard template first CleveRoids.Frame = CreateFrame("Frame")
local ok, _ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
if not ok or not CleveRoidsTooltipScan then
-- Fallback: manual tooltip with plenty of prebuilt lines
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
for i = 2, 32 do
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
end
end
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
CleveRoidsTooltipScan:EnableMouse(false)
end
-- This single dummy frame handles events AND serves as our tooltip scanner.
CleveRoids.Frame = CreateFrame("GameTooltip")
CleveRoids.Frame:EnableMouse(false) -- Prevent tooltip from capturing mouse input
-- Create the extra font strings needed for other functions like GetSpellCost.
CleveRoids.Frame.costFontString = CleveRoids.Frame:CreateFontString()
CleveRoids.Frame.rangeFontString = CleveRoids.Frame:CreateFontString()
CleveRoids.Frame.reagentFontString = CleveRoids.Frame:CreateFontString()
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.costFontString, CleveRoids.Frame.rangeFontString)
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.reagentFontString, CleveRoids.Frame:CreateFontString())
CleveRoids.Frame:SetScript("OnUpdate", CleveRoids.OnUpdate) CleveRoids.Frame:SetScript("OnUpdate", CleveRoids.OnUpdate)
CleveRoids.Frame:SetScript("OnEvent", function(...) CleveRoids.Frame:SetScript("OnEvent", function(...)
@@ -5261,8 +4975,7 @@ function CleveRoids.DoWDBWarmup()
for bag = 0, 4 do for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0 local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do for slot = 1, slots do
local link = GetContainerItemLink(bag, slot) if C_Container.GetContainerItemID(bag, slot) then
if link then
-- Tooltip scan loads the item into WDB -- Tooltip scan loads the item into WDB
tip:ClearLines() tip:ClearLines()
tip:SetBagItem(bag, slot) tip:SetBagItem(bag, slot)
@@ -5272,9 +4985,8 @@ function CleveRoids.DoWDBWarmup()
end end
-- Scan equipped items -- Scan equipped items
for slot = 0, 19 do for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot) if GetInventoryItemID("player", slot) then
if link then
tip:ClearLines() tip:ClearLines()
tip:SetInventoryItem("player", slot) tip:SetInventoryItem("player", slot)
scanned = scanned + 1 scanned = scanned + 1
@@ -6289,18 +6001,36 @@ function CleveRoids.Frame:KEY_UP()
CleveRoids.isActionUpdateQueued = true CleveRoids.isActionUpdateQueued = true
end end
CleveRoids.Hooks.SendChatMessage = SendChatMessage -- Base SendChatMessage captured at first hook; used to break a hook cycle.
function SendChatMessage(msg, ...) local baseSendChatMessage = SendChatMessage
-- Filter out #showtooltip lines local sendingChatMessage = false
-- pfUI's macrotweak also does this, but our pattern is more specific
-- Swallow #showtooltip lines Blizzard's macro executor sends to chat.
local function CleveRoids_SendChatMessage(msg, ...)
if msg and string.find(msg, "^#showtooltip") then if msg and string.find(msg, "^#showtooltip") then
return return
end end
if sendingChatMessage then
-- Call the original (or pfUI's hook if it's in the chain) -- Re-entered via a hook cycle; go straight to base to avoid recursion.
CleveRoids.Hooks.SendChatMessage(msg, unpack(arg)) return baseSendChatMessage(msg, unpack(arg))
end
sendingChatMessage = true
local ok, err = pcall(CleveRoids.Hooks.SendChatMessage, msg, unpack(arg))
sendingChatMessage = false
if not ok and geterrorhandler then geterrorhandler()(err) end
end end
-- Reassert our filter as the outermost SendChatMessage hook (no-op if already on
-- top). See the OnUpdate caller for why this is needed.
function CleveRoids.EnsureSendChatMessageHook()
if SendChatMessage ~= CleveRoids_SendChatMessage then
CleveRoids.Hooks.SendChatMessage = SendChatMessage
SendChatMessage = CleveRoids_SendChatMessage
end
end
CleveRoids.EnsureSendChatMessageHook()
CleveRoids.RegisterActionEventHandler = function(fn) CleveRoids.RegisterActionEventHandler = function(fn)
if type(fn) == "function" then if type(fn) == "function" then
table.insert(CleveRoids.actionEventHandlers, fn) table.insert(CleveRoids.actionEventHandlers, fn)
@@ -7327,9 +7057,9 @@ local function FindItemInBags(itemName)
local searchName = string.lower(itemName) local searchName = string.lower(itemName)
for bag = 0, 4 do for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot) local id = C_Container.GetContainerItemID(bag, slot)
if link then if id then
local _, _, foundName = string.find(link, "%[(.+)%]") local foundName = C_Item.GetItemNameByID(id)
if foundName and string.find(string.lower(foundName), searchName, 1, true) then if foundName and string.find(string.lower(foundName), searchName, 1, true) then
return bag, slot, foundName return bag, slot, foundName
end end
+69 -98
View File
@@ -58,11 +58,7 @@ function CleveRoids.IndexSpells()
bookType = CleveRoids.bookTypes[book] bookType = CleveRoids.bookTypes[book]
spells[bookType] = {} spells[bookType] = {}
else else
local cost, reagent = CleveRoids.GetSpellCost(i, bookType) local cost, reagent, reagentId = CleveRoids.GetSpellCost(i, bookType, spellId)
-- Fallback for known reagent spells if tooltip scan failed
if (not reagent or reagent == "") and CleveRoids.ReagentBySpell then
reagent = CleveRoids.ReagentBySpell[spellName]
end
if not spells[bookType][spellName] then if not spells[bookType][spellName] then
spells[bookType][spellName] = { spells[bookType][spellName] = {
spellSlot = i, spellSlot = i,
@@ -71,7 +67,7 @@ function CleveRoids.IndexSpells()
bookType = bookType, bookType = bookType,
texture = texture, texture = texture,
cost = cost, cost = cost,
reagent = reagent, reagentId = reagentId,
} }
end end
if spellRank and not spells[bookType][spellName][spellRank] then if spellRank and not spells[bookType][spellName][spellRank] then
@@ -83,7 +79,7 @@ function CleveRoids.IndexSpells()
bookType = bookType, bookType = bookType,
texture = texture, texture = texture,
cost = cost, cost = cost,
reagent = reagent reagentId = reagentId,
} }
spells[bookType][spellName].highest = spells[bookType][spellName][spellRank] spells[bookType][spellName].highest = spells[bookType][spellName][spellRank]
end end
@@ -94,6 +90,13 @@ function CleveRoids.IndexSpells()
if reagent then if reagent then
CleveRoids.countedItemTypes[reagent] = true 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 end
end end
@@ -156,10 +159,9 @@ end
function CleveRoids.IndexEquippedItems() function CleveRoids.IndexEquippedItems()
local items = CleveRoids.Items or {} local items = CleveRoids.Items or {}
for inventoryID = 0, 19 do for inventoryID = 1, 19 do
local link = GetInventoryItemLink("player", inventoryID) local itemID = GetInventoryItemID("player", inventoryID)
if link then if itemID then
local _, _, itemID = string.find(link, "item:(%d+)")
local name, link, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID) local name, link, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then if name then
local count = GetInventoryItemCount("player", inventoryID) local count = GetInventoryItemCount("player", inventoryID)
@@ -199,10 +201,9 @@ function CleveRoids.IndexEquipSlot(inventoryID)
if not inventoryID then return end if not inventoryID then return end
local items = CleveRoids.Items or {} local items = CleveRoids.Items or {}
local link = GetInventoryItemLink("player", inventoryID) local itemID = GetInventoryItemID("player", inventoryID)
if link then if itemID then
local _, _, itemID = string.find(link, "item:(%d+)")
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID) local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then if name then
local count = GetInventoryItemCount("player", inventoryID) local count = GetInventoryItemCount("player", inventoryID)
@@ -265,21 +266,17 @@ function CleveRoids.IndexItems()
-- PERFORMANCE: Local function references -- PERFORMANCE: Local function references
local GetContainerNumSlots = GetContainerNumSlots local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemLink = GetContainerItemLink
local GetContainerItemInfo = GetContainerItemInfo local GetContainerItemInfo = GetContainerItemInfo
local GetInventoryItemLink = GetInventoryItemLink
local GetInventoryItemCount = GetInventoryItemCount local GetInventoryItemCount = GetInventoryItemCount
-- Scan bags (reverse order to prefer first stack) -- Scan bags (reverse order to prefer first stack)
for bagID = 0, NUM_BAG_SLOTS do for bagID = 0, NUM_BAG_SLOTS do
local numSlots = GetContainerNumSlots(bagID) local numSlots = GetContainerNumSlots(bagID)
for slot = numSlots, 1, -1 do for slot = numSlots, 1, -1 do
local link = GetContainerItemLink(bagID, slot) local itemID = C_Container.GetContainerItemID(bagID, slot)
if link then if itemID then
local _, _, itemID = string_find(link, "item:(%d+)") -- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ bagID = bagID, slotIndex = slot })
-- PERFORMANCE: Try to extract name from link first to check for duplicates
local _, _, linkName = string_find(link, "%[(.+)%]")
local existing = linkName and items[linkName] local existing = linkName and items[linkName]
if existing then if existing then
@@ -317,13 +314,11 @@ function CleveRoids.IndexItems()
end end
-- Scan equipped items -- Scan equipped items
for inventoryID = 0, 19 do for inventoryID = 1, 19 do
local link = GetInventoryItemLink("player", inventoryID) local itemID = GetInventoryItemID("player", inventoryID)
if link then if itemID then
local _, _, itemID = string_find(link, "item:(%d+)") -- Decorated name for the duplicate fast-path (no link string built)
local linkName = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
-- PERFORMANCE: Try to extract name from link first
local _, _, linkName = string_find(link, "%[(.+)%]")
local existing = linkName and items[linkName] local existing = linkName and items[linkName]
if existing then if existing then
@@ -477,10 +472,8 @@ local function makeInventoryItem(inventoryID, link, Items)
if not link then link = GetInventoryItemLink("player", inventoryID) end if not link then link = GetInventoryItemLink("player", inventoryID) end
if not link then return end if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = GetInventoryItemID("player", inventoryID)
itemID = itemID and tonumber(itemID) or nil local name = C_Item.GetItemName({ equipmentSlotIndex = inventoryID })
local name = itemID and GetItemInfo(itemID) or nil
local texture = GetInventoryItemTexture("player", inventoryID) local texture = GetInventoryItemTexture("player", inventoryID)
local count = GetInventoryItemCount("player", inventoryID) local count = GetInventoryItemCount("player", inventoryID)
@@ -509,14 +502,12 @@ local function makeBagItem(bagID, slot, link, Items)
end end
if not link then return end if not link then return end
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = C_Container.GetContainerItemID(bagID, slot)
itemID = itemID and tonumber(itemID) or nil
local name, _, _, _, _, _, _, _, texture = GetItemInfo(itemID) local name = C_Item.GetItemName({bagID = bagID, slotIndex = slot})
local count = 0 local count = 0
local tex, itemCount = GetContainerItemInfo(bagID, slot) local texture, itemCount = GetContainerItemInfo(bagID, slot)
if itemCount then count = itemCount end if itemCount then count = itemCount end
if not texture then texture = tex end
local it = { local it = {
bagID = bagID, bagID = bagID,
@@ -574,9 +565,7 @@ function CleveRoids.GetItem(text)
for inv = 1, 19 do for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv) local link = GetInventoryItemLink("player", inv)
if link then if link then
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = GetInventoryItemID("player", inv)
itemID = itemID and tonumber(itemID) or nil
if qid and itemID and qid == itemID then if qid and itemID and qid == itemID then
return makeInventoryItem(inv, link, Items) return makeInventoryItem(inv, link, Items)
elseif qname then elseif qname then
@@ -595,9 +584,7 @@ function CleveRoids.GetItem(text)
for slot = 1, slots do for slot = 1, slots do
local link = GetContainerItemLink(bag, slot) local link = GetContainerItemLink(bag, slot)
if link then if link then
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = C_Container.GetContainerItemID(bag, slot)
itemID = itemID and tonumber(itemID) or nil
if qid and itemID and qid == itemID then if qid and itemID and qid == itemID then
return makeBagItem(bag, slot, link, Items) return makeBagItem(bag, slot, link, Items)
elseif qname then elseif qname then
@@ -720,18 +707,16 @@ function CleveRoids.FindItemQuick(text)
if cached then if cached then
-- Validate: check if item is actually at the cached location -- Validate: check if item is actually at the cached location
if cached.inventoryID then if cached.inventoryID then
local link = GetInventoryItemLink("player", cached.inventoryID) if qid then
if link then if GetInventoryItemID("player", cached.inventoryID) == qid then
local nm = GetNameFromLink(link) 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 if nm and qname and string_lower(nm) == qname then
cached._validated = true cached._validated = true
return cached -- Cache is valid 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
end end
-- Cache is stale - item not at cached equipped slot, invalidate -- Cache is stale - item not at cached equipped slot, invalidate
@@ -740,18 +725,16 @@ function CleveRoids.FindItemQuick(text)
Items[string_lower(cached.name)] = nil Items[string_lower(cached.name)] = nil
end end
elseif cached.bagID and cached.slot then elseif cached.bagID and cached.slot then
local link = GetContainerItemLink(cached.bagID, cached.slot) if qid then
if link then if C_Container.GetContainerItemID(cached.bagID, cached.slot) == qid then
local nm = GetNameFromLink(link) 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 if nm and qname and string_lower(nm) == qname then
cached._validated = true cached._validated = true
return cached -- Cache is valid 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
end end
-- Cache is stale - item not at cached bag slot, invalidate -- Cache is stale - item not at cached bag slot, invalidate
@@ -767,20 +750,17 @@ function CleveRoids.FindItemQuick(text)
for inv = 1, 19 do for inv = 1, 19 do
local link = GetInventoryItemLink("player", inv) local link = GetInventoryItemLink("player", inv)
if link then if link then
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = GetInventoryItemID("player", inv)
if itemID then -- ID match: fast path
itemID = tonumber(itemID) if qid and qid == itemID then
-- ID match: fast path return makeInventoryItem(inv, link, Items)
if qid and qid == itemID then 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) return makeInventoryItem(inv, link, Items)
end 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 end
end end
@@ -791,20 +771,17 @@ function CleveRoids.FindItemQuick(text)
for slot = 1, slots do for slot = 1, slots do
local link = GetContainerItemLink(bag, slot) local link = GetContainerItemLink(bag, slot)
if link then if link then
local _, _, itemID = string_find(link, "item:(%d+)") local itemID = C_Container.GetContainerItemID(bag, slot)
if itemID then -- ID match: fast path
itemID = tonumber(itemID) if qid and qid == itemID then
-- ID match: fast path return makeBagItem(bag, slot, link, Items)
if qid and qid == itemID then 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) return makeBagItem(bag, slot, link, Items)
end 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 end
end end
@@ -818,25 +795,19 @@ end
function CleveRoids.IsItemEquipped(text, inventoryId) function CleveRoids.IsItemEquipped(text, inventoryId)
if not text or not inventoryId then return false end if not text or not inventoryId then return false end
local link = GetInventoryItemLink("player", inventoryId) local currentID = GetInventoryItemID("player", inventoryId)
if not link then return false end
local _, _, currentID = string_find(link, "item:(%d+)")
if not currentID then return false end if not currentID then return false end
-- Check by ID (fast path) -- Check by ID (fast path)
local textId = tonumber(text) local textId = tonumber(text)
if textId and textId == tonumber(currentID) then if textId and textId == currentID then
return true return true
end end
-- Check by name - extract from link instead of GetItemInfo for performance -- Check by name (decorated, so suffixed gear still matches)
local currentName = GetNameFromLink(link) local currentName = C_Item.GetItemName({ equipmentSlotIndex = inventoryId })
if currentName then if currentName and string_lower(currentName) == string_lower(text) then
local textLower = string_lower(text) return true
if string_lower(currentName) == textLower then
return true
end
end end
return false return false
+1 -6
View File
@@ -6918,12 +6918,7 @@ function CleveRoids.ApplyEquipmentModifier(spellID, baseDuration)
local modifiedDuration = modifier.modifier(baseDuration, itemID) local modifiedDuration = modifier.modifier(baseDuration, itemID)
if modifiedDuration ~= baseDuration and CleveRoids.debug then if modifiedDuration ~= baseDuration and CleveRoids.debug then
local itemName = "Unknown" local itemName = C_Item.GetItemName({ equipmentSlotIndex = modifier.slot }) or "Unknown"
local itemLink = GetInventoryItemLink("player", modifier.slot)
if itemLink then
local _, _, _n = string.find(itemLink, "%[(.-)%]")
itemName = _n or "Unknown"
end
DEFAULT_CHAT_FRAME:AddMessage( DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Equipment Modifier]|r %s (ID:%d): %ds -> %ds (item: %s [%d])", string.format("|cffff00ff[Equipment Modifier]|r %s (ID:%d): %ds -> %ds (item: %s [%d])",