WoW:Hooking functions

From AddOn Studio
Revision as of 19:18, 27 May 2008 by WoWWiki>Peeka
Jump to navigation Jump to search

In LUA, a function is simply a variable. From the LUA documentation:

The statement
 function f () ... end
translates to
 f = function () ... end

That means that any function can be replaced by any other function via simple assignment. With this in mind, it becomes quite easy to "Hook", or attach your own function into a predefined function.

How to Hook a Function


Let's say we wanted to show the target's level instead of the skull for players and monsters much higher level than us. The function responsible for hiding the level is TargetFrame_CheckLevel(), so we need to hook that function to make it un-hide the level.

Let's assume our AddOn is named "MyAddOn", and that is has an OnLoad handler that gets called from its XML <OnLoad> event. In the Lua file we would have:

local MyAddOn_Orig_TargetFrame_CheckLevel;
 
function MyAddOn_OnLoad()
  MyAddOn_Orig_TargetFrame_CheckLevel = TargetFrame_CheckLevel;  -- Remember old function
  TargetFrame_CheckLevel = MyAddOn_TargetFrame_CheckLevel;       -- Hook in ours
end

So what the above does is that it remembers the reference to the original "TargetFrame_CheckLevel" in "MyAddOn_Orig_TargetFrame_CheckLevel". Then it puts our function in place of the old one, so that anyone calling TargetFrame_CheckLevel() is now actually invoking MyAddOn_TargetFrame_CheckLevel() instead.

The next step is to create our MyAddOn_TargetFrame_CheckLevel(). Let's assume we want it to show target levels.

function MyAddOn_TargetFrame_CheckLevel()
  local retval = MyAddOn_Orig_TargetFrame_CheckLevel();  -- Call original function!
  TargetLevelText:Show();
  TargetHighLevelTexture:Hide();
  return retval;
end

So in this function, we first invoke the old function to let it do what it needs to do. Next, un-hide the level and hide the skull. Pretty simple right?

This is only an example for how to hook, it will not actually show you the level anymore. -- Shadowed

However, the above can be simplified even further if you plan to outright replace that code. To do so, you could use the following:

function MyAddOn_OnLoad()
   OldFunction = NewFunction;
end

Hooking object:method()

To hook "object:method()" to your own function "myfunction()", you'd hook it in the following manner:

object.method = myfunction;

An Easier Way to Hook a Function?

If you have the Sea library, then you can hook a function using Sea.util.hook.

Sea.util.hook("OldFunctionName", "NewFunctionName", "before|after|hide|replace"); 

If you specify replace, then the old function will only be called if the new function returns true.

If you use Sea.util.hook, then you're also able to remove the hook later with Sea.util.unhook.

Sea.util.unhook("OldFunctionName, "NewFunctionName");

Using Sea.util.hook will take care of parameter passing, priorities, chains and making sure you can clean yourself up later.

Optionally using Sea

If you don't want your AddOn to depend on Sea, but you would like to take advantage of it when it's available, you can check for its presence:

 local MyAddOn_Old_FunctionToHook = function() end;
 if Sea then
   Sea.util.hook("FunctionToHook", "ReplacementFunction", "after");
 else
   MyAddOn_Old_FunctionToHook = FunctionToHook;
   FunctionToHook = ReplacementFunction;
 end
 
 function ReplacementFunction()
   MyAddOn_Old_FunctionToHook();
   ...
 end

This allows you to list Sea as an OptionalDep in your TOC. This can help prevent future conflicts with other AddOns that replace the old function entirely, as long as the user installs Sea.

Other Hooking libraries

Stubby

With Stubby you can hook functions with Stubby.HookFunction

 Stubby.RegisterFunctionHook("FunctionToHook", position, replacementFunction [, hook1, hook2, .., hookN])
  • The FunctionToHook is a string that names the function you want to hook into. eg: "GameTooltip.SetOwner"
  • The position is a negative or positive number that defines the actual calling order of the addon. The smaller or more negative the number, the earlier in the call sequence your hookFunction will be called, the larger the number, the later your hook will be called. The actual original (hooked) function is called at position 0, so if your addon is hooked at a negative position, your return values table (see below) will contain the return values of the previous function in Stubby's call chain.
  • You pass (by reference) your function that you wish called as replacementFunction. This function will be called with the following parameters: hookFunction(hookParams, returnValue, hook1, hook2 .. hookN)
    • hookParams is a table containing the additional parameters passed to the RegisterFunctionHook function (the "hook1, hook2, .., hookN" params)
    • returnValue is an array of the returned values of the function called before you in Stubby's call chain or nil if none.
    • hook1..hookN are the original parameters of the hooked function in the original order.
  • You can optionally return the following strings to Stubby for special handling cases:
    • "abort" Will abort the call chain (will not call any other functions scheduled after your own).
    • "killorig" Will not call the original function, but will continue the call chain.
    • "setparams" Will replace the function's call paramenters with the ones you specify in a list on your function's second return value.
    • "setreturn" will change the return values for the next function in Stubby's call chain with your with the ones you specify in a list on your function's second return value.


Ace

Ace2

Where to Call the Old Function

This can be tricky, and really depends on what you are writing. By default you should probably call the old function first, and then do whatever you need. However, in some instances, you will want to call the old function last, call it conditionally, or not even call it at all.

  • Note: Not calling the old function at all can have very interesting effects if another addon hooked it before you did. What you will be doing then, is not letting that addon see the function call. This is where hook libraries do help.

Functions that Take Arguments

If you hook a function that takes arguments, make sure that your new function takes the same arguments, and that it passes it on.

So for instance if we want to hook ActionButton_GetPagedID( id ) our function definition should be something like:

function New_ActionButton_GetPagedID( id )
    Old_ActionButton_GetPagedID( id );
    ...
end

Functions that Use Global Variables

Some functions use global variables, and might change these global variables during execution. For instance, GameTooltip_OnEvent() is one such function, in that it uses the global event variable, and changes it during the course of execution. In this case, it's important to make a copy of the variable before calling the old function. Something like:

function New_GameTooltip_OnEvent()
    local myEvent = event;
    Old_GameTooltip_OnEvent();
    if ( myEvent = ... ) then
        ...
    end
end

Hook Chains


This can be a little tricky, but it works. Let's say there are two separate addons, one that attaches the player tooltip to the mouse, and one that replaces ?? with the player's level. One addon is called "AnchorToolTip", and the other is called "ShowLevel"

If the code for AnchorToolTip is:

local Pre_AnchorToolTip_GameTooltip_OnEvent;
function AnchorToolTip_OnLoad()
    Pre_AnchorToolTip_GameTooltip_OnEvent = GameTooltip_OnEvent;
    GameTooltip_OnEvent = AnchorToolTip_GameTooltip_OnEvent;
end
function AnchorToolTip_GameTooltip_OnEvent()
    Pre_AnchorToolTip_GameTooltip_OnEvent();
    ...
end

And the code for ShowLevel is:

local Pre_ShowLevel_GameTooltip_OnEvent;
function ShowLevel_OnLoad()
    Pre_ShowLevel_GameTooltip_OnEvent = GameTooltip_OnEvent;
    GameTooltip_OnEvent = ShowLevel_GameTooltip_OnEvent;
end
function ShowLevel_GameTooltip_OnEvent()
    Pre_ShowLevel_GameTooltip_OnEvent();
    ...
end

This will actually work. However, let's say you only want to anchor the tooltip to the mouse when there the shift key is held down. A small innocent change like:

function AnchorToolTip_GameTooltip_OnEvent()
    if ( isShiftKeyDown() ) then
        Pre_AnchorToolTip_GameTooltip_OnEvent();
        ...
    end
end

May break the hook chain depending on which addon is loaded first. Thus, be very very careful when deciding when and where to call the old function. You're not only calling Blizzard's code, you might also be calling a entire chain of hook functions.

Notes

  • If you use a local variable to store the old function, its name matters less. But often in bigger addons, you may need to call it from several of your addon's files, and local variables won't work. Use a syntax that uniquely identifies your previous function reference, like:
Pre_addon_functionname   or
addon_Orig_functionname.
  • An alternative is to store the old function as a member of a global variable, a la MyAddOn.FunctionToBeHooked = FunctionToBeHooked; FunctionToBeHooked = function() ...
  • Warning! Blizzard has moved some pieces of the UI to be loaded on demand. The functions in those pieces cannot be hooked until they are loaded.