WoW:Hooking functions
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.
We create a new AddOn named ShowLevel and add an OnLoad handler in the XML. In the LUA file we have:
local Pre_ShowLevel_TargetFrame_CheckLevel; function ShowLevel_OnLoad() Pre_ShowLevel_TargetFrame_CheckLevel = TargetFrame_CheckLevel; TargetFrame_CheckLevel = ShowLevel_TargetFrame_CheckLevel; end
So what the above does is that it creates the Pre_ShowLevel_TargetFrame_CheckLevel function to hold the old function. Then it puts our function in place of the old one, so that anyone calling TargetFrame_CheckLevel() is now actually invoking ShowLevel_TargetFrame_CheckLevel() instead.
The next step is to create our ShowLevels_TargetFrame_CheckLevel()
function ShowLevels_TargetFrame_CheckLevel() Pre_ShowLevels_TargetFrame_CheckLevel(); TargetLevelText:Show(); TargetHighLevelTexture:Hide(); 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?
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.
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
- I'm not totally sure about scoping, but it's probably a good idea to uniquely name the variable where you store the old functionality. I tend to use a Pre_addon_function name syntax. Just don't use something like Old_function name
- 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.