Let's go back 5 years ago during the World of Warcraft beta. I was working on Cosmos UI, a projects that aimed to improve the World of Warcraft interface.
As interface modification was not officially supported by Blizzard, we went ahead and directly modify the game files written in Lua and XML. We provided the new edited files for the thousands of people using Cosmos.
Previous Method
Let's take a concrete example. We need to analyze the combat log in order to display a damage chart. The game calls the function ProcessChatMessage with all the required information.
function ProcessChatMessage(message, source, channel) { // ... Default Code ... } |
Note: World of Warcraft uses Lua as its programming language but I will use Javascript for this blog post as you, my readers, are most familiar with it. The technique remains exactly the same.
In order to process the combat log, we simply add some code at the beginning of the function in order to execute some action if the channel
is COMBAT_LOG_CHANNEL
. Nothing really extraordinary.
function ProcessChatMessage(message, source, channel) { if (channel == COMBAT_LOG_CHANNEL) { // ... Addon Code ... } // ... Default Code ... } |
However this solution is extremely intrusive as you have to change alter the code. Every time Blizzard releases a new version, we had to make a diff of all the files that changed and manually update all our code base. While we were doing this process, all the user of our interface could no longer play as they depended on us. This was an extremely stressful process on both parts.
This issue along with Blizzard servers being unstable helped start the meme Patch Day, No Play.
Hook Solution
I found out a non-intrusive way to alter the behavior of the game. With that technology unlocked, we started to modularize our codebase. Blizzard noticed and when our migration was nearly over, they added the ability to officially use modules (called Addons).
The technique is extremely simple. It only requires the language to have first class function. You store the function you want to alter in a variable (1). Then you override that function with yours (2), that will call the stored one to keep the default behavior (3).
// (1) Backup the function var defaultProcessChatMessage = ProcessChatMessage; // (2) Override the function ProcessChatMessage = function(message, source, channel) { if (channel == COMBAT_LOG_CHANNEL) { // ... Addon Code ... } // (3) Call Default Code defaultProcessChatMessage(message, source, channel); } |
This solution gives you a great control as you can add pre and post processing and even skip the old code if you need to. It works for most use-cases unless you have to modify what is inside of the function.
The technique has several important features:
- The target function does not need to be written in a special form. It does not require to support hooking and does not need to be registered as being hookable somewhere.
- Multiple hooks can be added independently. For example if another addon called VjeuxAddon wants to manage all the messages that start with
VjeuxAddon:
, you can just write the following code somewhere in the addon:var defaultProcessChatMessage = ProcessChatMessage; ProcessChatMessage = ProcessChatMessage(message, source, channel) { if (message.match(/^VjeuxAddon:/) { VjeuxAddon.processChatMessage(message, source, channel); } else { defaultProcessChatMessage(message, source, channel); } }
- The technique is decentralized. You don't have to have a system that coordinates all the hooks. Each addon sees only his own hooks.
- No library is required to use this technique.
Bonus Links
See these links for non-intrusive hooking techniques in other languages: