• πŸ† 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!
  • πŸ† Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! πŸ“… Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece! πŸ”— Click here to enter!

[Lua] Total Initialization

Total Initialization
  • Allows you to initialize your script in a safe, easy-to-debug environment.
  • Allows your map's triggers to be arranged in any order* (similar to JassHelper).
  • Enables functionality similar to Lua modules and vJass libraries.
  • Provides helpful tools to hook into certain key points in the initialization process (e.g. once all udg_ globals are declared, or once the game has started).
*provided that Total Initialization is declared towards the top of the trigger list

Special Thanks:
  • @Eikonium for the "try" function and for challenging bad API approaches, leading me to discovering far better API for this resource.
  • @HerlySQR for GetStackTrace, which makes debugging a much more straightforward process.
  • @Tasyen for helping me to better understand the "main" function, and for discovering MarkGameStarted can be hooked for OnInit.final's needs.
  • @Luashine for showing how I can implement OnInit.config, which - in turn - led to an actual OnInit.main (previous attempts had failed)
  • @Forsakn and Troll-Brain for help with early debugging (primarily with the pairs desync issue)

For laying the framework for requirements in WarCraft 3 Lua:

Why not just use do...end blocks in the Lua root?
While Lua only has the Lua root for initialization, Lua can use run-time hooks on the InitBlizzard function in order to postpone its own code until heavy WarCraft 3 natives need to be called.
  • Creating WarCraft 3 objects in the Lua root is dangerous as it causes desyncs.
  • The Lua root is an unstable place to initialize (e.g. it doesn't allow "print", which makes debugging extremely difficult)
  • do...end blocks force you to organize your triggers from top to bottom based on their requirements.
  • The Lua root is not split into separate pcalls, which means that failures can easily crash the entire loading sequence without showing an error message.
  • The Lua root is not yieldable, which means you need to do everything immediately or hook onto something like InitBlizzard or MarkGameStarted to await these loading steps.
Why I made this:
First, let me show you the sequence of initialization that JassHelper used:

1666109255220.png


There were two very key problems with JassHelper's initializer:
  1. Initialization precedence was Module > Struct > Library > Requirement, rather than Requirement > Module > Struct > Library. This created a problem with required libraries having not been loaded in time for a module to depend on them, meaning that everything in the matured vJass era needed to be initialized with a module in order to circumvent the problem.
  2. The initializers ran before InitGlobals, so it created a permanent rift between the GUI and vJass interfaces by ostracizing one from the other (if vJass changed the value of a udg_ variable, InitGlobals would just overwrite it again).
I therefore wanted to re-design the sequence to allow GUI and Lua to not suffer the same fate:
  1. The Lua root runs.
  2. OnInit functions that require nothing - or - already have their requirements fulfilled in the Lua root.
  3. OnInit functions that have their requirements fulfilled based on other OnInit declarations.
  4. OnInit "custom" initializers run sequentially, prolonging the initialization queue.
  5. Repeat step 2-4 until all executables are loaded and all subsequent initializers have run.
  6. OnInit.final is the final initializer, which is called after the loading screen has transitioned into the actual game screen.
  7. Display error messages for missing requirements.


Basic API for initializer functions:
Lua:
    OnInit.root(function()
        print "This is called immediately"
    end)
    OnInit.config(function()
        print "This is called during the map config process (in game lobby)"
    end)
    OnInit.main(function()
        print "This is called during the loading screen"
    end)
    OnInit(function()
        print "All udg_ variables have been initialized"
    end)
    OnInit.trig(function()
        print "All InitTrig_ functions have been called"
    end)
    OnInit.map(function()
        print "All Map Initialization events have run"
    end)
    OnInit.final(function()
        print "The game has now started"
    end)
Note: You can optionally include a string as an argument to give your initializer a name. This is useful in two scenarios:
  1. If you don't add anything to the global API but want it to be useful as a requirment.
  2. If you want it to be accurately defined for initializers that optionally require it.
API for Requirements:
local someLibrary = Require "SomeLibrary"
  • Functions similarly to Lua's built-in (but disabled in WarCraft 3) "require" function, provided that you use it from an OnInit callback function.
  • Returns either the return value of the OnInit function that declared its name as "SomeLibrary" or will return _G["SomeLibrary"] or just "true" if that named library was initialized but did not return anything nor add itself to the _G table.
  • Will throw an error if the requirement was not found by the time the map has fully loaded.
  • Can also require elements of a table: Require "table.property.subProperty"
local optionalRequirement = Require.optionally "OptionalRequirement"
  • Similar to the Require method, but will only wait if the optional requirement was already declared as an uninitialized library. This name can be whatever suits you (optional/lazy/nonStrict), since it uses the __index method rather than limit itself to one keyword.

The Code:

Lua:
if Debug then Debug.beginFile 'TotalInitialization' end
--[[β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
    Total Initialization version 5.3.1
    Created by: Bribe
    Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
    Inspiration: Almia, ScorpioT1000, Troll-Brain
    Hosted at: https://github.com/BribeFromTheHive/Lua/blob/master/TotalInitialization.lua
    Debug library hosted at: https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”]]

---Calls the user's initialization function during the map's loading process. The first argument should either be the init function,
---or it should be the string to give the initializer a name (works similarly to a module name/identically to a vJass library name).
---
---To use requirements, call `Require.strict 'LibraryName'` or `Require.optional 'LibraryName'`. Alternatively, the OnInit callback
---function can take the `Require` table as a single parameter: `OnInit(function(import) import.strict 'ThisIsTheSameAsRequire' end)`.
---
-- - `OnInit.global` or just `OnInit` is called after InitGlobals and is the standard point to initialize.
-- - `OnInit.trig` is called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
-- - `OnInit.map` is the last point in initialization before the loading screen is completed.
-- - `OnInit.final` occurs immediately after the loading screen has disappeared, and the game has started.
---@class OnInit
--
--Simple Initialization without declaring a library name:
---@overload async fun(initCallback: Initializer.Callback)
--
--Advanced initialization with a library name and an optional third argument to signal to Eikonium's DebugUtils that the file has ended.
---@overload async fun(libraryName: string, initCallback: Initializer.Callback, debugLineNum?: integer)
--
--A way to yield your library to allow other libraries in the same initialization sequence to load, then resume once they have loaded.
---@overload async fun(customInitializerName: string)
OnInit = {}

---@alias Initializer.Callback fun(require?: Requirement | {[string]: Requirement}):...?

---@alias Requirement async fun(reqName: string, source?: table): unknown

-- `Require` will yield the calling `OnInit` initialization function until the requirement (referenced as a string) exists. It will check the
-- global API (for example, does 'GlobalRemap' exist) and then check for any named OnInit resources which might use that same string as its name.
--
-- Due to the way Sumneko's syntax highlighter works, the return value will only be linted for defined @class objects (and doesn't work for regular
-- globals like `TimerStart`). I tried to request the functionality here: https://github.com/sumneko/lua-language-server/issues/1792 , however it
-- was closed. Presumably, there are other requests asking for it, but I wouldn't count on it.
--
-- To declare a requirement, use: `Require.strict 'SomeLibrary'` or (if you don't care about the missing linting functionality) `Require 'SomeLibrary'`
--
-- To optionally require something, use any other suffix (such as `.optionally` or `.nonstrict`): `Require.optional 'SomeLibrary'`
--
---@class Require: { [string]: Requirement }
---@overload async fun(reqName: string, source?: table): string
Require = {}
do
    local library = {} --You can change this to false if you don't use `Require` nor the `OnInit.library` API.

    --CONFIGURABLE LEGACY API FUNCTION:
    ---@param _ENV table
    ---@param OnInit any
    local function assignLegacyAPI(_ENV, OnInit)
        OnGlobalInit = OnInit; OnTrigInit = OnInit.trig; OnMapInit = OnInit.map; OnGameStart = OnInit.final              --Global Initialization Lite API
        --OnMainInit = OnInit.main; OnLibraryInit = OnInit.library; OnGameInit = OnInit.final                            --short-lived experimental API
        --onGlobalInit = OnInit; onTriggerInit = OnInit.trig; onInitialization = OnInit.map; onGameStart = OnInit.final  --original Global Initialization API
        --OnTriggerInit = OnInit.trig; OnInitialization = OnInit.map                                                     --Forsakn's Ordered Indices API
    end
    --END CONFIGURABLES

    local _G, rawget, insert =
        _G, rawget, table.insert

    local initFuncQueue = {}

    ---@param name string
    ---@param continue? function
    local function runInitializers(name, continue)
        --print('running:', name, tostring(initFuncQueue[name]))
        if initFuncQueue[name] then
            for _,func in ipairs(initFuncQueue[name]) do
                coroutine.wrap(func)(Require)
            end
            initFuncQueue[name] = nil
        end
        if library then
            library:resume()
        end
        if continue then
            continue()
        end
    end

    local function initEverything()
        ---@param hookName string
        ---@param continue? function
        local function hook(hookName, continue)
            local hookedFunc = rawget(_G, hookName)
            if hookedFunc then
                rawset(_G, hookName,
                    function()
                        hookedFunc()
                        runInitializers(hookName, continue)
                    end
                )
            else
                runInitializers(hookName, continue)
            end
        end

        hook(
            'InitGlobals',
            function()
                hook(
                    'InitCustomTriggers',
                    function()
                        hook('RunInitializationTriggers')
                    end
                )
            end
        )

        hook(
            'MarkGameStarted',
            function()
                if library then
                    for _,func in ipairs(library.queuedInitializerList) do
                        func(nil, true) --run errors for missing requirements.
                    end
                    for _,func in pairs(library.yieldedModuleMatrix) do
                        func(true) --run errors for modules that aren't required.
                    end
                end
                OnInit = nil
                Require = nil
            end
        )
    end

    ---@param initName       string
    ---@param libraryName    string | Initializer.Callback
    ---@param func?          Initializer.Callback
    ---@param debugLineNum?  integer
    ---@param incDebugLevel? boolean
    local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
        if not func then
            ---@cast libraryName Initializer.Callback
            func = libraryName
        else
            assert(type(libraryName) == 'string')
            if debugLineNum and Debug then
                Debug.beginFile(libraryName, incDebugLevel and 3 or 2)
                Debug.data.sourceMap[#Debug.data.sourceMap].lastLine = debugLineNum
            end
            if library then
                func = library:create(libraryName, func)
            end
        end
        assert(type(func) == 'function')

        --print('adding user func: ' , initName , libraryName, debugLineNum, incDebugLevel)

        initFuncQueue[initName] = initFuncQueue[initName] or {}
        insert(initFuncQueue[initName], func)

        if initName == 'root' or initName == 'module' then
            runInitializers(initName)
        end
    end

    ---@param name string
    local function createInit(name)
        ---@async
        ---@param libraryName string                --Assign your callback a unique name, allowing other OnInit callbacks can use it as a requirement.
        ---@param userInitFunc Initializer.Callback --Define a function to be called at the chosen point in the initialization process. It can optionally take the `Require` object as a parameter. Its optional return value(s) are passed to a requiring library via the `Require` object (defaults to `true`).
        ---@param debugLineNum? integer             --If the Debug library is present, you can call Debug.getLine() for this parameter (which should coincide with the last line of your script file). This will neatly tie-in with OnInit's built-in Debug library functionality to define a starting line and an ending line for your module.
        ---@overload async fun(userInitFunc: Initializer.Callback)
        return function(libraryName, userInitFunc, debugLineNum)
            addUserFunc(name, libraryName, userInitFunc, debugLineNum)
        end
    end
    OnInit.global = createInit 'InitGlobals'                -- Called after InitGlobals, and is the standard point to initialize.
    OnInit.trig   = createInit 'InitCustomTriggers'         -- Called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
    OnInit.map    = createInit 'RunInitializationTriggers'  -- Called last in the script's loading screen sequence. Runs after the GUI "Map Initialization" events have run.
    OnInit.final  = createInit 'MarkGameStarted'            -- Called immediately after the loading screen has disappeared, and the game has started.

    do
        ---@param self table
        ---@param libraryNameOrInitFunc function | string
        ---@param userInitFunc function
        ---@param debugLineNum number
        local function __call(
            self,
            libraryNameOrInitFunc,
            userInitFunc,
            debugLineNum
        )
            if userInitFunc or type(libraryNameOrInitFunc) == 'function' then
                addUserFunc(
                    'InitGlobals', --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
                    libraryNameOrInitFunc,
                    userInitFunc,
                    debugLineNum,
                    true
                )
            elseif library then
                library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
            else
                error(
                    "Bad OnInit args: "..
                    tostring(libraryNameOrInitFunc) .. ", " ..
                    tostring(userInitFunc)
                )
            end
        end
        setmetatable(OnInit --[[@as table]], { __call = __call })
    end

    do --if you don't need the initializers for 'root', 'config' and 'main', you can delete this do...end block.
        local gmt = getmetatable(_G) or
            getmetatable(setmetatable(_G, {}))

        local rawIndex = gmt.__newindex or rawset

        local hookMainAndConfig
        ---@param _G table
        ---@param key string
        ---@param fnOrDiscard unknown
        function hookMainAndConfig(_G, key, fnOrDiscard)
            if key == 'main' or key == 'config' then
                ---@cast fnOrDiscard function
                if key == 'main' then
                    runInitializers 'root'
                end
                rawIndex(_G, key, function()
                    if key == 'config' then
                        fnOrDiscard()
                    elseif gmt.__newindex == hookMainAndConfig then
                        gmt.__newindex = rawIndex --restore the original __newindex if no further hooks on __newindex exist.
                    end
                    runInitializers(key)
                    if key == 'main' then
                        fnOrDiscard()
                    end
                end)
            else
                rawIndex(_G, key, fnOrDiscard)
            end
        end
        gmt.__newindex = hookMainAndConfig
        OnInit.root    = createInit 'root'   -- Runs immediately during the Lua root, but is yieldable (allowing requirements) and pcalled.
        OnInit.config  = createInit 'config' -- Runs when `config` is called. Credit to @Luashine: https://www.hiveworkshop.com/threads/inject-main-config-from-we-trigger-code-like-jasshelper.338201/
        OnInit.main    = createInit 'main'   -- Runs when `main` is called. Idea from @Tasyen: https://www.hiveworkshop.com/threads/global-initialization.317099/post-3374063
    end
    if library then
        library.queuedInitializerList = {}
        library.customDeclarationList = {}
        library.yieldedModuleMatrix   = {}
        library.moduleValueMatrix     = {}

        function library:pack(name, ...)
            self.moduleValueMatrix[name] = table.pack(...)
        end

        function library:resume()
            if self.queuedInitializerList[1] then
                local continue, tempQueue, forceOptional

                ::initLibraries::
                repeat
                    continue=false
                    self.queuedInitializerList, tempQueue =
                        {}, self.queuedInitializerList

                    for _,func in ipairs(tempQueue) do
                        if func(forceOptional) then
                            continue=true --Something was initialized; therefore further systems might be able to initialize.
                        else
                            insert(self.queuedInitializerList, func) --If the queued initializer returns false, that means its requirement wasn't met, so we re-queue it.
                        end
                    end
                until not continue or not self.queuedInitializerList[1]

                if self.customDeclarationList[1] then
                    self.customDeclarationList, tempQueue =
                        {}, self.customDeclarationList
                    for _,func in ipairs(tempQueue) do
                        func() --unfreeze any custom initializers.
                    end
                elseif not forceOptional then
                    forceOptional = true
                else
                    return
                end
                goto initLibraries
            end
        end
        local function declareName(name, initialValue)
            assert(type(name) == 'string')
            assert(library.moduleValueMatrix[name] == nil)
            library.moduleValueMatrix[name] =
                initialValue and { true, n = 1 }
        end
        function library:create(name, userFunc)
            assert(type(userFunc) == 'function')
            declareName(name, false)                --declare itself as a non-loaded library.
            return function()
                self:pack(name, userFunc(Require))  --pack return values to allow multiple values to be communicated.
                if self.moduleValueMatrix[name].n == 0 then
                    self:pack(name, true)           --No values were returned; therefore simply package the value as `true`
                end
            end
        end

        ---@async
        function library:declare(name)
            declareName(name, true)                 --declare itself as a loaded library.

            local co = coroutine.running()

            insert(
                self.customDeclarationList,
                function()
                    coroutine.resume(co)
                end
            )
            coroutine.yield() --yields the calling function until after all currently-queued initializers have run.
        end

        local processRequirement

        ---@async
        function processRequirement(
            optional,
            requirement,
            explicitSource
        )
            if type(optional) == 'string' then
                optional, requirement, explicitSource =
                    true, optional, requirement --optional requirement (processed by the __index method)
            else
                optional = false --strict requirement (processed by the __call method)
            end
            local source = explicitSource or _G

            assert(type(source)=='table')
            assert(type(requirement)=='string')

            ::reindex::
            local subSource, subReq =
                requirement:match("([\x25w_]+)\x25.(.+)") --Check if user is requiring using "table.property" syntax
            if subSource and subReq then
                source,
                requirement =
                    processRequirement(subSource, source), --If the container is nil, yield until it is not.
                    subReq

                if type(source)=='table' then
                    explicitSource = source
                    goto reindex --check for further nested properties ("table.property.subProperty.anyOthers").
                else
                    return --The source table for the requirement wasn't found, so disregard the rest (this only happens with optional requirements).
                end
            end
            local function loadRequirement(unpack)
                local package = rawget(source, requirement) --check if the requirement exists in the host table.
                if not package and not explicitSource then
                    if library.yieldedModuleMatrix[requirement] then
                        library.yieldedModuleMatrix[requirement]() --load module if it exists
                    end
                    package = library.moduleValueMatrix[requirement] --retrieve the return value from the module.
                    if unpack and type(package)=='table' then
                        return table.unpack(package, 1, package.n) --using unpack allows any number of values to be returned by the required library.
                    end
                end
                return package
            end

            local co, loaded

            local function checkReqs(forceOptional, printErrors)
                if not loaded then
                    loaded = loadRequirement()
                    loaded = loaded or optional and
                        (loaded==nil or forceOptional)
                    if loaded then
                        if co then coroutine.resume(co) end --resume only if it was yielded in the first place.
                        return loaded
                    elseif printErrors then
                        coroutine.resume(co, true)
                    end
                end
            end

            if not checkReqs() then --only yield if the requirement doesn't already exist.
                co = coroutine.running()
                insert(library.queuedInitializerList, checkReqs)
                if coroutine.yield() then
                    error("Missing Requirement: "..requirement) --handle the error within the user's function to get an accurate stack trace via the `try` function.
                end
            end

            return loadRequirement(true)
        end

        ---@type Requirement
        function Require.strict(name, explicitSource)
            return processRequirement(nil, name, explicitSource)
        end

        setmetatable(Require --[[@as table]], {
            __call = processRequirement,
            __index = function()
                return processRequirement
            end
        })

        local module  = createInit 'module'

        --- `OnInit.module` will only call the OnInit function if the module is required by another resource, rather than being called at a pre-
        --- specified point in the loading process. It works similarly to Go, in that including modules in your map that are not actually being
        --- required will throw an error message.
        ---@param name          string
        ---@param func          fun(require?: Initializer.Callback):any
        ---@param debugLineNum? integer
        OnInit.module = function(name, func, debugLineNum)
            if func then
                local userFunc = func
                func = function(require)
                    local co = coroutine.running()

                    library.yieldedModuleMatrix[name] =
                        function(failure)
                            library.yieldedModuleMatrix[name] = nil
                            coroutine.resume(co, failure)
                        end

                    if coroutine.yield() then
                        error("Module declared but not required: "..name)
                    end

                    return userFunc(require)
                end
            end
            module(name, func, debugLineNum)
        end
    end

    if assignLegacyAPI then --This block handles legacy code.
        ---Allows packaging multiple requirements into one table and queues the initialization for later.
        ---@deprecated
        ---@param initList string | table
        ---@param userFunc function
        function OnInit.library(initList, userFunc)
            local typeOf = type(initList)

            assert(typeOf=='table' or typeOf=='string')
            assert(type(userFunc) == 'function')

            local function caller(use)
                if typeOf=='string' then
                    use(initList)
                else
                    for _,initName in ipairs(initList) do
                        use(initName)
                    end
                    if initList.optional then
                        for _,initName in ipairs(initList.optional) do
                            use.lazily(initName)
                        end
                    end
                end
            end
            if initList.name then
                OnInit(initList.name, caller)
            else
                OnInit(caller)
            end
        end

        local legacyTable = {}

        assignLegacyAPI(legacyTable, OnInit)

        for key,func in pairs(legacyTable) do
            rawset(_G, key, func)
        end

        OnInit.final(function()
            for key in pairs(legacyTable) do
                rawset(_G, key, nil)
            end
        end)
    end

    initEverything()
end
if Debug then Debug.endFile() end


Lua:
OnInit("Yuna", function(needs)
    print("Yuna has arrived, thanks to Tidus, who is:",needs "Tidus")
    print("Yuna only optionally needs Kimhari, who is:",needs.optionally "Kimahri")
end)

OnInit("Tidus", function(requires)
    print("Tidus has loaded, thanks to Jecht, who is:", requires "Jecht")

    print "Tidus is declaring Rikku and yielding for initializers that need her."
    OnInit "Rikku"
 
    return "The Star Player of the Zanarkand Abes"
end)

OnInit("Cid", function(needs)
    print("Cid has arrived, thanks to Rikku, who is:",needs "Rikku")
end)

OnInit(function(braucht)
    print("Spira does not exist, so this library will never run.", braucht "Spira")
end)

OnInit("Jecht", function(needs)
    print("Jecht requires Blitzball, which is:",needs"Blitzball")
    print("Jecht has arrived, without Sin, who is:",needs.readyOrNot "Sin")
 
    return "A Retired Blitzball Player", "An Alcoholic", "A bad father to Tidus"
end)

OnInit("Wakka", function(needs)
    print("Wakka requires Blitzball, which is:",needs"Blitzball")
    print("Wakka optionally requires Lulu, who is:", needs.ifHeFeelsLikeIt "Lulu")
end)

OnInit("Lulu", function(needs)
    print("Lulu optionally requires Wakka, who is:", needs.ifSheFeelsLikeIt "Wakka")
end)

OnInit("Blitzball", function()
    print "Blitzball has no requirements."
    return "Round", "Popular", "Extremely Dangerous"
end)

OnInit("Bevel", function(import)
    print("Bevel will not wait for Zanarkand, which is:", import.sleepily "Zanarkand")
end)

OnInit.trig("Ronso Fangs", function()
    print "The Ronso Fangs run last, because they are late bloomers."
end)
Prints (with the help of the "try" function):
1668273754677.png




Use the following site to test: JDoodle - Online Compiler, Editor for Java, C/C++, etc

Ensure you copy the latest version of Total Initialization into the designated part of the script.
Lua:
DoNothing = function()end
MarkGameStarted = function() bj_gameStarted = true end
InitGlobals = DoNothing --this is always found at the top of the map script, even when there are no udg_ globals

-- Total Initialization script goes here --


-- (write your own unit tests here) --


-- auto-generated functions like "CreateAllUnits" are placed here --


-- map header script is placed here --


-- GUI triggers are placed here --


-- this marks the end of the user's access to the Lua root, everything is auto-generated by World Editor beyond this point --


InitCustomTriggers = DoNothing
RunInitializationTriggers = DoNothing

function main()
    InitGlobals()
    InitCustomTriggers()
    RunInitializationTriggers()
end
config = DoNothing

-- end of Lua root --

config() --called during the game lobby menu/prior to the game's loading screen.
main() --called first during the map loading screen.
MarkGameStarted() --synthesize that the game has loaded.


If you don't have any need of libraries or requirements, you might find this "Lite" version to be more your taste:

Lua:
--Global Initialization 'Lite' by Bribe, with special thanks to Tasyen and Eikonium
--Last updated 11 Nov 2022
do
    local addInit
    function OnGlobalInit(initFunc) addInit("InitGlobals",               initFunc) end -- Runs once all GUI variables are instantiated.
    function OnTrigInit  (initFunc) addInit("InitCustomTriggers",        initFunc) end -- Runs once all InitTrig_ are called.
    function OnMapInit   (initFunc) addInit("RunInitializationTriggers", initFunc) end -- Runs once all Map Initialization triggers are run.
    function OnGameStart (initFunc) addInit("MarkGameStarted",           initFunc) end -- Runs once the game has actually started.
    do
        local initializers = {}
        addInit=function(initName, initFunc)
            initializers[initName] = initializers[initName] or {}
            table.insert(initializers[initName], initFunc)
        end
        local function init(initName, continue)
            if initializers[initName] then
                for _,initFunc in ipairs(initializers[initName]) do pcall(initFunc) end
            end
            if continue then continue() end
        end
        local function hook(name, continue)
            local _name=rawget(_G, name)
            if _name then
                rawset(_G, name, function()
                    _name()
                    init(name, continue) --run the initializer after the hooked handler function has been called.
                end)
            else
                init(name, continue) --run initializer immediately
            end
        end
        hook("InitGlobals",function()
            hook("InitCustomTriggers",function() --InitCustomTriggers and RunInitializationTriggers are declared after the users' code,
                hook "RunInitializationTriggers" --hence users need to wait until they have been declared.
            end)
        end)
        hook "MarkGameStarted"
    end
end
 
Last edited:

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
@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.
 
Level 15
Joined
Mar 25, 2016
Messages
1,327
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.
 
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. :)
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,913
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:
 
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:
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:
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.
 
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 24
Joined
Jun 26, 2020
Messages
1,855
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.
 
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.
 
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.
 
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:
---@param varStr string
---@param func function
---@return function old_function
---@return 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
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,913
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.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,913
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:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,913
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: 9
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:
---@param varStr string
---@param func function
---@return function old_function
---@return 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.
 
Major update!

Global Initialization 4.0 introduces "OnLibraryInit" which allows code to be placed at any height in the trigger editor without needing to make sure its requirements are listed above it (which was a freedom that JassHelper gave us). The only consideration is that Global Initialization should be added to the top of each map's trigger list.

Unlike with other dependency-based systems I've seen, this one does not require the "required" libraries to actually be using Global Initialization. For example, "Hook" doesn't use the Global Initialization API, but something using OnLibraryInit can mention "AddHook" as a requirement (as AddHook is found in the _G table) then it will work accordingly.
 
Last edited:
Top