• πŸ† Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. πŸ”—Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Hook

Level 20
Joined
Jul 10, 2009
Messages
477
The new approach looks interesting. I think I like it, although the Hook:<name> syntax has unnecessary restrictions in comparison to AddHook (doesn't support priorities and host tables).

A few things to consider:
  1. You are using object-oriented style now, but it's unclear what your objects are. Doesn't feel good for the user to write self without understanding what that stands for.
  2. Traditionally, colon-notation is used for methods (on class objects), and dot-notation for library functions and class functions. I find it confusing that old and remove use different notation, although they belong to the same type.
  3. Maybe rename AddHook to Hook.add now where you have a class for it? You can keep AddHook for legacy reasons ofc.
  4. You could add a proper class-annotation with all methods available (add, old, remove) to support the user.
  5. It's unclear to me how to use old and remove inside a normal AddHook.
  6. Hook(...) looks confusing (and even moreso after reading your description), what are reasons to use it? If you don't find an answer, probably better deprecate it.
  7. self.old sounds like I would call the original native, while in reality I call the next hook. Maybe self.next would be a more fitting name. Or maybe provide both?
  8. You seem to use rawset to write into the host table. Why is that? Host tables relying on __index and __newindex could be corrupted this way.
  9. AddHook returns self.old, which seems to be the predecessor of the new hook. The real predecessor however can change at any time (new hook added in between, predecessor removes itself, ...). What's the sense behind returning it anyway? Is the user supposed to do anything with it?
  10. You are using error to print an error message, which will only work in a protected call (but still crash the thread in a non-protected call without the user being notified). Maybe use print instead, if you just want to inform the user?
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Preview of v7.1 which addresses the problems poised by @Eikonium, and tries to solve the issue with Sumneko's syntax highlighting.

Lua:
if Debug then Debug.beginFile "Hook" end
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
-- Hook version 7.1 Preview
-- Created by: Bribe
-- Contributors: Eikonium, Jampion, MyPad, Wrda
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
---@class Hook
---@field next function|Hook            --Call the next/native function. Also works with any given name (old/native/original/etc.). The args and return values match those of the original function.
---@field remove fun(all?: boolean)     --Remove the hook. Pass the boolean "true" to remove all hooks.
---@field protected tree HookTree       --Reference to the tree storing each hook on that particular key in that particular host.
---@field private delete fun(h: Hook, all?: boolean) --Does not extend to child instances of Hook.
---@field package priority number
---@field package index integer
---@field package basic function|false
Hook = {}

---@class HookTree: { [integer]: Hook } --For some reason, the extension doesn't recognize Hook[] as correct syntax.
---@field host table
---@field key any                       --What was the function indexed to (_G items are typically indexed via strings)
---@field package basic function|false
do
    local mode_v = {__mode="v"}
    local treeMap = setmetatable({ [_G] = setmetatable({}, mode_v) }, mode_v) ---@type table<table,table<any,HookTree>>

    ---@param tree HookTree
    ---@param index integer
    ---@param new? table
    local function resizeTree(tree, index, new)
        table[new and "insert" or "remove"](tree, index, new)
        local top, prev = #tree, tree[index-1]
        for i=index, top do
            local h = tree[i]
            h.index = i
            h.next = (i > 1) and prev.basic or prev
            prev = h
        end
        local basic = tree[top].basic
        if basic then
            if tree.basic ~= basic then
                tree.basic = basic
                tree.host[tree.key] = basic
            end
        elseif tree.basic ~= false then
            tree.basic = false
            tree.host[tree.key] = function(...) return tree[#tree](...) end
        end
    end

    ---@param h Hook
    ---@param all? boolean
    function Hook.delete(h, all)
        local tree = h.tree
        h.tree = nil
        if (all==true) or (#tree == 1) then
            tree.host[tree.key] = (tree[0] ~= DoNothing) and tree[0] or nil
            treeMap[tree.host][tree.key] = nil
        else
            resizeTree(tree, h.index)
        end
    end

    local function getIndex(h, key) return (key == "remove") and function(all) Hook.delete(h, all) end or h.next end

    ---@param key        any                Usually a string (the name of the native you wish to hook)
    ---@param callback   fun(Hook, ...):any The function you want to run when the native is called. The first parameter is type "Hook", and the remaining parameters (and return value(s)) align with the original function.
    ---@param priority?  number             Defaults to 0. Hooks are called in order of highest priority down to lowest priority. The native itself has the lowest priority.
    ---@param host?      table              Defaults to _G (the table that stores all global variables).
    ---@param default?   function           If the native does not exist in the host table, use this default instead.
    ---@param metatable? boolean            Whether to store into the host's metatable instead. Defaults to true if the "default" parameter is given.
    ---@return Hook
    function Hook.add(key, callback, priority, host, default, metatable, basic)
        priority=priority or 0; host=host or _G
        if metatable or (default and metatable==nil) then
            host = getmetatable(host) or getmetatable(setmetatable(host, {}))
        end
        treeMap[host]=treeMap[host] or setmetatable({}, mode_v)
        local index, tree = 1, treeMap[host][key]
        if tree then
            local exit = #tree
            repeat if priority <= tree[index].priority then break end
            index=index+1 until index > exit
        else
            tree = { host=host, key=key, [0] = rawget(host, key) or default or (host ~= _G or type(key)~="string") and DoNothing or (Debug and Debug.error or print)("No value found for key: "..key) }
            treeMap[host][key] = tree
        end
        local new = { priority=priority, tree=tree, basic=callback } ---@type Hook
        if not basic then
            setmetatable(new, {__call=callback, __index=getIndex}).basic = false
        end
        resizeTree(tree, index, new)
        return new
    end
end

---@param k  any        Usually a string (the name of the native you wish to hook)
---@param c  function   The function you want to run when the native is called. The parameters and return values align with those of the original native.
---@param p? number     Defaults to 0. Hooks are called in order of highest priority down to lowest priority. The native itself has the lowest priority.
---@param h? table      Defaults to _G (the table that stores all global variables).
---@param d? function   If the native does not exist in the host table, use this default instead.
---@param m? boolean    Whether to store into the host's metatable instead. Defaults to true if the "default" parameter is given.
---@return fun(...):any next    Calls the next (lower-priority) hook or the native function.
---@return fun(all?) remove
AddHook = function(k,c,p,h,d,m) local new = Hook.add(k,c,p,h,d,m,true); return function(...) return new.next(...) end, new.remove end

setmetatable(Hook, { __newindex = function(_, key, callback) Hook.add(key, callback) end } )

if Debug then Debug.endFile() end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I execute a list of triggers for which I alter some native getters. What benefit I would have from using hook over using local backups and overwrittes for the native getters?
The problem that this solves is not intended for static hooks that persist throughout the game. However, if you un-hook any of those natives and set them back to what you had cached, AND there are other scripts that could be in the map that do the same to those natives, you run the risk that the cached natives will override each other. That's the problem that this solves.

The other stuff is just syntactical sugar.
 
The problem that this solves is not intended for static hooks that persist throughout the game. However, if you un-hook any of those natives and set them back to what you had cached, AND there are other scripts that could be in the map that do the same to those natives, you run the risk that the cached natives will override each other. That's the problem that this solves.

The other stuff is just syntactical sugar.
Sounds like I benefit from it. As I want to hook, then call a list of triggers and unhook again.

Ty, for your fast reply.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I'm finally ready to reveal the actual verison 7.1 for Hook here:

I've created unit tests to ensure things are working correctly:

I haven't unit tested how the hooks work in non-_G nor metatables yet, but I'll do that once I have refactored and begun testing Global Variable Remapper. I'll update the main Hook thread with whatever tried-and-true version of Hook that I end up with.

This actual Hook version 7.1 was almost a complete rewrite of the previous version. The API is still the same, but I had no idea what the hell it was doing by the time I started to work on it again. That's unnacceptable, so I apologize to all of you for making such a mess of it in the first place. This new version should be much more readable and much easier to follow, even if the length of the code has exploded.
 
While this library is great at what it does, like all of your resources of course, I regret to report that I had a terrible time trying to get this to work. Part of it was my own fault, because I copied the code from another map, which apparently had an outdated version, and I was reading the thread here and it made no sense with what I was seeing in the code. But after I fixed that, things didn't become much clearer.

I had to maneuver myself through multiple errors that didn't make any sense to me without having any documentation to help me.

This is something that's frustrating me with a lot of the resources in the Code section. Some of the greatest minds on this forum collaborate for years to make all of these resources. Please take the time to just write a few paragraphs in plain english what the user has to do to get them to work. Remove all the technical mumbo-jumbo from the description and don't assume any prior knowledge that only those that collaborated on these resources have.

Anyway, the hook works now. It's a great resource to have! Just was very frustrating to set up. :pcry:

Here is my complete journey:

First, I tried to create my own function and use the Hook.add method. Before I fixed the thing with the old version, I got a trying to index nil error, and after I fixed it, it got replaced by a "No value found for key" error. And... I have no idea what that means... Does that mean the native wasn't found? Does that mean my hook function wasn't found? I didn't misspell either of the functions, so I don't know what's going on.

So, I eventually figure out that the hook function has to have the same name as the native I'm trying to hook, because when I rename it, my previous error gets replaced by a new error! This time it's "attempt to call a table value". Again, I have no idea why that would happen; I'm doing exactly the thing that the example is telling me, as far as I can tell. And I have no way of finding out where my thinking is wrong.

Eventually I figure out that my hook works already if I just declare it as Hook:CreateDestructable and do nothing else, but again, that's not documented anywhere, and there is all this talk about priorities and the Hook.add method has a long parameter breakdown, so I'm of course assuming that that's what I'm supposed to be using. But this left me wondering what the point of this library is if I just declare a function and then someone else declares a function with the same name and overwrites mine. But apparently, if I declare two functions with the same name, they both get added. I have no idea how that works - I guess it's magic.

So, now my hooks work (I think!?), but I'm still left wondering why there is a Hook.add function and what it's supposed to do.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
First, I tried to create my own function and use the Hook.add method. Before I fixed the thing with the old version, I got a trying to index nil error, and after I fixed it, it got replaced by a "No value found for key" error. And... I have no idea what that means... Does that mean the native wasn't found? Does that mean my hook function wasn't found? I didn't misspell either of the functions, so I don't know what's going on.

This means you either mis-spelled the name of the native or that you provided it a parent table that didn't have the method. The parent table defaults to _G (the one that stores all the global variables).

So, I eventually figure out that the hook function has to have the same name as the native I'm trying to hook, because when I rename it, my previous error gets replaced by a new error! This time it's "attempt to call a table value". Again, I have no idea why that would happen; I'm doing exactly the thing that the example is telling me, as far as I can tell. And I have no way of finding out where my thinking is wrong. Eventually I figure out that my hook works already if I just declare it as Hook:CreateDestructable and do nothing else

That's basically a simplification. Hook:CreateDestructable means that your function will have the hook instance as an invisible 'self' parameter. So when you open up a function like that:
Lua:
function Hook:CreateDestructable()
    return self.old()
end

You can see this 'self' argument just appears out of nowhere. This is Lua's equivalent to other languages' 'this' argument. It is created because you used ':' rather than '.' to extend the Hook object when declaring the function, which is just Lua's syntactical sugar to make a method have a 'this' as its first argument.

but again, that's not documented anywhere, and there is all this talk about priorities and the Hook.add method has a long parameter breakdown, so I'm of course assuming that that's what I'm supposed to be using. But this left me wondering what the point of this library is if I just declare a function and then someone else declares a function with the same name and overwrites mine. But apparently, if I declare two functions with the same name, they both get added. I have no idea how that works - I guess it's magic. So, now my hooks work (I think!?), but I'm still left wondering why there is a Hook.add function and what it's supposed to do.

The way of defining a function with Hook:whatever is simply a shorthand way of calling Hook.add. The issue is knowing the parameters to use when calling Hook.add. Most of the parameters are optional - these are the ones that have a question mark (?) symbol in their Sumneko-Lua annotations. The optional parameters do not need to be passed, and my guess is that you tried to pass some parameters where you didn't need to, and ended up passing the wrong parameters as a consequence.

It is difficult to write documentation for a resource like this, because I wouldn't know how simple nor how complex to make it. If I make the documentation too complex, no one will read it. If I make it too simple, it won't cover enough details. The best thing I can recommend to understand resources like this one, is to first have a decent understanding of Sumneko's parameter documentation, which is based on the style of EmmyLua, which itself is inspired by JSDoc. Lua has optional parameters, unlike JASS, so you'll often encounter scenarios where you just don't need to provide all the complicated stuff that you used to have to with vJass.
 
Thank you for your reply!

Ok, so, I can then tell you exactly how I arrived at my error.

Hooking "BJDebugMsg" and, instead of allowing the original function to be called, simply call "print":
Lua:
Hook.add("BJDebugMsg", print)
Interpretation: This is what you need to do if you want to replace the function call. As an example, you give a Lua incorporated function.

A very simple timer recycling system:
Lua:
    local recycledTimers = {}
    function Hook:CreateTimer()
        if recycledTimers[1] then
            return table.remove(recycledTimers, #recycledTimers)
        else
            return self.old() --Call the CreateTimer native and return its timer
        end
    end
    function Hook:DestroyTimer(whichTimer)
        if #recycledTimers < 100 then
            table.insert(recycledTimers, whichTimer)
        else
            self.old(whichTimer) --This will not trigger recursively (but calling "DestroyTimer" again will cause recursion).
        end
    end
Interpretation: If you still want to call the original function, you have to define your hook function in this way. You need to do this on top of the Hook.add call.

It is not documented anywhere that this is an either or.

My hook function was called CAT_CreateDestructable, so what I did was:

Lua:
        function Hook:CAT_CreateDestructable(...)
            local newDestructable
            newDestructable = self.old(...)
            CreateDestructableActor(newDestructable, false)
            return newDestructable
        end

Hook.add(CreateDestructable, Hook.CAT_CreateDestructable)

I was under the impression that the Hook.add line generated this error because I didn't know just declaring the function in the table already creates the hook. Again, how am I supposed to know this? Also, I got the "no value found for key" error message. This is a really cryptic error and you could be much more descriptive. Something like "Function to replace not found (CAT_CreateDestructable)."

Finally, when I removed the CAT_ because that would make it align more with your provided example, I got the attempting to call table value error, because now the function declaration wasn't producing an error, but the Hook.add. The Hook:CreateDestructable still had self.old(...) call instead of just old(...), which would be correct.

After you've explained this, I still see no way in which I could have been able to know this. I've only started coding in Lua a few months ago, but I think I'm relatively confident in my understanding of Lua syntax (until it gets into the advanced stuff with coroutines and whatnot). And I don't think this is the issue here. I have no idea about the internals of your system, so doing self.old(...) instead of old(...) makes perfect sense if I assume that the function gets added to the class table.

It is difficult to write documentation for a resource like this, because I wouldn't know how simple nor how complex to make it. If I make the documentation too complex, no one will read it. If I make it too simple, it won't cover enough details. The best thing I can recommend to understand resources like this one, is to first have a decent understanding of Sumneko's parameter documentation, which is based on the style of EmmyLua, which itself is inspired by JSDoc. Lua has optional parameters, unlike JASS, so you'll often encounter scenarios where you just don't need to provide all the complicated stuff that you used to have to with vJass.

I believe you're entirely wrong here. If you write a good description, people who are on your level can skip it, and everyone else will appreciate it. You don't have to write essays like I like to do, but everything you just explained to me right now should be in a shortened form in the documentation.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Your code would have worked if you had done:
Lua:
local function CAT_CreateDestructable(self)
    local newDestructable = self.old()
    CreateDestructableActor(newDestructable, false)
    return newDestructable
end
Hook.add("CreateDestructable", CAT_CreateDestructable)

I will work on updating the docs.
 
Last edited:
Top