WoW:Detecting an instant cast spell
Explanation of SPELLCAST_STOP
Unfortunately until more information is exposed to us through events or the API, there isn't currently a very reliable manner to catch instant cast spells being cast by the player, for interaction with the UI. For a spell with a casting time, we get SPELLCAST_START with the spell name and the length of the cast time. When the spell is finished (successfully) casting, we get a SPELLCAST_STOP. In between we could get SPELLCAST_FAILED, SPELLCAST_INTERRUPTED, etc. which help us keep our states straight.
With an instant cast spell, we get one single SPELLCAST_STOP message, with no information (no spell name, no target, nothing). As a result any mod that tries to track something involving these instant cast spells has to guess and play around. I finished writing a Heal Tracking addon that me asures your healing efficiency, and for this purpose-- I needed those instant cast spells. This is the method I used to get them reliably.
Spellcasting functions and timeline
We have the following functions involved in the casting/targeting process:
- API_CastSpell - Called when we want to cast a spell from a specific spellbook
- API_CastSpellByName - Called when we want to cast a spell by name.
- API_UseAction - Called when we're clicking or using a binding to use an ActionButton
- API_SpellTargetUnit - Called when a spell is awaiting targeting, and we want to target a specific unit programatically
- API_TargetUnit - Called to change the physical target (in game) to a specific unit
- API_SpellStopTargeting - Stops the targeting for the current spell awaiting targeting
- API_CameraOrSelectOrMoveStart - Called when a spell is waiting targeting, and you click on the 3-D world.
Keep in mind when looking at this world, there are plenty of combinations that need to be considered when trying to detect instant cast spells. I try to take these into consideration with my hooks.
Implementing this system
Caveats
This code will grab every spell that is being cast as well as every action that is being used on the client side. Due to global cooldowns and other timing the overhead we incur here is very small, and I've done everything I can to make the code efficient and to ensure that the original functions are called as quickly as possible.
If you are interested in grabbing just a subset of instant cast functions, you can put them in a localized table and use that lookup to determine whether or not you change the MyMod_Spell variable.
Provide a custom tooltip
As much as I wish it wasn't necessary, we need some way to glean the spell information. This step is somewhat optional if you have your own tooltip to look this up, but I like my functions to be somewhat complete in providing accurate information. Let's create a tooltip in our XML file so we can use it later.
<GameTooltip name="MyMod_Tooltip" frameStrata="TOOLTIP" hidden="true" inherits="GameTooltipTemplate" parent="UIParent">
Variables
We could use some variables here to store information as we progress, so we'll define the following in the LUA body:
-- This will contain the spell that is waiting to be targeted MyMod_Spell = nil -- This will contain the spell that has been cast and targeted and is awaiting a SPELLCAST_STOP MyMod_EndCast = nil -- This contains the target of the current spell being casting MyMod_Target = nil
Hooking the casting functions
CastSpell
MyMod_oldCastSpell = CastSpell; function MyMod_newCastSpell(spellId, spellbookTabNum) -- Call the original function so there's no delay while we process MyMod_oldCastSpell(spellId, spellbookTabNum) -- Load the tooltip with the spell information MyMod_Tooltip:SetSpell(spellId, spellbookTabNum) local spellName = MyMod_TooltipTextLeft1:GetText() if SpellIsTargeting() then -- Spell is waiting for a target MyMod_Spell = spellName else -- Spell is being cast on the current target. -- If ClearTarget() had been called, we'd be waiting target MyMod_EndCast = spellName MyMod_Target = UnitName("target") end end CastSpell = MyMod_newCastSpell
CastSpellByName
MyMod_oldCastSpellByName = CastSpellByName; function MyMod_newCastSpellByName(spellName) -- Call the original function MyMod_oldCastSpellByName(spellName) -- This will give us the full spellname, including Rank. -- This can be filtered out quite easily local spellName = spellName if SpellIsTargeting() then -- Spell is waiting for a target MyMod_Spell = spellName else -- Spell is being cast on the current target MyMod_EndCast = spellName MyMod_Target = UnitName("target") end end CastSpellByName = MyMod_CastSpellByName
UseAction
MyMod_oldUseAction = UseAction function MyMod_newUseAction(a1, a2, a3) -- Call the original function MyMod_oldUseAction(a1, a2, a3) -- Test to see if this is a macro if GetActionText(a1) then return end MyMod_Tooltip:SetAction(a1) local spellName = MyMod_TooltipTextLeft1:GetText() if SpellIsTargeting() then -- Spell is waiting for a target MyMod_Spell = spellName else -- Spell is being cast on the current target MyMod_EndCast = spellName MyMod_Target = UnitName("target") end end UseAction = MyMod_newUseAction
Hooking the targeting functions
SpellTargetUnit
This one is nice, and provides us with a good framwork because of its limitations. Since SpellTargetUnit can ONLY be called after a spellcast has been issued, we can always be sure of who we're casting the spell on. Even if ClearTarget is used, it just triggers the need to target after casting.
MyMod_oldSpellTargetUnit = SpellTargetUnit function MyMod_newSpellTargetUnit(unit) -- Call the original function MyMod_oldSpellTargetUnit(unit) -- Look to see if we're currently waiting for a target internally -- If we are, then well glean the target info here. if MyMod_Spell then -- Currently casting.. lets grab the target MyMod_EndCast = MyMod_Spell MyMod_Target = UnitName(unit) -- Clear MyMod_Spell so we can wait for SPELLCAST_STOP MyMod_Spell = nil end end SpellTargetUnit = MyMod_newSpellTargetUnit
TargetUnit
This is the same code from SpellTargetUnit, with the names changed to protect the innocent.
MyMod_oldTargetUnit = TargetUnit function MyMod_newTargetUnit(unit) -- Call the original function MyMod_oldTargetUnit(unit) -- Look to see if we're currently waiting for a target internally -- If we are, then well glean the target info here. if MyMod_Spell then -- Currently casting.. lets grab the target MyMod_EndCast = MyMod_Spell MyMod_Target = UnitName(unit) -- Clear MyMod_Spell so we can wait for SPELLCAST_STOP MyMod_Spell = nil end end TargetUnit = MyMod_newTargetUnit
SpellStopTargeting
All we need to do here is clear MyMod_Spell to ensure we keep our state clean.
MyMod_SpellStopTargeting = SpellStopTargeting function MyMod_newSpellStopTargeting() MyMod_oldSpellStopTargeting() if MyMod_Spell then MyMod_Spell = nil MyMod_EndCast = nil MyMod_Target = nil end end SpellStopTargeting = MyMod_newSpellStopTargeting
CameraOrSelectOrMoveStart
This one is a little trickier.. we need to grab the information before we call the original function, so lets be quick about it:
MyMod_oldCameraOrSelectOrMoveStart = CameraOrSelectOrMoveStart function MyMod_newCameraOrSelectOrMoveStart() -- If we're waiting to target local targetName = nil if MyMod_Spell and UnitName("mouseover") then targetName = UnitName("mouseover") end MyMod_oldCameraOrSelectOrMoveStart(); if MyMod_Spell then MyMod_EndCast = MyMod_Spell MyMod_Target = targetName MyMod_Spell = nil end end CameraOrSelectOrMoveStart = MyMod_newCameraOrSelectOrMoveStart
Watching for SPELLCAST events
In your OnLoad handler, register the following events:
this:RegisterEvent("SPELLCAST_FAILED"); this:RegisterEvent("SPELLCAST_STOP"); this:RegisterEvent("SPELLCAST_INTERRUPTED");
In your OnEvent handler, add the following:
if event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then if MyMod_EndCast then MyMod_EndCast = nil MyMod_Target = nil MyMod_Spell = nil end -- This fires when a spell is completed casting, or you double escape a targeting spell elseif event == "SPELLCAST_STOP" then if MyMod_EndCast then -- This is where you implement your handling, and pull the SpellName and Target information MyMod_EndCast = nil MyMod_Target = nil MyMod_Spell = nil end