• 🏆 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] Inject main/config from WE trigger code (like JassHelper)

Level 19
Joined
Jan 3, 2022
Messages
320

Update:​

Use Bribe's library instead.

Motivation​

JassHelper has a feature that allows you to replace war3map.j's main() and config() functions, from its manual:

inject​

Certain advanced users might use the world editor yet prefer to have more control over the map script, namely making their own main or config functions, the inject preprocessors allows to replace such functions.
main() is called during map loading and initialization.
config() is called in the lobby, a popular example is lobby music. Because your entire triggers and code are placed BEFORE the config/main() functions war3map.lua, there's a little more work to do to override them.

Unlike JassHelper, my goal was to extend both main() - to emulate what vJass is doing with onInit, and config() to allow modifications yet without replacing it - allow WorldEdit to generate as much as possible. Smooth integration.

Code​

Lua:
-- version 1.0.0
--[[ injects two given functions into main() and config() similar to what JassHelper did

Injection point for main() is between the calls to InitBlizzard() and InitGlobals()

Injection point for config() is after its execution
]]
function luahelperInject(funcMain, funcConfig)
    if InitGlobals then
        local real_InitGlobals = InitGlobals
        function InitGlobals()
            funcMain()
            real_InitGlobals()
        end
    end
 
    local mt = getmetatable(_G) or {}
    mt.__newindex = function(tbl, key, val)
        if key == "config" then
            --[[ intercept config and put a wrapper around ]]
            rawset(tbl, key, function()
                val()
                funcConfig()
            end)
            --[[ remove this hook ]]
            mt.__newindex = nil
        else
            --[[ pass through ]]
            rawset(tbl, key, val)
        end
    end
    setmetatable(_G, mt)
end

In other words, save this code to a script.lua somewhere and run it from Lua console. This is just to demonstrate how it works in principle.
Lua:
#!/usr/bin/env lua

function a(t)
    print("t is: ", t)
    return t
end

--[[ injects two given functions into main() and config()
similar to what JassHelper did

Injection point for main() is between the calls to
InitBlizzard() and InitGlobals()

Injection point for config() is after its execution
]]
---
function luahelperInject(funcMain, funcConfig)
    if InitGlobals then
        local real_InitGlobals = InitGlobals
        function InitGlobals()
            funcMain()
            real_InitGlobals()
        end
    end
 
    local mt = getmetatable(_G) or {}
    mt.__newindex = function(tbl, key, val)
        if key == "config" then
            --[[ intercept config and put a wrapper around ]]
            rawset(tbl, key, function()
                val()
                funcConfig()
            end)
            --[[ remove this hook ]]
            mt.__newindex = nil
        else
            --[[ pass through ]]
            rawset(tbl, key, val)
        end
    end
    setmetatable(_G, mt)
end

function InitGlobals()
    print("Dummy Globals")
end

luahelperInject(
    function() print("MAIN INJECT") end,
    function() print("CONFIG INJECT") end
)

function b(x)
    print("x+1 is:", x+1)
    return x+1
end

function InitBlizzard()
    print("Dummy InitBlizzard")
end



function main()
    print("start of main")
    InitBlizzard()
    InitGlobals()
    print("end of main")
end

function config()
    print("start of config")
    print("end of config")
end

config()
main()
Lua:
do
    local function luahelperInject(funcMain, funcConfig)
        if InitGlobals then
            local real_InitGlobals = InitGlobals
            function InitGlobals()
                funcMain()
                real_InitGlobals()
            end
        end
  
        local mt = getmetatable(_G) or {}
        mt.__newindex = function(tbl, key, val)
            if key == "config" then
                --[[ intercept config and put a wrapper around ]]
                rawset(tbl, key, function()
                    val()
                    funcConfig()
                end)
                --[[ remove this hook ]]
                mt.__newindex = nil
            else
                --[[ pass through ]]
                rawset(tbl, key, val)
            end
        end
        setmetatable(_G, mt)
    end
    -- create our code functions for injection
    local inject_into_main = function()
        DidMainInject = "Yes, main() inject worked!"
    end
    local inject_into_config = function()
        DidConfigInject = "Yes, config() inject worked!"
    end
 
    -- inject and enjoy the sunset
    luahelperInject(inject_into_main, inject_into_config)
end

How to use​

Create a custom code item in Trigger Editor. Paste the code. Call the luahelperInject(func_into_main, func_into_config) function. First argument is a function to inject in main(), second argument is a function to inject in config().
If you only want to inject one of them, supply an empty function like luahelperInject(realFunctionIcreatedBefore, function() end)
It's preferable to put the created trigger at the very bottom of all triggers, because it intercepts the creation of all global variables until it finds config(), then does its job and disables itself.

Limitations​

You cannot use this function multiple times to add more than one function to the hook. Only use it once. I think it's simple, easy and useful as it is without complexity. EDIT: If someone wants to use it for a system as a library, tell me and I will add the functionality.

Overriden functions:​

Incompatibility: __newindex metatable of _G is used until it the config function is defined. __newindex is set to default nil after.
Override: InitGlobals, config. Your code is "injected" inside rather than replacing them.


PS: Alternative to my code above - @Bribe's Global Initialization + Hook libraries (see post #6 below)
 

Attachments

  • LuaInject-config-main-v1_0_0.w3m
    13.8 KB · Views: 16
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Aha, I guess if someone wanted to override Config, this would work. Why though, I'm not sure. In any case, is there a performance penalty to having an empty metatable assigned to the _G table? I would think that something like this would perform better, considering how operation-heavy it is to override the global index:

Lua:
    local mt = getmetatable(_G) or {}
    mt.__newindex = function(tbl, key, val)
        if key == "config" then
            --[[ intercept config and put a wrapper around ]]
            rawset(tbl, key, function()
                val()
                funcConfig()
            end)
            --[[ remove this hook ]]
            mt.__newindex = nil
            setmetatable(_G, nil) -- would this make a difference during runtime?
        else
            --[[ pass through ]]
            rawset(tbl, key, val)
        end
    end
    setmetatable(_G, mt)

Also, considering that this overrides the _G __newindex method, wouldn't it would prevent other libraries that want access to the same metamethod from working correctly alongside of it (provided they are assigned immediately rather than after one of these hooks)?
 
Last edited:
Level 19
Joined
Jan 3, 2022
Messages
320
The default metatable is nil, the above code would reset it. Yes you're correct about other libraries, I don't expect anything else to use __newindex (rather rare compared to __index) but I will change the wording in the post so nobody can miss that.
I thought about restoring the previous value of __newindex but that kind of override may silently cause improper functioning of the code that hooked there first.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
With Hook 3.6, you can do this without worrying about setmetatable conflicts, and it will leave _G's metatable intact or set it back to nil if necessary:

Lua:
do
    local ran
    local funcs = {}
    local mt
    local def
    local function func(tbl, key, val)
        if key == "config" then
            rawset(tbl, key, function()
                val()
                for _, userFunc in ipairs(funcs) do userFunc() end
            end)
            if Hook.remove("__newindex", func, nil, mt) == 0  and def then
                mt.__newindex = nil
                if ran == 2 and #mt == 0 then
                    setmetatable(_G, nil)
                end
            end
            return "skip hook"
        end
    end
    
    local function default(tbl, key, val)
        rawset(tbl, key, val)
    end
    
    function OnConfig(userFunc)
        if not ran then
            mt = getmetatable(_G)
            if not mt then
                mt = {}
                setmetatable(_G, mt)
                ran = 2
            else
                ran = 1
            end
            def = Hook.addSimple("__newindex", func, nil, mt, default) == default
        end
        funcs[#funcs + 1] = userFunc
    end
end

For hooking "main" the same way you mention, one would just use Global Initialization.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Using Hook 5.0, this resource can be reduced to:

Lua:
do
    --Requires Hook 5.0: https://www.hiveworkshop.com/threads/hook-v5-0.339153/
    
    local funcs
    
    function OnConfigInit(userFunc)
        if not funcs then
            funcs = {}
            local oldFunc, removeFunc
            oldFunc, removeFunc = AddHook("__newindex", func(tbl, key, val)
                if key == "config" then
                    rawset(tbl, key, function()
                        val()
                        for _, userFunc in ipairs(funcs) do userFunc() end
                    end)
                    removeFunc()
                else
                    oldFunc(tbl, key, val)
                end
            end, 1, _G, rawset, true)
        end
        funcs[#funcs + 1] = userFunc
    end
end

This leaves the metatable attached to _G, but considering how much I am pushing Global Variable Remapper forward, I would like to think that both the __index method and __newindex methods are likely to be hooked.
 
Top