[Lua] Global Initialization

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
You can use this library to initialize your Lua scripts, rather than being restricted to GUI triggers to execute your code for you.

Lua:
do -- Global Initialization 3.0.0.1
   -- Special thanks to Tasyen, Forsakn and Troll-Brain
    
    local call = pcall
    if try then call = try end --Requires optional Error Console by Eikonium: https://www.hiveworkshop.com/threads/lua-debug-utils-ingame-console-etc.330758
    
    local PRINT_CAUGHT_ERRORS = true
    local errors = {}
 
    local gFuncs = {}
    local tFuncs = {}
    local iFuncs = {}
    local sFuncs = {}
 
    function OnGlobalInit(func) -- Runs once all variables are instantiated.
        gFuncs[#gFuncs+1] = func
    end
 
    function OnTrigInit(func) -- Runs once all InitTrig_ are called
        tFuncs[#tFuncs+1] = func
    end
 
    function OnMapInit(func) -- Runs once all Map Initialization triggers are run
        iFuncs[#iFuncs+1] = func
    end
 
    function OnGameStart(func) -- Runs once the game has actually started
        sFuncs[#sFuncs+1] = func
    end
 
    local function saveError(where, error)
        if not PRINT_CAUGHT_ERRORS then return end
        errors[#errors+1] = "Global Initialization: caught error in " .. where .. ": " .. error
    end
 
    local function printErrors()
        if #errors < 1 then return end
        print("|cffff0000Global Initialization: " .. #errors .. " errors occured during Initialization.|r")
        for _, error in ipairs(errors) do
            print(error)
        end
        errors = nil
    end
    
    local function run(list, name)
        for _, func in ipairs(list) do
            local result, error = call(func)
            if not result then
                saveError(name, error)
            end
        end
    end
 
    local function runInitialization()
        run(iFuncs, "OnMapInit")
        iFuncs = nil
    end
 
    local function runTriggerInit()
        run(tFuncs, "OnTrigInit")
        tFuncs = nil
 
        local old = RunInitializationTriggers
        if old then
            function RunInitializationTriggers()
                old()
                runInitialization()
            end
        else
            runInitialization()
        end
    end
 
    local function runGlobalInit()
        run(gFuncs, "OnGlobalInit")
        gFuncs = nil
 
        local old = InitCustomTriggers
        if old then
            function InitCustomTriggers()
                old()
                runTriggerInit()
            end
        else
            runTriggerInit()
        end
    end
 
    local oldBliz = InitBlizzard
    function InitBlizzard()
        oldBliz()
 
        local old = InitGlobals
        if old then
            function InitGlobals()
                old()
                runGlobalInit()
            end
        else
            runGlobalInit()
        end
 
        -- Start timer to run gamestart-functions and then print all caught errors if PRINT_CAUGHT_ERRORS
        TimerStart(CreateTimer(), 0.00, false, function()
            DestroyTimer(GetExpiredTimer())
 
            run(sFuncs, "OnGameStart")
            sFuncs = nil
            if PRINT_CAUGHT_ERRORS then
                printErrors()
            end
        end)
    end
end--End of Global Initialization

Examples

Lua:
OnGlobalInit(function() print "udg_ variables have been assigned" end)

OnTrigInit(function() print "gg_trg_ variables have been assigned" end)

OnMapInit(function() print "Map Initialization triggers have been run" end)

OnGameStart(function() print "The game has started" end)
 
Last edited:

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
I haven’t tested the below, but it should do the trick of adding an onGameStart event. I will update the primary resource in a couple weeks.

Lua:
--Global Initialization 1.2 adds an onGameStart function.
do
   local gFuncs = {}
   local tFuncs = {}
   local iFuncs = {}
   local sFuncs
  
   function onGlobalInit(func) --Runs once all variables are instantiated.
      gFuncs[func] = func --Simplification thanks to TheReviewer and Zed on Hive Discord
   end
  
   function onTriggerInit(func) -- Runs once all InitTrig_ are called
      tFuncs[func] = func
   end
  
   function onInitialization(func) -- Runs once all Map Initialization triggers are run
      iFuncs[func] = func
   end
  
   function onGameStart(func) --runs once the game has actually started
      if not sFuncs then
         sFuncs = {}
         TimerStart(CreateTimer(), 0.00, false, function()
            DestroyTimer(GetExpiredTimer())
            for k, f in pairs(sFuncs) do f() end
            sFuncs = nil
         end)
      end
      sFuncs[func] = func
   end

   local function runInitialization()
      for k, f in pairs(iFuncs) do f() end
      iFuncs = nil
   end
  
   local function runTriggerInit()
      for k, f in pairs(tFuncs) do f() end
      tFuncs = nil
      local old = RunInitializationTriggers
      if old then
         function RunInitializationTriggers()
            old()
            runInitialization()
         end
      else
         runInitialization()
      end
   end
  
    local function runGlobalInit()
        for k, f in pairs(gFuncs) do f() end
        gFuncs = nil
        local old = InitCustomTriggers
        if old then
            function InitCustomTriggers()
                old()
                runTriggerInit()
            end
        else
            runTriggerInit()
        end
    end
  
    local oldBliz = InitBlizzard
    function InitBlizzard()
        oldBliz()
        local old = InitGlobals
        if old then
            function InitGlobals()
                old()
                runGlobalInit()
            end
        else
            runGlobalInit()
        end
    end
end
 
How about an earlier entry Point? Like before items/Units are created, actualy pre unit creation is the more important one at least in my opinion. With that one could Setup trigger/Events also affecting preplaced Units like HeroSkills selected in Editor which would throw learning skills Events when that Event would be created before CreateAllUnits(), gaining items, beeing low hp … . Which otherwise have to be handled Special.
 
Here a main()
From my testings the functions between SetMapMusic and InitBlizzard do only exist when needed.
Lua:
function main()
    SetCameraBounds(-3328.0 + GetCameraMargin(CAMERA_MARGIN_LEFT), -3584.0 + GetCameraMargin(CAMERA_MARGIN_BOTTOM), 3328.0 - GetCameraMargin(CAMERA_MARGIN_RIGHT), 3072.0 - GetCameraMargin(CAMERA_MARGIN_TOP), -3328.0 + GetCameraMargin(CAMERA_MARGIN_LEFT), 3072.0 - GetCameraMargin(CAMERA_MARGIN_TOP), 3328.0 - GetCameraMargin(CAMERA_MARGIN_RIGHT), -3584.0 + GetCameraMargin(CAMERA_MARGIN_BOTTOM))
    SetDayNightModels("Environment\\DNC\\DNCLordaeron\\DNCLordaeronTerrain\\DNCLordaeronTerrain.mdl", "Environment\\DNC\\DNCLordaeron\\DNCLordaeronUnit\\DNCLordaeronUnit.mdl")
    NewSoundEnvironment("Default")
    SetAmbientDaySound("LordaeronSummerDay")
    SetAmbientNightSound("LordaeronSummerNight")
    SetMapMusic("Music", true, 0)
    InitSounds()
    CreateRegions()
    CreateCameras()
    InitUpgrades()
    InitTechTree()
    CreateAllDestructables()
    CreateAllItems()
    CreateAllUnits()
    InitBlizzard()
    InitGlobals()
    InitCustomTriggers()
    RunInitializationTriggers()
end
 
I used global initializiation in some map. Currently the iteration with pairs is not synced in multiplayer, means it can happen that for player A the iteration is ABC while for player B it is BCA. Therefore if one adds more then one function to a entry point the game becomes desync in such a case, Because the triggers/timers executing are not the same for all players anymore.

Hopefuly pairs becomes fixed, currently ipairs would be a safe alternative (which iterates from 1 to size of array)
(I am not the discoverer of that pairs bug, I have that from promises which said that recently in the hive discord channel but just remembered that after testing why my map got dcs)
 
Last edited:
Level 17
Joined
Apr 27, 2008
Messages
2,455
Bribe said:
gFuncs[func] = func --Simplification thanks to TheReviewer and Zed on Hive Discord
At the cost of using 2 tables instead of one, you coud call initializers as a queue, instead of a "random" order.
That makes more sense and more power imho.
It was maybe your first code before the simplification, idk.
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
In my tests number and string indexes were fine in pairs iteration.
Are you sure about that ?
We know that pairs iteration order is undefined, but how much is it ?
I mean will it be the same for every player or idk maybe the internal string table (which is not synced by nature) will influence the iteration ?
I have no clue how pairs is built, i'm just asking.

For the same script and same input i have different results, but it might be the _ENV table itself, i have to check further.
But again is the _ENV table same for everyone ? i mean ordered the same, obviously the data itself should be the same.
EDIT : no it's not necessary the same for everyone because of localised strings (different wc3 languages), or just the strings you use on a local block of code.
However that doesn't mean using pairs on a table with the same strings for everyone would have different iterations on each computer, idk.

I was considering using the ascii code of a string as a key instead of the string itself : string.byte, then sort it and use ipairs instead, so every player would have the same iteration on the table.
Maybe it could have key collisions (two different strings but same byte result), but it shouldn't be the case for variables and functions names, isn't it ?

EDIT :

Nevermind, i didn't realized ascii code method was way too complex for such a task, considering it works letter by letter, while it's just easier to use several tables and sort, then using ipairs.

And i suppose it's safe and reasonable to assume that pair iterations can be different for each players, no matter the nature of the keys.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
I mean, have I start my triggers manually?
If you write your your code in this manner like in Bribe's examples:
Lua:
do
    ...

    onInitialization(function ()
        --
        -- Initialization code here
        --
    end)

end
The code inside there will be run at map start. It is like an equivalent to an 'InitTrig_' in jass
JASS:
function InitTrig_TriggerName takes nothing returns nothing
    //
    // Initialization code here
    //
endfunction

You also have other choices aside from onInitialization() - onGlobalInit() and onTriggerInit() - which both runs before onInitialization().
 
Level 4
Joined
Mar 25, 2021
Messages
18
Hi @Forsakn @Troll-Brain , if you have a non-desync version of this, please feel free to upload it as your own resource or paste it here and I'll update the original post with it and give you full credit.
Sure, it's a very basic change but should have posted it. Since then I've changed my code to the one in the first spoiler, second one is what I used earlier but using ordered indexes should be a lot more performant (if that even matters). The ipairs()-version works using only one counter variable because ipairs can handle sparse tables and that's what it was implemented for to my understanding. Included it just for showcase.

Lua:
-- Global Initialization 1.2 adds an onGameStart function.

do
    local PRINT_CAUGHT_ERRORS = true
    local errors = {}
    local errorCount = 0

    local gFuncs = {}
    local gFuncCount = 0
    local tFuncs = {}
    local tFuncCount = 0
    local iFuncs = {}
    local iFuncCount = 0
    local sFuncs = {}
    local sFuncCount = 0

    function OnGlobalInit(func) -- Runs once all variables are instantiated.
        gFuncCount = gFuncCount + 1
        gFuncs[gFuncCount] = func
    end

    function OnTriggerInit(func) -- Runs once all InitTrig_ are called
        tFuncCount = tFuncCount + 1
        tFuncs[tFuncCount] = func
    end

    function OnInitialization(func) -- Runs once all Map Initialization triggers are run
        iFuncCount = iFuncCount + 1
        iFuncs[iFuncCount] = func
    end

    function OnGameStart(func) -- Runs once the game has actually started
        sFuncCount = sFuncCount + 1
        sFuncs[sFuncCount] = func
    end

    local function saveError(where, error)
        if not PRINT_CAUGHT_ERRORS then return end

        errorCount = errorCount + 1
        errors[errorCount] = "Global Initialization: caught error in " .. where .. ": " .. error
    end

    local function printErrors()
        if errorCount < 1 then return end

        print("|cffff0000Global Initialization: " .. errorCount .. " errors occured during Initialization.|r")
        for i = 1, errorCount do
            print(errors[i])
        end

        errors = nil
    end

    local function runGameStart()
        for i = 1, sFuncCount do
            local result, error = pcall(sFuncs[i])
            if not result then
                saveError("onGameStart", error)
            end
        end

        sFuncs = nil
    end

    local function runInitialization()
        for i = 1, iFuncCount do
            local result, error = pcall(iFuncs[i])
            if not result then
                saveError("runInitialization", error)
            end
        end

        iFuncs = nil
    end

    local function runTriggerInit()
        for i = 1, tFuncCount do
            local result, error = pcall(tFuncs[i])
            if not result then
                saveError("runTriggerInit", error)
            end
        end
        tFuncs = nil

        local old = RunInitializationTriggers
        if old then
            function RunInitializationTriggers()
                old()
                runInitialization()
            end
        else
            runInitialization()
        end
    end

    local function runGlobalInit()
        for i = 1, gFuncCount do
            local result, error = pcall(gFuncs[i])
            if not result then
                saveError("runGlobalInit", error)
            end
        end
        gFuncs = nil

        local old = InitCustomTriggers
        if old then
            function InitCustomTriggers()
                old()
                runTriggerInit()
            end
        else
            runTriggerInit()
        end
    end

    local oldBliz = InitBlizzard
    function InitBlizzard()
        oldBliz()

        local old = InitGlobals
        if old then
            function InitGlobals()
                old()
                runGlobalInit()
            end
        else
            runGlobalInit()
        end

        -- Start timer to run gamestart-functions and then print all caught errors if PRINT_CAUGHT_ERRORS
        TimerStart(
            CreateTimer(),
            0.00,
            false,
            function()
                DestroyTimer(GetExpiredTimer())

                runGameStart()
                if PRINT_CAUGHT_ERRORS then
                    printErrors()
                end
            end
        )
    end
end

Lua:
-- Global Initialization 1.2 adds an onGameStart function.

do
    local gFuncs = {}
    local tFuncs = {}
    local iFuncs = {}
    local sFuncs
    local funcCount = 0

    local function incFuncCount()
        funcCount = funcCount + 1
    end

    function onGlobalInit(func) --Runs once all variables are instantiated.
        incFuncCount()
        gFuncs[funcCount] = func --Simplification thanks to TheReviewer and Zed on Hive Discord
    end

    function onTriggerInit(func) -- Runs once all InitTrig_ are called
        incFuncCount()
        tFuncs[funcCount] = func
    end

    function onInitialization(func) -- Runs once all Map Initialization triggers are run
        incFuncCount()
        iFuncs[funcCount] = func
    end

    function onGameStart(func) --runs once the game has actually started
        if not sFuncs then
            sFuncs = {}
            TimerStart(
                CreateTimer(),
                0.00,
                false,
                function()
                    DestroyTimer(GetExpiredTimer())
                    for _, f in ipairs(sFuncs) do
                        f()
                    end
                    sFuncs = nil
                end
            )
        end
        incFuncCount()
        sFuncs[funcCount] = func
    end

    local function runInitialization()
        for _, f in ipairs(iFuncs) do
            f()
        end
        iFuncs = nil
    end

    local function runTriggerInit()
        for _, f in ipairs(tFuncs) do
            f()
        end
        tFuncs = nil
        local old = RunInitializationTriggers
        if old then
            function RunInitializationTriggers()
                old()
                runInitialization()
            end
        else
            runInitialization()
        end
    end

    local function runGlobalInit()
        for _, f in ipairs(gFuncs) do
            f()
        end
        gFuncs = nil
        local old = InitCustomTriggers
        if old then
            function InitCustomTriggers()
                old()
                runTriggerInit()
            end
        else
            runTriggerInit()
        end
    end

    local oldBliz = InitBlizzard
    function InitBlizzard()
        oldBliz()
        local old = InitGlobals
        if old then
            function InitGlobals()
                old()
                runGlobalInit()
            end
        else
            runGlobalInit()
        end
    end
end

edit: Added use of pcall like @AGD suggested, with the option of printing eventual errors, if you use custom errors you should change the output to some kind of deep-print of the error. If you for some reason wish to use the ipairs() version I recommend doing the same there.

Errors can be tested adding this script to your map, given you don't have functions with these names :grin:
Lua:
OnGlobalInit(function () a() end)
OnTriggerInit(function () b() end)
OnInitialization(function () c() end)
OnGameStart(function () d() end)

edit 2: Now starting the onGameStart timer from inside of the overwritten InitBlizzard function like @Bribe suggested.

edit 3: Made sure errors are printed after onGameStart functions have been called. Also renamed public functions to use pascal case and removed error report if no errors occurred when PRINT_CAUGHT_ERRORS was true like @Jampion suggested.
 
Last edited:
Level 4
Joined
Mar 25, 2021
Messages
18
Btw, I think this should use pcall(), no? So that crashes from one initialization function would not stop other initializations from running.
You are correct, thanks, I updated the code with an option to print eventual errors.
 
Level 4
Joined
Mar 25, 2021
Messages
18
@Forsakn I am pretty sure the onGameStart function will crash the thread unless it is called from within a different initializer. To simplify things for the user, you would probably want to start the timer from within the overwritten InitBlizzard function.
Hadn't experienced any issues with it but added it just to be safe, it does make more sense. Maybe the timer thread isn't started until the game has finished loading and that's why it worked.. but I dont know, not experienced enough to say.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,328
A good way to initialize lua code without having to rely on GUI triggers.

Is PRINT_CAUGHT_ERRORS intended to be used for testing only? Because right now even if no errors are reported, it will print that 0 errors were reported, so it can't be used outside of testing. I think only printing something when errors are encountered would be useful, so it can be used for normal playing.

The public functions like onGlobalInit should be named OnGlobalInit.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
A good way to initialize lua code without having to rely on GUI triggers.

Is PRINT_CAUGHT_ERRORS intended to be used for testing only? Because right now even if no errors are reported, it will print that 0 errors were reported, so it can't be used outside of testing. I think only printing something when errors are encountered would be useful, so it can be used for normal playing.

The public functions like onGlobalInit should be named OnGlobalInit.
I am 99% sure that the error thing was added by someone else who edited my post.
 
Level 4
Joined
Mar 25, 2021
Messages
18
A good way to initialize lua code without having to rely on GUI triggers.

Is PRINT_CAUGHT_ERRORS intended to be used for testing only? Because right now even if no errors are reported, it will print that 0 errors were reported, so it can't be used outside of testing. I think only printing something when errors are encountered would be useful, so it can be used for normal playing.

The public functions like onGlobalInit should be named OnGlobalInit.
It was, but I changed it so it wont say anything if there were no errors now, also changed the naming of the functions, new code is in the "Ordered indices" spoiler in my old post. :)
 
Level 20
Joined
Nov 18, 2012
Messages
1,612
Yes ordered indices are a must, but you are still thinking too jassy like.
You don't need gFouncCount, tFuncCount, iFuncCount, sFuncCount, just use # operator or table.insert
Lua:
    function onGlobalInit(func) -- Runs once all variables are instantiated.
        --table.insert(gFuncs, func)    either this
        --gFuncs[#gFuncs + 1] = func    or this
    end
ez :D
 
Level 4
Joined
Mar 25, 2021
Messages
18
Yes ordered indices are a must, but you are still thinking too jassy like.
You don't need gFouncCount, tFuncCount, iFuncCount, sFuncCount, just use # operator or table.insert
Lua:
    function onGlobalInit(func) -- Runs once all variables are instantiated.
        --table.insert(gFuncs, func)    either this
        --gFuncs[#gFuncs + 1] = func    or this
    end
ez :D
Yup I know, just did it the old way cause it's faster, very unnecessary probably, but it's readable enough for a script this size. :grin:
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
I have updated this to version 2.0, which changes the syntax to:

OnGlobalInit (was onGlobalInit)
OnTrigInit (was onTriggerInit)
OnMapInit (was onInitialization)
OnGameStart (was onGameStart)

Version 2.0 has a much shorter code as it now uses [Lua] - Hook, which does all the heavy lifting.

Update to v2.1 to take advantage of the new Hook.flush functionality introduced in Hook 3.2.

Note: This does not need to be updated to take advantage of Hook 3.4 as the hooks it uses are too simple to have needed any changes.
 
Last edited:

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
Updated to version 2.2.1 to (optionally) take advantage of the priority sequence introduced in Hook 4.0. This will still work as it always did if a priority is not assigned, however will now allow you to add "before" hooks to each of these by passing a negative number, or set a priority for an "after" hook on each of these by passing a positive number.

Edit: updated to 2.2.2 to make it so the priority can be the first argument (or the second, whichever one prefers) and also made it so that nothing gets hooked until someone invokes this system (previously, it would run its own timer even if no one is using any of the methods).
 
Last edited:

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
Updated to version 3.0 - this goes back to the 1.0 method of not depending on Hook.

Also, all 2.0 versions, including the recent "Lite", had a problem with initialization that I didn't catch up until now. They all had been acting as InitBlizzard hooks.

Version 3.0 fixes that, ditches the "priority" thing that hooks use (doubtful it was of any use to anyone) and this should once again be ready for prime time.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,328
This resource is fundamental to run initialization code purely with Lua and straight forward to use. Approved.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
Here is a new version, which inlines the core components of Eikonium's "try" method while eliminating all unnecessary duplicated code. I have also added "OnMainInint" which runs even before InitBlizzard, in case someone wants to grab any potential handles generated from the CreateAllUnits/Items/Destructables/etc. functions. I see no reason to fragment those functions.

Lua:
do  -- Global Initialization 3.1.0.0
  
    -- Special thanks to Tasyen, Forsakn, Troll-Brain and Eikonium
 
    local _PRINT_ERRORS = "|cffff5555" --if this is a non-false/nil value, prints any caught errors at the start of the game.
  
    local run, errors = {}, nil
    local function createInit(name)
        local funcs = {}
        _G[name] = function(func)
            funcs[#funcs+1] = type(func) == "function" and func or load(func)
            if not errors then
                errors = _PRINT_ERRORS and {}
              
                local function hook(oldFunc, userFunc, chunk)
                    local old = _G[oldFunc]
                    if oldFunc == "InitBlizzard" then
                        run[userFunc], old = old, run[userFunc]
                    end
                    if old then
                        _G[oldFunc] = function()
                            old()
                            run[userFunc]()
                            chunk()
                        end
                    else
                        run[userFunc]()
                        chunk()
                    end
                end
                hook("InitBlizzard", "OnMainInit", function()
                    hook("InitGlobals", "OnGlobalInit", function()
                        hook("InitCustomTriggers", "OnTrigInit", function()
                            hook("RunInitializationTriggers", "OnMapInit", function()
                                TimerStart(CreateTimer(), 0.00, false, function()
                                    DestroyTimer(GetExpiredTimer())
                                    run["OnGameStart"]()
                                    run = nil
                                    if _PRINT_ERRORS then
                                        local errorN = #errors
                                        if errorN > 0 then
                                            print(_PRINT_ERRORS.."Global Initialization: "..errorN.." errors occured during Initialization.|r")
                                            for i = 1, errorN do print(errors[i]) end
                                        end
                                        errors = nil
                                    end
                                end)
                            end)
                        end)
                    end)
                end)
            end
        end
        run[name] = function()
            for _, f in ipairs(funcs) do
                local _, fail = pcall(f)
                if fail then
                    errors[#errors+1] = _PRINT_ERRORS..name.." error: "..fail.."|r"
                end
            end
            funcs, _G[name] = nil, nil
        end
    end
    createInit("OnMainInit")    -- Runs "before" InitBlizzard is called. Meant for assigning things like hooks.
    createInit("OnGlobalInit")  -- Runs once all variables are instantiated.
    createInit("OnTrigInit")    -- Runs once all InitTrig_ are called
    createInit("OnMapInit")     -- Runs once all Map Initialization triggers are run
    createInit("OnGameStart")   -- Runs once the game has actually started
end--End of Global Initialization
 
Last edited:
Level 18
Joined
Jun 26, 2020
Messages
1,451
There is something that bothers me, but even that in Lua "don't matter the order in where you define a global value" it seems to not be correct for this system, sometimes is defined before calling it and sometimes not, if I move the position of where it is (more specific move it to a custom script code placed time ago) finally is detected.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
There is something that bothers me, but even that in Lua "don't matter the order in where you define a global value" it seems to not be correct for this system, sometimes is defined before calling it and sometimes not, if I move the position of where it is (more specific move it to a custom script code placed time ago) finally is detected.
Yes, some people use an external code compiler like Ceres/Typescript2Lua for this reason. There was a big discussion on @Almia's Module thread last month about what should be done with initialization order.
 
There is something that bothers me, but even that in Lua "don't matter the order in where you define a global value" it seems to not be correct for this system, sometimes is defined before calling it and sometimes not, if I move the position of where it is (more specific move it to a custom script code placed time ago) finally is detected.
The line that defines/sets/creates the global has to run first to be able to use it. One scope executes from Top to Bottom, like a function or the Lua root code. Hence Global Initialization also needs to be logical executed/created before you can use it which is more likely when you place it to a lower line nr.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
The line that defines/sets/creates the global has to run first to be able to use it. One scope executes from Top to Bottom, like a function or the Lua root code. Hence Global Initialization also needs to be logical executed/created before you can use it which is more likely when you place it to a lower line nr.
Exactly. The major complication is that vJass allowed code to be placed anywhere, but Lua does not. This is one (of many) reasons why a lot of people have reverted to third-party tools for coding in Lua, because their tools can use things such as Ceres, TypeScript2Lua or @ScorpioT1000 's WLPM (GitHub - Indaxia/wc3-wlpm-module-manager: Warcraft 3 Lua Module Manager (like ES6)) in order to avoid the need for object flow.

At one point, I had a version of this which was using a priority system so that people can use Global Initialization with different priorities to execute code. That way, Global Initialization would make sure that it executes its usercode in the order the user specified in code, rather than the order in which the triggers are aligned in the Trigger Editor.

I think that if I added some optional flavor to Global Initialization which gave some kind of hybrid flavor of module/WLPM, that we could solve the depenency problem for people who aren't using third-party tools and don't want to mess around with the trigger ordering.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
I had actually created a major change to this code a couple of days ago, which I unfortunately lost the file for. I'm putting it here now (untested until tonight CET), which showcases the major changes:

1) OnMainInit will run before any (known) CreateAll... function
2) "print" will now be hooked at the start of the loading and will force all "print" calls to wait until the game has actually started, before printing them.
3) "print" will try to un-hook itself afterwards to improve performance on subsequent calls

Fun fact: I originally had a beta "Hook Lite" which used the method below to try to handle hooks, but I realized that it could potentially have infinite "dead" functions registered in cases involving dynamic hook/unhook processes. This ultimately led to the development of Hook 5.0, which solved that problem and made the entire Hook library extremely fast and lightweight. I must have been so excited to release Hook 5 that I forgot to save a copy of the Global Initialization update.

Lua:
do  -- Global Initialization 3.2.0.0 beta
  
    -- Special thanks to Tasyen, Forsakn, Troll-Brain and Eikonium
 
    local _PRINT_ERRORS = "|cffff5555" --if this is a non-false/nil value, prints any caught errors at the start of the game.
    local prePrint, run = {}, {}
    local init
    
    local oldPrint = print
    local newPrint = function(s)
        if prePrint then
            if init then init() end
            prePrint[#prePrint+1] = s
        else
            oldPrint(s)
        end
    end
    print = newPrint
    
    init = function()
        init = nil
        local function hook(oldFunc, userFunc, chunk)
            local old = _G[oldFunc]
            if old then
                _G[oldFunc] = function()
                    old()
                    run[userFunc]()
                    chunk()
                end
            else
                run[userFunc]()
                chunk()
            end
        end
        local checkStr = function(s)
            return _G[s] and s
        end
        local hookAt =
            checkStr("InitSounds") or
            checkStr("CreateRegions") or
            checkStr("CreateCameras") or
            checkStr("InitUpgrades") or
            checkStr("InitTechTree") or
            checkStr("CreateAllDestructables") or
            checkStr("CreateAllItems") or
            checkStr("CreateAllUnits") or
            checkStr("InitBlizzard")
        local hookMain = _G[hookAt]
        _G[hookAt] = function()
            run["OnMainInit"]()
            hookMain()
            hook("InitGlobals", "OnGlobalInit", function()
                hook("InitCustomTriggers", "OnTrigInit", function()
                    hook("RunInitializationTriggers", "OnMapInit", function()
                        TimerStart(CreateTimer(), 0.00, false, function()
                            DestroyTimer(GetExpiredTimer())
                            run["OnGameStart"]()
                            run = nil
                            for i = 1, #prePrint do oldPrint(prePrint[i]) end
                            prePrint = nil
                            if print == newPrint then print = oldPrint end --restore the function only if no other functions have overriden it.
                        end)
                    end)
                end)
            end)
        end
    end
    
    local function createInit(name)
        local funcs = {}
        _G[name] = function(func)
            funcs[#funcs+1] = type(func) == "function" and func or load(func)
            if init then init() end
            if not init then
            
            end
        end
        run[name] = function()
            for _, f in ipairs(funcs) do
                local _, fail = pcall(f)
                if fail and _PRINT_ERRORS then
                    print(_PRINT_ERRORS..name.." error: "..fail.."|r")
                end
            end
            funcs, _G[name] = nil, nil
        end
    end
    createInit("OnMainInit")    -- Runs "before" InitBlizzard is called. Meant for assigning things like hooks.
    createInit("OnGlobalInit")  -- Runs once all variables are instantiated.
    createInit("OnTrigInit")    -- Runs once all InitTrig_ are called
    createInit("OnMapInit")     -- Runs once all Map Initialization triggers are run
    createInit("OnGameStart")   -- Runs once the game has actually started
end--End of Global Initialization

For reference, this is what that Hook Lite version looked like:

Lua:
[email protected] varStr string
[email protected] func function
[email protected] function old_function
[email protected] function call_this_to_remove
function AddHook(varStr, func)
    local old = _G[varStr]
    local removed
    if old and func and type(func) == "function" then
        local new = function(...)
            if removed then
                old(...)
            else
                func(...)
            end
        end
        _G[varStr] = new
        return old, function()
            if _G[varStr] == new then
                _G[varStr] = old
            else
                removed = true
            end
        end
    end
end
 
Level 20
Joined
Nov 18, 2012
Messages
1,612
I just came across something odd, to say the least. I might be just doing a dumb thing but I can't find the reason as to why my idea doesn't work in some circunstances.
I'm using v3.0.0.1 of your initialization and here's the full context and range of triggers that I've played around with. Also using the Debug Utils, and EventListener.
Lua:
do
    UnitMapEvent = {}
    local trigger
    function InitTrig_UnitMapEvent()
        UnitMapEvent.onEnter = EventListener.create() --inherit
        UnitMapEvent.onLeave = EventListener.create()
        trigger = CreateTrigger()
        local region = CreateRegion()
        local rect = GetWorldBounds()
        RegionAddRect(region, rect)
        TriggerRegisterEnterRegion(trigger, region, nil)
        TriggerAddCondition(trigger, Filter(function() 
            if IsTriggerEnabled(trigger) then
                print("ran by ".. GetUnitName(GetTriggerUnit()))
                UnitMapEvent.onEnter:run(GetTriggerUnit())
            end
        end))
    end
    function UnitMapEvent.Enable(isEnabled)
        if isEnabled then
            EnableTrigger(trigger)
        else
            DisableTrigger(trigger)
        end
    end
end
Lua:
OnGlobalInit(function()
    InitTrig_IngameConsole()
    InitTrig_UnitMapEvent()
    UnitMapEvent.onEnter:register(function()
        print("registered first function")
    end)
end)
  • Map Init GUI
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Game - Display to (All players) the text: MAP INIT GUI
      • Unit - Create 1 Rifleman for Player 1 (Red) at (Center of (Playable map area)) facing Default building facing degrees
  • Test with Unit elapsed
    • Events
      • Time - Elapsed game time is 2.00 seconds
    • Conditions
    • Actions
      • Game - Display to (All players) the text: Elapsed Creation
      • Unit - Create 1 Footman for Player 1 (Red) at (Center of (Playable map area)) facing Default building facing degrees
The idea was to make one trigger that calls a list of functions when a unit enters the map bounds and thus, only having to register the rect once. And I wanted to use OnGlobalInit, but it just breaks all gui triggers above, nothing happens, yet there's absolutely no errors in debug mode (PRINT_CAUGHT_ERRORS = true), meaning that the "Init" section is fully working. Also "UnitMapEvent" seems to be fired. If I use OnTrigInit instead, only "Map Init GUI" doesn't work. If I use OnMapInit, then everything works as supposed to.

Any clue why is this the case? Feels like I've been missing something. I've been tormented day and night upon this.
 
Level 20
Joined
Nov 18, 2012
Messages
1,612
Yes, but that only applies to locals in that scope, and when they have no references pointed at them. These are global variables (only "trigger" is local). "trigger" is also used in UnitMapEvent.Enable, so it can't be garbage collected, since it keeps being used.
But another question arises, if that was true that it was being garbage collected and thus crashes the thread with the rest of the triggers, why does changing from OnGlobalInit to OnMapInit in "Init" section makes everything work?
 
Last edited:
Level 20
Joined
Nov 18, 2012
Messages
1,612
Because I got a universal brainfart from this. I was only trying to make my trigger run "once all variables are instantiated" (OnGlobalInit) and coexist with the GUI map initialization triggers.

Also, I just narrowed the problem...it seems if PRINT_CAUGHT_ERRORS is true, the following happens:
  • using OnGlobalInit, breaks all subsequent OnGlobalInit(func) calls and GUI triggers;
  • using OnTrigInit, same as above except non-initialization GUI triggers work;
  • using OnMapInit, same as above except all GUI triggers work.
if PRINT_CAUGHT_ERRORS is false then everything works, regardless of the options above.

Well, this makes absolutely no sense to me.
Also, it still prints the green coloured message "no error occured" despite variable being false.

Take a look and tell me I'm not crazy.
 

Attachments

  • SanityTest.w3m
    51.5 KB · Views: 1

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,960
I had actually created a major change to this code a couple of days ago, which I unfortunately lost the file for. I'm putting it here now (untested until tonight CET), which showcases the major changes:

1) OnMainInit will run before any (known) CreateAll... function
2) "print" will now be hooked at the start of the loading and will force all "print" calls to wait until the game has actually started, before printing them.
3) "print" will try to un-hook itself afterwards to improve performance on subsequent calls

Fun fact: I originally had a beta "Hook Lite" which used the method below to try to handle hooks, but I realized that it could potentially have infinite "dead" functions registered in cases involving dynamic hook/unhook processes. This ultimately led to the development of Hook 5.0, which solved that problem and made the entire Hook library extremely fast and lightweight. I must have been so excited to release Hook 5 that I forgot to save a copy of the Global Initialization update.

Lua:
do  -- Global Initialization 3.2.0.0 beta
 
    -- Special thanks to Tasyen, Forsakn, Troll-Brain and Eikonium
 
    local _PRINT_ERRORS = "|cffff5555" --if this is a non-false/nil value, prints any caught errors at the start of the game.
    local prePrint, run = {}, {}
    local init
   
    local oldPrint = print
    local newPrint = function(s)
        if prePrint then
            if init then init() end
            prePrint[#prePrint+1] = s
        else
            oldPrint(s)
        end
    end
    print = newPrint
   
    init = function()
        init = nil
        local function hook(oldFunc, userFunc, chunk)
            local old = _G[oldFunc]
            if old then
                _G[oldFunc] = function()
                    old()
                    run[userFunc]()
                    chunk()
                end
            else
                run[userFunc]()
                chunk()
            end
        end
        local checkStr = function(s)
            return _G[s] and s
        end
        local hookAt =
            checkStr("InitSounds") or
            checkStr("CreateRegions") or
            checkStr("CreateCameras") or
            checkStr("InitUpgrades") or
            checkStr("InitTechTree") or
            checkStr("CreateAllDestructables") or
            checkStr("CreateAllItems") or
            checkStr("CreateAllUnits") or
            checkStr("InitBlizzard")
        local hookMain = _G[hookAt]
        _G[hookAt] = function()
            run["OnMainInit"]()
            hookMain()
            hook("InitGlobals", "OnGlobalInit", function()
                hook("InitCustomTriggers", "OnTrigInit", function()
                    hook("RunInitializationTriggers", "OnMapInit", function()
                        TimerStart(CreateTimer(), 0.00, false, function()
                            DestroyTimer(GetExpiredTimer())
                            run["OnGameStart"]()
                            run = nil
                            for i = 1, #prePrint do oldPrint(prePrint[i]) end
                            prePrint = nil
                            if print == newPrint then print = oldPrint end --restore the function only if no other functions have overriden it.
                        end)
                    end)
                end)
            end)
        end
    end
   
    local function createInit(name)
        local funcs = {}
        _G[name] = function(func)
            funcs[#funcs+1] = type(func) == "function" and func or load(func)
            if init then init() end
            if not init then
           
            end
        end
        run[name] = function()
            for _, f in ipairs(funcs) do
                local _, fail = pcall(f)
                if fail and _PRINT_ERRORS then
                    print(_PRINT_ERRORS..name.." error: "..fail.."|r")
                end
            end
            funcs, _G[name] = nil, nil
        end
    end
    createInit("OnMainInit")    -- Runs "before" InitBlizzard is called. Meant for assigning things like hooks.
    createInit("OnGlobalInit")  -- Runs once all variables are instantiated.
    createInit("OnTrigInit")    -- Runs once all InitTrig_ are called
    createInit("OnMapInit")     -- Runs once all Map Initialization triggers are run
    createInit("OnGameStart")   -- Runs once the game has actually started
end--End of Global Initialization

For reference, this is what that Hook Lite version looked like:

Lua:
[email protected] varStr string
[email protected] func function
[email protected] function old_function
[email protected] function call_this_to_remove
function AddHook(varStr, func)
    local old = _G[varStr]
    local removed
    if old and func and type(func) == "function" then
        local new = function(...)
            if removed then
                old(...)
            else
                func(...)
            end
        end
        _G[varStr] = new
        return old, function()
            if _G[varStr] == new then
                _G[varStr] = old
            else
                removed = true
            end
        end
    end
end
Try this version (3.2) as it has a better error handling method and may illustrate the problem better.
 
Top