WoW:Hooking functions

Revision as of 18:41, 16 July 2006 by WoWWiki>Shadowed (→‎How to Hook a Function: Added a note that the level showing no longer works and is just an example)

What a Function Is

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

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.

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 seperate 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.