|
|
(One intermediate revision by one other user not shown) |
Line 1: |
Line 1: |
| {{outdated}}
| | == Using UNIT_SPELLCAST_SUCCEEDED to track instant casts == |
| == Important note regarding 2.0 changes == | |
| This article is deprecated since the release of World of Warcraft version 2.0. It is no longer possible to hook the casting, action, targeting, or camera functions in addon code in the manner described below.
| |
|
| |
|
| Instead, an entire new set of UNIT_SPELLCAST events have been created that allow addons to detect spellcasting with complete accuracy and without requiring any function hooks.
| | The core event here is UNIT_SPELLCAST_SUCCEEDED (hereafter _SUCCEEDED). This event fires whenever a spellcast is completed server end, regardless of whether it is instant or cast. Therefore whenever an instant cast has been used successfully, this event will fire. However, there are times when _SUCCEEDED will fire but the spell is not instant. The events UNIT_SPELLCAST_START, UNIT_SPELLCAST_FAILED, and UNIT_SPELLCAST_INTERRUPTED (hereafter _START, _FAILED, and _INTERRUPTED) allow us to track precisely which spellcasts are instant and which are not. |
|
| |
|
| == Explanation of SPELLCAST_STOP ==
| | The key to this code is the relation between these events. Here are the relevant details: |
|
| |
|
| 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.
| | * All spells eventually trigger _SUCCEEDED if the cast completes, or _FAILED if it doesn't. |
| | * Only spells with cast times fire _START. |
| | * Every spell that starts must finish before another can begin; in other words, for every _START, we must hear '''finishing event''' (_SUCCEEDED, _FAILED or _INTERRUPTED) before we can hear another _START. |
|
| |
|
| 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.
| | Thus, to detect instant spells, note when someone starts] casting a spell, then watch for a corresponding finishing event. Any finishing event that occurs without an unfinished start event preceding it describes an instant cast spell. |
|
| |
|
| === Spellcasting functions and timeline === | | === Example code === |
|
| |
|
| We have the following functions involved in the casting/targeting process:
| | -- you should replace this with a better one |
| | | local print = function (msg) |
| * [[API_CastSpell]] - Called when we want to cast a spell from a specific spellbook
| | DEFAULT_CHAT_FRAME:AddMessage(msg) |
| * [[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. ''This function is blocked in 1.10.''
| |
| * OnMouseDown - Hook this event to detect clicks on the 3-D world. ([[XML_User_Interface#Event_Handler_reference]])
| |
| | |
| 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.
| |
| | |
| After the 1.10 patch, it is necessary to provide an anchor for all tooltips; otherwise they will not be filled in by the UI. See [[UIOBJECT_GameTooltip]] for additional information.
| |
| | |
| <GameTooltip name="MyMod_Tooltip" frameStrata="TOOLTIP" hidden="true"
| |
| inherits="GameTooltipTemplate" parent="UIParent">
| |
| <Scripts>
| |
| <OnLoad>
| |
| this:SetOwner(UIParent, "ANCHOR_NONE");
| |
| </OnLoad>
| |
| </Scripts>
| |
| </GameTooltip>
| |
| | |
| === 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 ====
| |
| Corrected syntax for 1.10 patch.
| |
| | |
| MyMod_oldCastSpellByName = CastSpellByName; | |
| function MyMod_newCastSpellByName(spellName, onSelf)
| |
| -- Call the original function
| |
| MyMod_oldCastSpellByName(spellName, onSelf)
| |
|
| |
| -- 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
| |
| if onSelf then
| |
| MyMod_Target = UnitName("player")
| |
| else
| |
| MyMod_Target = UnitName("target")
| |
| end
| |
| end
| |
| end
| |
| CastSpellByName = MyMod_newCastSpellByName
| |
| | |
| ==== 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 | | 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 we''ll 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 we''ll 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 ====
| | is_casting = false |
| | | SampleTrackerFunctions = { } |
| All we need to do here is clear MyMod_Spell to ensure we keep our state clean.
| |
| | |
| MyMod_oldSpellStopTargeting = 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 ====
| |
| | |
| After the 1.10 patch, CameraOrSelectOrMoveStart may no longer be hooked by custom UI mods. The below code is preserved for historical purposes, but will no longer work.
| |
| | |
| ----
| |
| | |
| 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 | |
| | |
| === Hooking the mouse click ===
| |
| In the 1.10 patch, CameraOrSelectOrMoveStart can no longer be hooked by the user interface. Attempting to call the hooked function will cause your addon to be reported as blocked by the game. The solution to this problem is to hook the mouse click itself. Since the user clicks on the screen to target a spell, all we have to do is add a call to our custom script when this occurs.
| |
| | |
| ==== OnMouseDown ====
| |
| | | |
| function myMouseDown()
| | SampleTracker = CreateFrame("Frame", nil, UIParent) |
| if arg1 == "LeftButton" then
| | SampleTracker:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") |
| local targetName;
| | SampleTracker:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") |
|
| | SampleTracker:RegisterEvent("UNIT_SPELLCAST_FAILED") |
| if MyMod_Spell and UnitExists("mouseover") then
| | SampleTracker:RegisterEvent("UNIT_SPELLCAST_START") |
| targetName = UnitName("mouseover")
| | SampleTracker:SetScript("OnEvent", function (_,e) SampleTrackerFunctions[e]() end) |
| end
| |
| | | |
| if MyMod_Spell and targetName then
| | ---- |
| MyMod_EndCast = MyMod_Spell
| | function SampleTrackerFunctions.UNIT_SPELLCAST_SUCCEEDED() |
| MyMod_Target = targetName
| | if not is_casting then |
| MyMod_Spell = nil
| | print("An instant spell, oh my!") |
| end
| | end |
| end
| | is_casting = false |
| end | | end |
| | | ---- |
| local oldFunc = WorldFrame:GetScript("OnMouseDown"); | | function SampleTrackerFunctions.UNIT_SPELLCAST_START() |
| if ( oldFunc ) then --hook the old function if one already exists | | is_casting = true |
| WorldFrame:SetScript("OnMouseDown", function() oldFunc(); myMouseDown(); end );
| | end |
| else
| | ---- |
| WorldFrame:SetScript("OnMouseDown", myMouseDown);
| | function SampleTrackerFunctions.UNIT_SPELLCAST_FAILED() |
| | is_casting = false |
| end | | end |
|
| |
|
| === Watching for SPELLCAST events === | | === Channeled Spells === |
| | | Note that channeled spells are actually considered two casts by the event engine: first, the '''preceding component''' spellcast, then '''channeling component''' itself. For clarity, consider Mind Control. The preceding component is the 3-second cast before the spell takes effect, and the channeling component is the actual mind control portion of the spell. Note that '''every channeled spell has a preceding component, though many of them are instant.''' |
| 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
| | In other words, don't be surprised when the sample code (or any code using this strategy) picks up abilities like Mind Vision or Drain Life as instant. Technically, there is an instant cast spell associated with such abilities. |
| 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
| |
|
| |
|
| [[Category: HOWTOs|Detect Instant Cast Spells]] | | [[Category: HOWTOs|Detect Instant Cast Spells]] |