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

[Lua] Total Initialization

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Major update to version 4.1: Optional OnLibraryInit is now a thing. Additionally, you can name your libraries (in case you don't have anything that will add to the global API).

Here's an example of how this works, using characters from my favorite Final Fantasy game (Rikku's ability to bribe monsters is actually the inspiration for my username, by the way):

Lua:
--OnLibraryInit tests

OnLibraryInit({name="Yuna", "Tidus", optional={"Kimahri"}}, function()
    print "Yuna has arrived, thanks to Tidus, but without Kimahri."
end)

OnLibraryInit({name="Tidus", "Jecht"}, function()
    print "Tidus has arrived, thanks to Jecht."
end)

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

OnLibraryInit({name="Jecht", optional={"Sin"}}, function()
    print "Jecht has arrived, without Sin."
end)

OnLibraryInit({name="Wakka", optional={"Lulu"}}, function()
    print "This will never be called, because Lulu also optionally requires Wakka."
end)

OnLibraryInit({name="Lulu", optional={"Wakka"}}, function()
    print "This will never be called, because Wakka also optionally requires Lulu."
end)

The above code will produce the following output:

2022-10-04 20_24_45.png


However, this does not produce an error message for Lulu and Wakka. I expect that no one would actually be silly enough to have circular optional dependencies; so instead of throwing an error, it just does nothing.
 
Level 20
Joined
Jul 10, 2009
Messages
474
I've just skimmed through your code and made a few tests with OnLibraryInit in preparation for including it into [Mapping] - A comprehensive guide to Mapping in Lua.

I've last checked several months ago. Since then, the code has evolved, but also got much much harder to read. You really like nested code ;) I see references to upvalues being declared in later parts of your code within some function that is executed somewhere else, but only when another thing exists and so on. By now, I have literally no clue what is going on, so I can't judge code quality. That isn't necessarily an issue, as I trust you on having tested everything thouroughly. Still, I liked the simplicity that the code had in earlier versions. Maybe you could try going in that direction again in the future? Maybe it's just me, though.
Anyway, thanks for your continuous work on this resource and your take on managing dependencies!

I have the following questions and remarks despite not understanding the code too well:
  1. I've run a few tests and the results looked fine. Good stuff!
  2. Could you add to the main post that OnLibraryInit is only necessary to use for dependencies in the Lua root?
    95% of dependencies happen during runtime, where no system is required at all, because everything has already been loaded.
  3. As I consider this being one of the mandatory resources for Lua-mapping, I think the general description of the main post should be improved as well. It currently contains code and a few short examples, but I honestly wouldn't understand what this is about when I didn't already knew the matter. Maybe you could add a description of what Global Initialization actually does, why it is useful and relevant examples (trigger creation)?
  4. OnMapInit and the other library functions don't have Emmy Annotation. In fact, they have no visible declaration at all. As a consequence, VSCode doesn't even know the function name. You can type "OnMapInit", but you will neither get auto-complete nor function params.
    I think, this really should be solved. Not even having proper function names for auto-complete makes using this resource way too tedious.
    Potential solution in next bullet :)
  5. You currently use code similar to rawset(_G, "OnMapInit", function() ... end) for the definition of the library functions, which feels very hacky. What is the reason behind this?
    If you just wanted to recycle the code in the loop, you might want to use a function factory instead. Example code:
    Lua:
    ---@param privateStorage table
    ---@return fun(func:function)
    local initializerFactory = function(privateStorage)
        return function(func)
            privateStorage[#privateStorage+1]= func
            initInitializers()
        end
    end
    
    do
        local onMapInitStorage = {}
    
        ---Runs the argument after the Wc3 Map Initialization event has fired.
        OnMapInit = initializerFactory(onMapInitStorage) --doesn't need param annotation, because the return value of the factory says it all
    
        [define hook also using onMapInitStorage]
    end
  6. funcs[#funcs+1]=type(func)=="function" and func or load(func)
    It's up to you, but I personally wouldn't input a string to OnMapInit or any other library function.
  7. Why don't you just hook OnMainInit on InitBlizzard? Does it have any advantage to use earlier functions?
  8. Optional Requirements in OnLibraryInit have really weird syntax. Also the function would align better to the others, if the function param came first. I'd prefer something like OnLibraryInit(func, {mandatoryRequirements}, {optionalRequirements}) (table being optional for both requirement lists).
  9. At which point during loading screen does OnLibraryInit actually execute?
  10. Lua:
    if not rawget(_G, initName) and not rawget(missingRequirements, initName) then
        table.insert(missingRequirements, initName)
    end
    table.insert(missingRequirements, initName) basically executes missingRequirements[#missingRequirements+1] = initName, while rawget(missingRequirements, initName) asks for missingRequirements[initName]. This looks buggy, although I'm not sure how to actually produce an erroneous example from it :D
  11. Lua:
    if whichInit.name then
        initHandlerExists[whichInit.name]=true
    end
    You can run the body without the if-block. If whichInit.name doesn't exist, the line will simply execute initHandlerExists[nil]=true (i.e. not do anything).

P.S: I think after improving the main post, this resource should definitely be stickied. Whom can I reach out to?
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
  1. Thanks for testing this out!
  2. I'm not sure what you mean here. Dependencies can pop into existence at any time from the Lua root to the OnMapInit event, and OnLibraryInit will throw errors for any API or named OnLibraryInit objects that weren't found at that point.
  3. Thanks, I will be updating the documentation/examples accordingly.
  4. Yes, that's just one example of me being frustrated with Emmy annotation: I'd like to be able to declare "this thing is a global, here are its properties".

    Writing into the _G table is a hack I used to reduce the number of times I'd need to declare an object by its name. Like you said, it's better to have it be more legible and easier to follow the logic (at bare minimum by the parser itself).
  5. Returning a function and assigning the variable to it is definitely the right approach, and I'll implement that.
  6. Good point. What I'd rather do in that case is just throw an error if the thing is not a function.
  7. The idea behind that is to be able to hook all the stuff the comes before InitBlizzard, such as sounds/items/rects that might want to have special handlers.
  8. I agree the syntax can get funny if you use optional params, and a third field would be good, but I do like being able to define a name in that thing, and that it's optional to declare that name.

    However, it would be terrible to have the function come first there, as it would mean that all of the requirements are being placed at the very bottom, instead of the top (where they are naturally suited).
    .
    One option would be to take up to 4 arguments, with the first 3 of them being optional. I did this recently in Timed Call and Echo. It would look like this:
    4 arguments:
    Lua:
    [name, requirements, optional_requirements, ]initFunc
    3 arguments:
    Lua:
    [name_or_requirements, requirements_or_optional, ]initFunc
    2 arguments:
    Lua:
    [requirements_or_optional, ]initFunc
    .
    However, this is actually quite terrible at differentiating between optional requirements or actual requirements in that last boolean. If a library only has optional requirements, it would have to look like this:
    Lua:
    OnLibraryInit({}, {optionalRequirement1, optionalRequirement2}, function() print "hey there" end)
    .
    It also means that a "name" and an "optional requirement" cannot both exist:
    Lua:
    OnLibraryInit("MyLibName", {optionalRequirement1}, function() end)
    -- this thinks that the MyLibName is actually a requirement, when it is instead a name you wanted to give to your library.
    .
    Therefore, to avoid confusion, you can just throw in whatever properties you need to that first table, and the second argument is always a function:
    Lua:
    OnLibraryInit({ name = "myLib",
    --requires:
        "requirement1", --link/to/requirement/1
        "requirement2", --link/to/requirement/2
    optional = {
        "optionalRequirement1", --link/to/optional/requirement/1
        "optionalRequirement2", --link/to/optional/requirement/2
    }
    }, function() print "myLib initialized" end)
    This ends up looking similar to vJass libraries:
    JASS:
    library myLib initializer Init/*
    */  requires /*
    */      requirement1, /* link/to/requirement/1
    */      requirement2, /* link/to/requirement/2
    */  optional /*
    */      optionalRequirement1 /* link/to/optional/requirement/1
    */      optionalRequirement2 // link/to/optional/requirement/2
    
        private function Init takes nothing returns nothing
            call BJDebugMsg("myLib initialized")
        endfunction
    endlibrary
  9. It will execute during the OnGlobalInit phase, then any leftovers during OnTrigInit, then OnMapInit, then OnGameStart, then will throw errors for anything missed. I was getting errors when I allowed it to run during the OnMainInit phase (though I didn't have the GetStackTrace within an xpcall back then to try to diagnose it, so I ended up "throwing the baby out with the bathwater" to instead have a working product that skips that part). I'd rather nothing execute before OnGlobalInit anyway that doesn't have to (LuaInfused GUI has to, for the sake of converting groups/locations/rects/forces to tables), so I see this as fine.
  10. Thank you - didn't catch that! This is only used for debugging purposes, so I will likely end up just doing an o(n) search through the list each time (provided the user has enabled error printing).
  11. I recall having encountered problems in the past where someTable[nil] = someValue threw an error (something like index is nil). Or maybe pcall just gave it a warning, but let the code run regardless.
I proposed to Ralle and Archian about an alternative for "showcasing" resources on here, such as different colored tags. I am not sure when or if anything will come of it, though. Ralle in the past has added tags when I had a need of them, but I think Archian has the same authority.
 
Level 20
Joined
Jul 10, 2009
Messages
474
I'm not sure what you mean here. Dependencies can pop into existence at any time from the Lua root to the OnMapInit event, and OnLibraryInit will throw errors for any API or named OnLibraryInit objects that weren't found at that point.
I meant that the vast majority of dependencies just happen later, after all systems have already been loaded.
Lua:
--Example

---some system that provides an API function.
A = {}
A.someFunction = function() ... end

--Another system is using that API-function
B = {}
--This function is supposed to be used within the game
B.someFunction = function()
    A.someFunction()
    ...
end

You don't need OnLibraryInit for this example, because at the time B.someFunction is executed, the dependency already exists (regardless of code order above). From my perspective, most dependencies are like that (some are not, though, and that's where your function jumps in). I prefer to let the users know in the main post where using the initializer is necessary and where not.

Writing into the _G table is a hack I used to reduce the number of times I'd need to declare an object by its name.
Noo, don't do that! You can always skip annotations instead of completely hiding the function, if you need to (but also don't do that :p ).
Not using Emmy will always make you more annoyed than annotating the stuff in the first place. :)

The idea behind that is to be able to hook all the stuff the comes before InitBlizzard, such as sounds/items/rects that might want to have special handlers.
Who would be able to hook those functions? The user? Or are you going to define "OnSoundInit" etc. in the future?

However, it would be terrible to have the function come first there, as it would mean that all of the requirements are being placed at the very bottom, instead of the top (where they are naturally suited).
.
Don't think that's terrible. The order can look a bit weird, when you use anonymous functions within hooks, like OnLibraryInit({}, function() ... end). But isn't the "standard" scenario that we define a local function first and execute that within the hook?
Lua:
do
    local foo = function() ... end
    OnLibraryInit(foo)
end
At least, code like that is much better to read and should be encouraged (and I would even prefer to have the example code like that on the main page).

Regarding order of requirements/name param, you can separate them and users can just not pass a value (or {}).
Lua:
--name, mandatoryReq, optionalReq, actualFunc
OnLibraryInit(nil, {"A"}, {}, foo)
But I still prefer to have the function first :)

I recall having encountered problems in the past where someTable[nil] = someValue threw an error (something like index is nil). Or maybe pcall just gave it a warning, but let the code run regardless.
something[nil] will never throw an error, only nil[something] will.

Edit: You are right on this, my bad. Reading something[nil] is fine, but writing something[nil] = whatever throws an error.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
I have a question, how should be used from now the others On...Init? Because using OnGlobalInit inside of OnLibraryInit block throws me an error.
If it has dependencies that are not yet declared, yes it will throw an error. I'm not sure which code you have that's running based off of OnGlobalInit, but if you are expecting certain API to exist at that point, but doesn't, it is possible that OnLibraryInit has not complete for those yet.

Now that I've thought about this, I realize that I should run OnLibraryInit before the vanilla initializers.

@Eikonium , the main reason to avoid the function coming first is to make sure declarations appear in the code (visibly) before the components of the library (the function). I get what you mean about a "precedent", but in this case I can't see a way around it.

To avoid the tables, I could allow the user to just pass a string (least complex):
Lua:
OnLibraryInit("myLib requires dude, bro optional man, woman", function print "duuuude" end)

Permutations could be:

"optional man, woman"

"requires dude, bro"

"myLib optional man, woman"

But then this would also make it annoying to split out the URL link (which I guess could be just listed above the thing).
 
Level 23
Joined
Jun 26, 2020
Messages
1,838
If it has dependencies that are not yet declared, yes it will throw an error. I'm not sure which code you have that's running based off of OnGlobalInit, but if you are expecting certain API to exist at that point, but doesn't, it is possible that OnLibraryInit has not complete for those yet.

Now that I've thought about this, I realize that I should run OnLibraryInit before the vanilla initializers.
Basically I have something like this:
Lua:
OnLibraryInit({...}, function ()
    ...
    OnGlobalInit(function ()
        ...
    end)
end)
And throws me the error of OnGlobalInit doesn't exist and only happens with it, with the others On...Init didn't happen.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Basically I have something like this:
Lua:
OnLibraryInit({...}, function ()
    ...
    OnGlobalInit(function ()
        ...
    end)
end)
And throws me the error of OnGlobalInit doesn't exist and only happens with it, with the others On...Init didn't happen.
Oh, now that is an interesting scenario that I can help with. In this case, using OnGlobalInit is redundant, because OnLibraryInit already waits for that part of the initialization process. So you don't need it.

However, you get the error because OnGlobalInit was wiped out from the API by the time your function is called. So no OnGlobalInit can be called after the OnLibraryInit/OnGlobalInit point in time.

After the OnGameStart point, for example, it is not possible to call any On...Init function, because the system self-destructs after it runs.
 
Level 20
Joined
Jul 10, 2009
Messages
474
@Eikonium , the main reason to avoid the function coming first is to make sure declarations appear in the code (visibly) before the components of the library (the function). I get what you mean about a "precedent", but in this case I can't see a way around it.

To avoid the tables, I could allow the user to just pass a string (least complex):
Let's stick to tables, strings would make it worse.
No worries, it's your library and if you feel the dependencies should come first, we can go with it.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Let's stick to tables, strings would make it worse.
No worries, it's your library and if you feel the dependencies should come first, we can go with it.
Well, I don't see anything as "my library", as that kind of defeats the purpose of open source code sharing. At the end of the day, I want stuff to be intuitive and as painless as possible to work with. My philosophy is that I try to make everybody happy. If I show a look at how this is "supposed" to work (in Lua terms):

Lua:
require "someLibrary"

--the rest of the library proceeds next.

The only other syntax that could somehow help to break this out into a more "traditional" approach would be this:

Lua:
OnGlobalInit("myLibrary", --myLibrary is just a name used for requirements purposes. It would be an optional parameter which OnGlobalInit could take if the user wants.
function()
    Require "someLibrary"
    Require "someOtherLibrary"
    Require.optional "someOptionalLibrary"
    Require.optional "someOtherOptionalLibrary"
 
    --The rest of the library proceeds next
end)

The logic is this:

On...Init would be changed to coroutines, so that a "Require" or "Require.optional" call will yield the coroutine.

If required global variables or named libraries are not found in the map, it will keep checking after each successful queued initialization process, and will systematically and recursively resume coroutines as many times as necessary, until it either stops yielding, or until the initializer has completed. I could then handle errors in two ways:

1) Throw a soft error if the requirement is not found during the specified On...Init.
2) Throw a hard error if the requirement eventually came into existence during a later On...Init.

Currently, OnLibraryInit only follows the second rule (so it will not throw an error unless it can't find its requirements even at the point where the loading screen has wrapped up).

Each time an "On...Init" has a string parameter (either before or after the function, as this would be easy enough for me to detect and flexibly swap around), it will ensure that the initialization function that is assigned to that name is not yielded, and once its coroutine has run it will flag it as a successful declaration and inform any dependencies accordingly.

The logic works, and I'd be happy to code it (since I've already done something more complex than that for CreateEvent), but as I said several months ago here: [Lua] - Module System, I would rather stick to something that the community is happy with.

I personally think the coroutine approach would be the easiest for the user to understand, and would have the cleanest and most intuitive visual implementation for those who are coming from a Lua background (or most any background which declares its requirements similarly).

If anyone is interested in this idea, then I can add it as an optional alternative to OnLibraryInit (and keep OnLibraryInit around for those who prefer to do it that way).
 
Last edited:
Level 23
Joined
Jun 26, 2020
Messages
1,838
Hello, can you add that if you add an optional requirement and doesn't exist, then set it to false?, to be according with this trick:
Lua:
setmetatable(_G,{__index=function(_, n) if string.sub(tostring(n),1,3) ~= 'bj_' then print("Trying to read undeclared global : "..tostring(n)) end end,}) --Shows a message ingame, when a non-declared global is used.
Because I think the __index metamethod is called if and only if the index is nil, to do something like this:
Lua:
OnLibraryInit({name = "MyLib", optional = {"OtherLib"}}, function ()
    if OtherLib then
        -- Do things with the OtherLib
    end
end)
without having the necessity of avoiding the message by doing this:
Lua:
OnLibraryInit({name = "MyLib", optional = {"OtherLib"}}, function ()
    if rawget(_G, "OtherLib") then
        -- Do things with the OtherLib
    end
end)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Okay, Require and Require.optional are now a part of the below API, and they will work provided that you call them only from an On...Init function (or a manually-created coroutine) and not from the Lua root.

If anyone would like to volunteer to help me test the below, I would appreciate it. I have tested it a bit, and it appears to work in all scenarios I could think of:

Lua:
do  --Global Initialization 4.2.0.1

    --4.2 adds Require and Require.optional to the API. You can now declare libraries
    --via a string parameter passed to On...Init and OnGameStart. You can add requirements
    --during the initialization process via Require "something" or optional requirements
    --via Require.optional "somethingElse". Requirements must be listed at the top of
    --the function you registered to On...Init.
    --OnLibraryInit will still treat a string parameter as a requirement, rather than as
    --a library name.

    --4.1 adds optional requirements to OnLibraryInit, and gives the option to specify
    --the name of your library.
    
    --4.0 introduces "OnLibraryInit", which will delay the running of the function
    --until certain variable(s) are present in the global table. This means that
    --only Global Initialization needs to be placed as the top trigger in your map,
    --and any resources which use OnLibraryInit to wait for each other can be found
    --in any order below that.
    
    --Special thanks to Tasyen, Forsakn, Troll-Brain and Eikonium
    
    --change this assignment to false or nil if you don't want to print any caught errors at the start of the game.
    --You can otherwise change the color code to a different hex code if you want.
    local _ERROR    = "ff5555"
    
    local prePrint,run,genericDeclarations,initHandlerExists={},{},{},{}
    local initInitializers
    local _G        = _G
    local rawget    = rawget
    local insert    = table.insert
    local oldPrint  = print
    local newPrint  = function(s)
        if prePrint then
            initInitializers()
            prePrint[#prePrint+1]=s
        else
            oldPrint(s)
        end
    end
    print           = newPrint
    
    local displayError=_ERROR and function(errorMsg)
        print("|cff".._ERROR..errorMsg.."|r")
    end or DoNothing
    
    local libraryInitQueue, missingRequirements
    
    initInitializers=function()
        initInitializers = DoNothing
        --try to hook anything that could be called before InitBlizzard to see if we can initialize even sooner
        for _,hookAt in ipairs {
            "InitSounds",
            "CreateRegions",
            "CreateCameras",
            "InitUpgrades",
            "InitTechTree",
            "CreateAllDestructables",
            "CreateAllItems",
            "CreateAllUnits",
            "InitBlizzard"
        } do
            local oldMain=rawget(_G, hookAt)
            if oldMain then
                rawset(_G, hookAt, function()
                    run["OnMainInit"]()
                    oldMain()
                    local function hook(oldFunc, userFunc, chunk)
                        local old = rawget(_G, oldFunc)
                        if old then
                            rawset(_G, oldFunc, function()
                                old()
                                run[userFunc]()
                                chunk()
                            end)
                        else
                            run[userFunc]()
                            chunk()
                        end
                    end
                    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 libraryInitQueue then
                                        if _ERROR and #libraryInitQueue>0 then
                                            for _,ini in ipairs(missingRequirements) do
                                                if not rawget(_G, ini) and not initHandlerExists[ini] then
                                                    displayError("OnLibraryInit missing requirement: "..ini)
                                                end
                                            end
                                        end
                                        libraryInitQueue=nil
                                    end
                                    missingRequirements=nil
                                    genericDeclarations=nil
                                    initHandlerExists=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)
                break
            end
        end
    end
    
    local state
    local function callUserInitFunction(initFunc, name, declareNameReady)
        state = coroutine.create(function()
            xpcall(initFunc, function(msg)
                xpcall(error, displayError, "\nGlobal Initialization Error with "..name..":\n"..msg, 4)
            end)
            if declareNameReady then
                initHandlerExists[declareNameReady]=true
            end
        end)
        coroutine.resume(state)
    end

    local function initLibraries(name)
        --I encountered some bugs with allowing libraries to initialize in sync with OnMainInit, so I need to exclude that block.
        if libraryInitQueue and name ~= "OnMainInit" then
            ::runAgain::
            local runRecursively,tempQ
            tempQ,libraryInitQueue=libraryInitQueue,{}
            
            for _,func in ipairs(tempQ) do
                --If the queued initializer returns true, that means it ran, so we can remove it.
                if func() then
                    runRecursively=runRecursively or coroutine.status(state)~="suspended"
                else
                    insert(libraryInitQueue, func)
                end
            end
            if runRecursively and #libraryInitQueue > 0 then
                --Something was initialized, which might mean that further systems can now be initialized.
                goto runAgain
            end
        end
    end

    ---Handle logic for initialization functions that wait for certain initialization points during the map's loading sequence.
    ---@param name string
    ---@return fun() userFunc --Calls userFunc during the defined initialization stage.
    local function createInitAPI(name)
        local funcs={}
        --Create a handler function to run all initializers pertaining to this initialization level.
        run[name]=function()
            initLibraries(name)
            for _,f in ipairs(funcs) do
                callUserInitFunction(f, name, genericDeclarations[f])
            end
            funcs=nil
            rawset(_G, name, nil)
            initLibraries(name)
        end
        ---Calls userFunc during the map loading process.
        ---@param libName string
        ---@param userFunc fun()
        return function(libName, userFunc)
            if not userFunc or type(userFunc)=="string" then
                libName,userFunc=userFunc,libName
            end
            if type(userFunc) == "function" then
                funcs[#funcs+1]=userFunc
                if libName then
                    genericDeclarations[userFunc] = libName
                    initHandlerExists[libName]=initHandlerExists[libName] or false
                end
                initInitializers()
            else
                displayError(name.." Error: function expected, got "..type(userFunc))
            end
        end
    end
    
    OnMainInit   = createInitAPI("OnMainInit")    -- Runs "before" InitBlizzard is called. Meant for assigning things like hooks.
    OnGlobalInit = createInitAPI("OnGlobalInit")  -- Runs once all GUI variables are instantiated.
    OnTrigInit   = createInitAPI("OnTrigInit")    -- Runs once all InitTrig_ are called.
    OnMapInit    = createInitAPI("OnMapInit")     -- Runs once all Map Initialization triggers are run.
    OnGameStart  = createInitAPI("OnGameStart")   -- Runs once the game has actually started.

    ---OnLibraryInit is a new function that allows your initialization to wait until others items exist.
    ---This is comparable to vJass library requirements in that you can specify your "library" to wait for
    ---those other libraries to be initialized, before initializing your own.
    ---For example, if you want to ensure your script is processed after "GlobalRemap" has been declared,
    ---you would use:
    ---OnLibraryInit("GlobalRemap", function() print "my library is initializing after GlobalRemap was declared" end)
    ---
    ---To include multiple requirements, pass a string table:
    ---OnLibraryInit({"GlobalRemap", "LinkedList", "MDTable"}, function() print "my library has waited for 3 requirements" end)
    ---To have optional requirements or to have named libraries (names are only useful for requirements):
    ---OnLibraryInit({name="MyLibrary", "MandatoryRequirement", optional={OptionalRequirement1, OptionalRequirement2}})
    ---@param whichInit string|table
    ---@param userFunc fun()
    function OnLibraryInit(whichInit, userFunc)
        if not libraryInitQueue then
            initInitializers()
            libraryInitQueue={} ---@type function[] fun():boolean
            missingRequirements=_ERROR and {}
        end
        local runInit;runInit=function()
            runInit=nil --nullify itself to prevent potential recursive calls during initFunc's execution.
            
            callUserInitFunction(userFunc, "OnLibraryInit", type(whichInit)=="table" and whichInit.name)
            return true
        end
        local initFuncHandler
        if type(whichInit)=="string" then
            if _ERROR and not rawget(_G, whichInit) and not missingRequirements[whichInit] then
                insert(missingRequirements, whichInit)
            end
            initFuncHandler=function() return runInit and rawget(_G, whichInit) and runInit() end
        elseif type(whichInit)=="table" then
            initFuncHandler=function()
                if runInit then
                    for _,initName in ipairs(whichInit) do
                        --check all strings in the table and make sure they exist in _G or were already initialized by OnLibraryInit with a non-global name.
                        if not rawget(_G, initName) and not initHandlerExists[initName] then return end
                    end
                    if whichInit.optional then
                        for _,initName in ipairs(whichInit.optional) do
                            --If the item isn't yet initialized, but is queued to initialize, then we postpone the initialization.
                            --Declarations would be made in the Lua root, so if optional dependencies are not found by the time
                            --OnLibraryInit runs its triggers, we can assume that it doesn't exist in the first place.
                            if not rawget(_G, initName) then
                                if initHandlerExists[initName]==false then return end
                                rawset(_G, initName, false)
                            end
                        end
                    end
                    --run the initializer if all requirements either exist in _G or have been loaded by OnLibraryInit.
                    return runInit()
                end
            end
            if whichInit.name then
                initHandlerExists[whichInit.name]=false
            end
            if _ERROR then
                for _,initName in ipairs(whichInit) do
                    if not rawget(_G, initName) then
                        insert(missingRequirements, initName)
                    end
                end
            end
        else
            displayError("Invalid requirement type passed to OnLibraryInit: "..type(whichInit))
            return
        end
        insert(libraryInitQueue, initFuncHandler)
    end

    local function addReq(name, optional)
        if not rawget(_G, name) then
            local co = coroutine.running()
            OnLibraryInit(optional and {optional={name}} or name, function() coroutine.resume(co) end)
            coroutine.yield(co)
        end
    end
    Require = {
        __call   = function(_, name)                                     addReq(name)           end,
        optional = function(name) if initHandlerExists[name]==false then addReq(name, true) end end
    }
    setmetatable(Require, Require)
end
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
474
A few additional remarks from my side:

  1. I like the coroutine-based approach and especially the fact that I can use Require inside my own setup function.
    I also like that we can define requirements in all hooks, not just in OnLibraryInit. The latter probably can be deprecated now. Good job!
  2. I ran some small tests and the results looked fine. Will probably test further in two weeks from now, I currently don't have time.
  3. I'd like to encourage you again to write less nested and cross-referential code. You spoke about open source code sharing in one of your previous posts, but other community members are simply not able to understand/improve your code, when you add that much complexity. It's fine as long as the complexity is required for the code to work, but I think there are currently some unnecessary parts.
    I am aware of that your code is still in development and that you probably want to create a working version before optimizing it for the reader. Still want to remind you to be sure ;)
  4. Can you publish one of the old versions of Global Init in the main post in addition to the current one?
    I ask, because I personally haven't had a real use for library management in my own map so far.
    My dependencies either happen in the Lua root upon initializing data structures - in which case I write them below each other in the right order (because I find that less annoying than packing the whole thing into a setup function).
    Or they happen after loading screen, where everything has been loaded anyway.
    This might all change in the future, but the point is I prefer to only import the code to my map that I really need (and understand) :)
  5. Why do you overwrite the print-function? I see no reason for that. You already use xpcall, so just let your message handler save the error message to your own log and print all log entries after game start.
  6. @HerlySQR suggested to use rawget(_G, name), because he got issues with if OtherLib then producing "Trying to read undeclared global"-warnings. This issue however occurs, because you have overwritten the print-function instead of using a proper message handler. Not doing so solves the issue (because the prints will vanish in loading screen), you don't need to use rawget for that.
  7. If you want a function to support string arguments and table arguments alike, you can reassign the string to a table by x = (type(x) == 'string') and {x} or x to make a table of it instead of having an additional case-separation.

  8. The idea behind that is to be able to hook all the stuff the comes before InitBlizzard, such as sounds/items/rects that might want to have special handlers.
    Who would be able to hook those functions? The user? Or are you going to define "OnSoundInit" etc. in the future?
    I'm still not sure why we need this "get-earliest-init"-hook or GetMainInit in general. Isn't using OnGlobalInit just as fine?
    Is this part of the code just a preparation for future enhancements of your system? Or what would the user do with it?
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Thanks as always for the very very constructive feedback - I really appreciate it.

  1. I intend to no longer promote OnLibraryInit in my systems documentation, and maybe some time down the line (once people are getting more familiar with this Require approach and generally using Lua to begin with), I'll be able to measure the impact of removing OnLibraryInit altogether (so it may end up being that I keep it around if there are too many dependencies which are using it, or at the very least include it as a drop-in module).
  2. Thanks a lot for helping me test this!
  3. I'll definitely work on the code readability, comments, documentation and make sure that the Emmy comments are working correctly. I tried to consolidate where there was too much code duplication, but I'll look for areas where I can make things either: a) less cryptic by building them out, or b) give them better variable/function names.
  4. I do still have Global Initialization - Lite, which I can give a little sprucing up and add it as an option for those who haven't yet drunk from "Bribe's Kool-Aid".
  5. The only reason I overwrite it is for debugging purposes within the code, for the following reasons:
    1. The messages disappear too quickly if the user prints something from their code
    2. They are invisible if the map is already printing enough print statements to fill the page.
    3. The print statement doesn't work from the Lua root, so delaying it until the map's loading sequence will also fix the problem.
    However, I do set it back to the original (after the initialization process is done). On a separate note, I've been meaning to override the default WarCraft 3 print method permanently, as you currently can't do the following: print(table, unit). If I change WarCraft 3's print method to use "tostring" on each parameter, then it will be able to do that, just like in a real Lua environment. I think this is rather something worthwhile for you to add to the Debug Library, as such a thing is not "initialization" related.
  6. I did fix HerlySQR's issue in this version by including the unfulfilled optional requirements in the _G table as false values. Even if I do not override "print", the message will still appear shortly after the loading sequence is completed.
  7. That's a good idea to streamline the OnLibraryInit function a little better.
  8. The only reason I've had so far to use OnMainInit is for Lua-Infused GUI. I don't want to replace the natives within the Lua root, so that people can still cache the original natives themselves from within the Lua root. But then I still want that code to run ASAP and definitely before CreateAllRegions and InitGlobals are called.
I should have some time over the next week to refine this thing based on the criteria you've identified for me. Thanks again!
 
Level 20
Joined
Jul 10, 2009
Messages
474
You're welcome :)

1. I reckon, OnLibraryInit is new enough to safely deprecate it. If you want to keep it, I like the idea of doing so as a separate drop-in module, so most users don't have to import old code into their map.

5. The problems you mentioned are valid, but hold for print-statements conducted by the user. Caching those until Game Start is a good idea for debugging, but such a process shouldn't stay in a map after release. As such, it would better be suited for Debug Utils, as you said.
Global Init itself should use a custom message handler in xpcall instead of using print at all.
On a separate note, I've been meaning to override the default WarCraft 3 print method permanently, as you currently can't do the following: print(table, unit).
You can perfectly do print(table, unit) in Wc3, no need to overwrite anything. Print does already apply tostring on all input params.

6.
Even if I do not override "print", the message will still appear shortly after the loading sequence is completed.
Weird. At least for me, it doesn't. Loading screen prints within your hooks or in Lua root neither show up after startup nor in the ingame F12-log.

8.
Your Lua-infused GUI is very invasive. Wouldn't using the original natives after overwriting everything cause more harm than benefit? Also, other resources that overwrite the original native would simply vanish, if they didn't use OnMainInit as well.
Consider a user who overwrites Location to print an error message, if coords happen to be outside Map.
That user will probably overwrite in Lua root and you are replacing his version with your own.

If you really want to make the original natives available, you could save them to OriginalNatives.Location etc. instead.

That said, I prefer overwriting in Lua root, not via OnMainInit.
 
Level 23
Joined
Jun 26, 2020
Messages
1,838
@HerlySQR suggested to use rawget(_G, name), because he got issues with if OtherLib then producing "Trying to read undeclared global"-warnings. This issue however occurs, because you have overwritten the print-function instead of using a proper message handler. Not doing so solves the issue (because the prints will vanish in loading screen), you don't need to use rawget for that.
That wasn't exactly what I said, I said what if an optional requiriment didn't exists? and you wanna do something like that, so to avoid the message do something like requiriment = false to not doing the rawget(_G, name) method (I don't know if that would work).
 
Level 20
Joined
Jul 10, 2009
Messages
474
That wasn't exactly what I said, I said what if an optional requiriment didn't exists? and you wanna do something like that, so to avoid the message do something like requiriment = false to not doing the rawget(_G, name) method (I don't know if that would work).
I think I got you right on this. My point was that requiriment = false is not even necessary, because the messages resulting from non-existent optional requirements would mostly be printed during loading screen and normally not be visible to the user anyway.
Bribe made them visible by delaying all loading screen prints to after game start, though - so I suggested to just not do that to solve your issue.
Global Init should still print its own errors after game start for sure.

Also, the current approach with setting things to false is maybe problematic, see below :)
(But otherwise, I liked it!)

Maybe another solution for your issue:
Lua:
--activate check after game start, where libraries have already been initialized
OnGameStart(function()
    setmetatable(_G,{__index=function(_, n) if string.sub(tostring(n),1,3) ~= 'bj_' then print("Trying to read undeclared global : "..tostring(n)) end end,})
end)

not rawget(_G, whichInit)
forgot to say this in my previous post:
Do you want to support global-variable-requirements as well? If so, the global variable might be a boolean that was set to false on purpose. In this case, you could instead check for rawget(_G, whichInit) ~= nil.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Update to version 4.3 - @HerlySQR you will now want to use the following to get around that problem you described:

local myRequirement = Require "Requirement"

myRequirement is then set to whatever "Requirement" is in the _G table.

@Eikonium , the description and the code layout/config are now much cleaner.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Updated to version 4.3.1, further expanded on the documentation to include background as to why I made this resource.

Initializers with optional requirements will now automatically load at the end of their designated initialization block after trying multiple times to load. It will eventually give up and load anyway, even if the optional requirement hasn't yet loaded.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Updated to version 4.4, which introduces the following:

1) Global Initialization 'Lite'
  • An alternative variation added to the main thread based on feedback from @Eikonium.
2) Require.await "Custom declaration"
  • Declares a name to be used as a requirement, then yields the thread, allowing other libraries who need that custom declaration to catch up.
  • This was mainly created to serve the next update of vJass2Lua Runtime, because it was not possible to handle the complex JassHelper initialization process otherwise.
  • A real-world scenario for this was in GUI Unit Indexer, to fill the space of the "Unit Indexer Initialized" event.
3) On...Init events now support a return value from the initializing function.
  • This functions similarly to Lua modules.
  • Require or Require.optional statements will return the cached return value OR the matching element from the _G table OR just "true".
  • "false" and "nil" return values will be ignored and replaced by the value in the _G table or "true".
  • Require.optional will be "nil" if the optional requirement does not exist and wasn't declared (this isn't new, but I hadn't announced it).
 
Level 20
Joined
Jul 10, 2009
Messages
474
Thanks for adding the lite version, much appreciated!
Can you elaborate more on the support of return values and also what Require.await does in contrast to normal Require? I'm somewhat confused after having read your update :)

Edit: Btw, I realised that you were right about something[nil] = ... throwing an error (only reading something[nil] is fine). Using optional parameters in the brackets indeed requires previous checking. Sorry for the inconveniences in case you now need to readjust.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Thanks for adding the lite version, much appreciated!
Can you elaborate more on the support of return values and also what Require.await does in contrast to normal Require? I'm somewhat confused after having read your update :)
The Require.await thing is definitely not the best name I've ever come up with, but it's an extension of naming a library, by giving that same library multiple different yield points that allow other libraries to catch up to its library sequence. Effectively, "wait for condition", and the "condition" is that any libraries that may or may not require that declaration will load before the "awaiting" library.

The return value is supposed to coincide with normal Lua modules that return values.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Major Update!

Global Initialization has now been renamed to Total Initialization.

What is new with Total Initialization?
  • Main API has been consolidated to "OnInit". The below names are interchangeable (I will not remove the original 4 and will keep them as legacy functions):
    • OnGlobalInit <> OnInit
    • OnTrigInit <> OnInit.trig
    • OnMapInit <> OnInit.map
    • OnGameStart <> OnInit.final
  • New API has been added, thanks to mechanics discovered by @Luashine in [Lua] - Inject main/config from WE trigger code (like JassHelper)
    • OnInit.main (runs before the main function is called)
    • OnInit.config (runs before the config function is called)
  • New API has been added to allow "instant" libraries that function end-to-end within the Lua root:
    • OnInit.root (called immediately, has the benefit of being wrapped in xpcall, called via coroutine, able to declare a name and can require)
  • OnInit callback functions now can take a single parameter which holds the "Require" object.
  • Now-obsolete API:
    • OnMainInit -> OnInit.main
    • OnLibraryInit -> OnInit.library
    • Require.await "something" -> OnInit "something"
The Lite version has been updated to reduce the syntax and complexity to be as simple as possible.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Another cool update - now supports the following syntax:

Require "myTable.property"

This will look for myTable in _G, will yield until myTable is declared in _G (if it isn't already), and will then yield until myTable has declared the specified property.

I intend this for libraries which want to check if a certain property of Event exists, but the user can obviously find other purposes for this capability.

It does NOT support nested properties (myTable.property.subProperty). If someone wants this, let me know.

As of version 5.1.1, this also supports nested properties like myTable.property.subProperty.otherProperty.how.long.can.this.go.on, as it will dynamically scan for sub-tables using a goto statement.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Update to version 5.2 - this was a near total-rewrite of the script. Hopefully, this helps to clarify things, as per feedback from @Eikonium .

New Functionality:
  • Supports multiple return values from libraries:
    Lua:
    OnInit(function() print(Require "MyLibrary") end) --prints 1,2,3,4
    OnInit("MyLibrary", function() return 1,2,3,4 end)
  • Require statements can now specify a table to pull the requirement from (otherwise it defaults to that key in _G or to a library by the same name):
    Require("requirement", fromTable)
  • Require statements that pull data from a non-_G table will no longer count declared libraries of the same name as part of that chain. Therefore, syntax like Require "GlobalTable.subTable.lowerTable" will check if GlobalTable is in _G or is a loaded library, but will only check for subTable as a component of GlobalTable and lowerTable as a component of subTable. This way, if a library by the name of "subTable" or "lowerTable" is declared, this particular chain will ignore it.
  • Error printing for missing requirements is now smarter.
Details:
  • The top part of the code of Total Initialization is now nearly identical to Global Initialization Lite, which may help to break down where the other pieces fit into the puzzle.
  • OnInit.library has been de-constructed to use separate "Require" statements for each declaration. At this point, that function is really short and may be worthwhile to split it into its own separate add-on resource, because it no longer needs to exist for the rest of the components to function.
  • The overall code length is fairly shorter in this version (even though it has more functionality).
  • Eliminated two of the constant variables for separately designating the use of coroutines and libraries. Coroutines are now mandatory for libraries to work.
If you want to do your own tests using JDoodle - Online Compiler, Editor for Java, C/C++, etc , you can use the following unit tester:

Lua:
DoNothing = function()end
InitBlizzard = DoNothing
MarkGameStarted = DoNothing
InitGlobals = DoNothing --this is always found at the top of the map script, even when there are no udg_ globals


-- user's access to the Lua root begins here (the first trigger in the trigger editor will be placed here) --


-- paste latest version of Total Initialization here for testing --


-- (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()
    InitBlizzard()
    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.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Hopefully the final update I'll need do on this resource: 5.2.1.
  • Legacy API is now neatly packaged at the top of the script, so users can enable or disable parts of the legacy code that they want or don't need.
  • No longer hooks InitBlizzard (same with Global Initialization Lite) - I've found that InitGlobals is always declared, even on a fresh map that never contained udg_ variables.
  • Error handling has been fully offloaded to @Eikonium's "try" function (if it exists). This enables:
    • Stack traceback for library requirements which weren't found.
    • Easy error disabling by simply not including that function in the map.
  • Fixed a couple of bugs discovered from the previous version with optional requirements and OnInit "name" syntax.
  • Code is slightly more efficient and consumes less memory. Readability has been improved even further.
  • Library process handling has been moved entirely into the "library" block. If you don't use libraries/requirements, just set "library" to "false" and delete the library block (or just use Global Initialization Lite).
I've updated the Final Fantasy X demo script on the original post to delve into some of the more obscure aspects of requirements in order to show how they work. I've also added a unit tester to the main post which incorporates the "try" function, but you'll need to paste in the latest version of Total Initialization into the designated part of the code.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
This update preview should fix the problem with syntax highlighting. I think I've finally found out how to enable the "Require" function to be recognized by Sumneko's extension for what it does as well.

It also introduces the "OnInit.module" syntax, as well.

Lua:
if Debug then Debug.beginFile "Total Initialization" end
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
-- Total Initialization version 5.3 Preview
-- Created by: Bribe
-- Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
-- Inspiration: Almia, ScorpioT1000, Troll-Brain
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
---@class OnInit
---@field overload  OnInitFunc
---@field root      OnInitFunc
---@field config    OnInitFunc
---@field main      OnInitFunc
---@field global    OnInitFunc
---@field trig      OnInitFunc
---@field map       OnInitFunc
---@field final     OnInitFunc
---@field module    OnInitFunc
OnInit = {}

---@alias OnInitCallback fun(require?: Require):any?
---@alias OnInitFunc fun(initCallback_or_libraryName: OnInitCallback|string, initCallback?: OnInitCallback, debugLineNum?: integer)

---@generic OnInitClass
---@alias OnInitRequirement async fun(requirementName:`OnInitClass`):OnInitClass

--"Require" only works within an OnInit callback.
--
--Syntax for strict requirements that throw errors if not found: Require "SomeLibrary"
--
--Syntax for requirements that give up if the required library or variable are not found: Require.optionally "SomeLibrary"
---@class Require: { [string]: OnInitRequirement }
---@field overload OnInitRequirement
Require = {}

do
    local library = {} --You can change this to false if you don't use "Require" nor the OnInit.library API.

    ---@diagnostic disable: global-in-nil-env, assign-type-mismatch

    --CONFIGURABLE LEGACY API FUNCTION:
    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 call   = try or pcall --'try' is extremely useful; found on https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/post-3552846
    local fCall  = library and function(...) coroutine.wrap(call)(...) end or call

    local initFuncQueue = {}
    local function runInitializers(name, continue)
        if initFuncQueue[name] then
            for _,func in ipairs(initFuncQueue[name]) do
                fCall(func, Require)
            end
            initFuncQueue[name] = nil
        end
        if library  then library:resume() end
        if continue then continue()       end
    end
    do
        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.yielded) do
                    func(nil, true) --run errors for missing requirements.
                end
                for _,func in pairs(library.modules) do
                    func(true) --run errors for modules that aren't required.
                end
            end
            OnInit=nil;Require=nil
        end)
    end
    local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
        if not func then
            func = libraryName
        else
            assert(type(libraryName)=="string")
            if debugLineNum and Debug then
                Debug.beginFile(libraryName, incDebugLevel and 7 or 6, debugLineNum)
            end
            if library then
                func = library:create(libraryName, func)
            end
        end
        assert(type(func) == "function")
        initFuncQueue[initName] = initFuncQueue[initName] or {}
        insert(initFuncQueue[initName], func)
        if initName == "root" then
            runInitializers "root"
        end
    end
    local function createInit(name)
        ---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 "LibraryName" or Require.optionally "LibraryName". Alternatively, the callback function can take
        ---the "Require" table as a single parameter: OnInit(function(import) import "ThisIsTheSameAsRequire" end).
        ---
        ---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.
        ---@param libraryNameOrInitFunc string|fun(require?:Require):any
        ---@param userInitFunc? fun(require?:Require):any
        ---@param debugLineNum? integer
        ---@param incDebugLevel? boolean
        return function(libraryNameOrInitFunc, userInitFunc, debugLineNum, incDebugLevel)
            addUserFunc(name, libraryNameOrInitFunc, userInitFunc, debugLineNum, incDebugLevel)
        end
    end
    OnInit.global = createInit "InitGlobals"
    OnInit.trig   = createInit "InitCustomTriggers"
    OnInit.map    = createInit "RunInitializationTriggers"
    OnInit.final  = createInit "MarkGameStarted"

    setmetatable(OnInit, {__call = function(self, libraryNameOrInitFunc, userInitFunc, debugLineNum)
        if userInitFunc or type(libraryNameOrInitFunc)=="function" then
            self.global(libraryNameOrInitFunc, userInitFunc, debugLineNum, true) --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
        elseif libraryNameOrInitFunc == "end" then
            return Debug and Debug.getLine(3)
        elseif library then
            library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
        else
            error("Bad OnInit args: "..tostring(libraryNameOrInitFunc) .. ", " .. tostring(userInitFunc))
        end
    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 ___newindex = gmt.__newindex or rawset
        local newIndex
        function newIndex(g, key, val)
            if key == "main" or key == "config" then
                if key == "main" then
                    runInitializers "root"
                end
                ___newindex(g, key, function()
                    if key == "main" and gmt.__newindex == newIndex then
                        gmt.__newindex = ___newindex --restore the original __newindex if no further hooks on __newindex exist.
                    end
                    runInitializers(key)
                    val()
                end)
            else
                ___newindex(g, key, val)
            end
        end
        gmt.__newindex = newIndex
        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.packed   = {}
        library.yielded  = {}
        library.declared = {}
        library.modules  = {}
        function library:pack(name, ...) self.packed[name] = table.pack(...) end
        function library:resume()
            if self.yielded[1] then
                local continue, tempQueue, forceOptional
                ::initLibraries::
                repeat
                    continue=false
                    self.yielded, tempQueue = {}, self.yielded
                    
                    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.yielded, 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.yielded[1]
                if self.declared[1] then
                    self.declared, tempQueue = {}, self.declared
                    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.packed[name]==nil)
            library.packed[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.packed[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.declared, function() coroutine.resume(co) end)
            coroutine.yield(co) --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), subReq --If the container is nil, yield until it is not.
                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)
                if not package and not explicitSource then
                    if library.modules[requirement] then
                        library.modules[requirement]()
                    end
                    package = library.packed[requirement]
                    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.yielded, checkReqs)
                if coroutine.yield(co) 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
        setmetatable(Require, { __call = processRequirement, function() return processRequirement end })

        local module  = createInit "module"

        ---@param name string
        ---@param func? fun(require?:Require):any
        ---@param debugLineNum? integer
        OnInit.module = function(name, func, debugLineNum)
            if func then
                local userFunc = func
                func = function(require)
                    local co = coroutine.running()
                    library.modules[name] = function()
                        library.modules[name] = nil
                        coroutine.resume(co)
                    end
                    if coroutine.yield() then
                        error("Module declared but not required: "..name) --works similarly to Go; if you don't need a module, then don't include it in your map.
                    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.
        ---@param initList table|string
        ---@param userFunc function
        ---@async
        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
end
if Debug then Debug.endFile() end
 
Level 23
Joined
Jun 26, 2020
Messages
1,838
This update preview should fix the problem with syntax highlighting. I think I've finally found out how to enable the "Require" function to be recognized by Sumneko's extension for what it does as well.

It also introduces the "OnInit.module" syntax, as well.

Lua:
if Debug then Debug.beginFile "Total Initialization" end
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
-- Total Initialization version 5.3 Preview
-- Created by: Bribe
-- Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
-- Inspiration: Almia, ScorpioT1000, Troll-Brain
--β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
---@class OnInit
---@field overload  OnInitFunc
---@field root      OnInitFunc
---@field config    OnInitFunc
---@field main      OnInitFunc
---@field global    OnInitFunc
---@field trig      OnInitFunc
---@field map       OnInitFunc
---@field final     OnInitFunc
---@field module    OnInitFunc
OnInit = {}

---@alias OnInitCallback fun(require?: Require):any?
---@alias OnInitFunc fun(initCallback_or_libraryName: OnInitCallback|string, initCallback?: OnInitCallback, debugLineNum?: integer)

---@generic OnInitClass
---@alias OnInitRequirement async fun(requirementName:`OnInitClass`):OnInitClass

--"Require" only works within an OnInit callback.
--
--Syntax for strict requirements that throw errors if not found: Require "SomeLibrary"
--
--Syntax for requirements that give up if the required library or variable are not found: Require.optionally "SomeLibrary"
---@class Require: { [string]: OnInitRequirement }
---@field overload OnInitRequirement
Require = {}

do
    local library = {} --You can change this to false if you don't use "Require" nor the OnInit.library API.

    ---@diagnostic disable: global-in-nil-env, assign-type-mismatch

    --CONFIGURABLE LEGACY API FUNCTION:
    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 call   = try or pcall --'try' is extremely useful; found on https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/post-3552846
    local fCall  = library and function(...) coroutine.wrap(call)(...) end or call

    local initFuncQueue = {}
    local function runInitializers(name, continue)
        if initFuncQueue[name] then
            for _,func in ipairs(initFuncQueue[name]) do
                fCall(func, Require)
            end
            initFuncQueue[name] = nil
        end
        if library  then library:resume() end
        if continue then continue()       end
    end
    do
        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.yielded) do
                    func(nil, true) --run errors for missing requirements.
                end
                for _,func in pairs(library.modules) do
                    func(true) --run errors for modules that aren't required.
                end
            end
            OnInit=nil;Require=nil
        end)
    end
    local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
        if not func then
            func = libraryName
        else
            assert(type(libraryName)=="string")
            if debugLineNum and Debug then
                Debug.beginFile(libraryName, incDebugLevel and 7 or 6, debugLineNum)
            end
            if library then
                func = library:create(libraryName, func)
            end
        end
        assert(type(func) == "function")
        initFuncQueue[initName] = initFuncQueue[initName] or {}
        insert(initFuncQueue[initName], func)
        if initName == "root" then
            runInitializers "root"
        end
    end
    local function createInit(name)
        ---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 "LibraryName" or Require.optionally "LibraryName". Alternatively, the callback function can take
        ---the "Require" table as a single parameter: OnInit(function(import) import "ThisIsTheSameAsRequire" end).
        ---
        ---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.
        ---@param libraryNameOrInitFunc string|fun(require?:Require):any
        ---@param userInitFunc? fun(require?:Require):any
        ---@param debugLineNum? integer
        ---@param incDebugLevel? boolean
        return function(libraryNameOrInitFunc, userInitFunc, debugLineNum, incDebugLevel)
            addUserFunc(name, libraryNameOrInitFunc, userInitFunc, debugLineNum, incDebugLevel)
        end
    end
    OnInit.global = createInit "InitGlobals"
    OnInit.trig   = createInit "InitCustomTriggers"
    OnInit.map    = createInit "RunInitializationTriggers"
    OnInit.final  = createInit "MarkGameStarted"

    setmetatable(OnInit, {__call = function(self, libraryNameOrInitFunc, userInitFunc, debugLineNum)
        if userInitFunc or type(libraryNameOrInitFunc)=="function" then
            self.global(libraryNameOrInitFunc, userInitFunc, debugLineNum, true) --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
        elseif libraryNameOrInitFunc == "end" then
            return Debug and Debug.getLine(3)
        elseif library then
            library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
        else
            error("Bad OnInit args: "..tostring(libraryNameOrInitFunc) .. ", " .. tostring(userInitFunc))
        end
    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 ___newindex = gmt.__newindex or rawset
        local newIndex
        function newIndex(g, key, val)
            if key == "main" or key == "config" then
                if key == "main" then
                    runInitializers "root"
                end
                ___newindex(g, key, function()
                    if key == "main" and gmt.__newindex == newIndex then
                        gmt.__newindex = ___newindex --restore the original __newindex if no further hooks on __newindex exist.
                    end
                    runInitializers(key)
                    val()
                end)
            else
                ___newindex(g, key, val)
            end
        end
        gmt.__newindex = newIndex
        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.packed   = {}
        library.yielded  = {}
        library.declared = {}
        library.modules  = {}
        function library:pack(name, ...) self.packed[name] = table.pack(...) end
        function library:resume()
            if self.yielded[1] then
                local continue, tempQueue, forceOptional
                ::initLibraries::
                repeat
                    continue=false
                    self.yielded, tempQueue = {}, self.yielded
                   
                    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.yielded, 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.yielded[1]
                if self.declared[1] then
                    self.declared, tempQueue = {}, self.declared
                    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.packed[name]==nil)
            library.packed[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.packed[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.declared, function() coroutine.resume(co) end)
            coroutine.yield(co) --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), subReq --If the container is nil, yield until it is not.
                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)
                if not package and not explicitSource then
                    if library.modules[requirement] then
                        library.modules[requirement]()
                    end
                    package = library.packed[requirement]
                    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.yielded, checkReqs)
                if coroutine.yield(co) 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
        setmetatable(Require, { __call = processRequirement, function() return processRequirement end })

        local module  = createInit "module"

        ---@param name string
        ---@param func? fun(require?:Require):any
        ---@param debugLineNum? integer
        OnInit.module = function(name, func, debugLineNum)
            if func then
                local userFunc = func
                func = function(require)
                    local co = coroutine.running()
                    library.modules[name] = function()
                        library.modules[name] = nil
                        coroutine.resume(co)
                    end
                    if coroutine.yield() then
                        error("Module declared but not required: "..name) --works similarly to Go; if you don't need a module, then don't include it in your map.
                    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.
        ---@param initList table|string
        ---@param userFunc function
        ---@async
        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
end
if Debug then Debug.endFile() end
Is funny because I just did Require = require to not only be "recognized" by the sumneko extension, but also to be redirected to file were is the library I wanna import if it has the same name.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Total Initialization 5.3 is here.
  • Removed (most) Debug API handling from within the script. Debug Utils v2 implements the Debugging for coroutines automatically now.
  • Removed OnInit'end' as a valid third parameter. If you want this functionality, use Debug.getLine().
  • Added OnInit.module() syntax for declaring a resource which will only load if and when it is required. If it is not required, an error will occur informing the user accordingly.
  • Major improvements to linting and syntax completion/suggestions. It is not perfect, but it is pretty good.
  • This is intended to be a stable release. I really, really don't want to work on this anymore, and want to focus on my other projects. If there are any bugs, please let me know as soon as possible, so I can hurry along with the fix and move on.
 
Level 20
Joined
Jul 10, 2009
Messages
474
Bug Report:
Lua:
if debugLineNum and Debug then
    Debug.beginFile(libraryName, incDebugLevel and 7 or 6)
    Debug.data.sourceMap[#Debug.data.sourceMap].lastLine = debugLineNum
end
needs to be Debug.beginFile(libraryName, incDebugLevel and 3 or 2, debugLineNum).
depth starts at 0.
Also, you write directly into the internal data structure of Debug Utils to achieve your goal of defining files within OnInit(). That is dangerous, as future changes to Debug Utils might break your resource as a whole.

Edit: I also suggest using "TotalInitialization" as file name. It looks better in stack traces, if it doesn't have a space.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
@Eikonium, I have made the changes proposed in your message here:

I had written out the long file path of Debug.data.sourceMap because you didn't include access for me to specify a debugLineNum in Debug.beginFile (whereas in my edit this functionality had been included). I can't just simply ask a user to write Debug.endFile() at the bottom of their script outside of the OnInit callback, unless I also ask them to include Debug.beginFile "FileName" at the top. As it is, I already removed the weird OnInit"end" syntax so as to allow readability to flow better.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
474
I know, I had missed the necessary change in DebugUtils 2.0, but I've hotfixed it in 2.0a, before I wrote my bug report. You can now use the API.
In any case, writing to Debug.data.sourceMap is waay too risky in relation to what you want to achieve . If I happen to rename sourceMap in the next version of Debug Utils, Total Initialization will break for all users. That would be terrible.

Btw. your Github version (broken link) currently uses 4 or 3 instead of 3 or 2. Have you tested that? I definitely needed the second to make it work in my tests. But I don't understand incDebugLevel, so it might just be me not using it properly.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
I know, I had missed the necessary change in DebugUtils 2.0, but I've hotfixed it in 2.0a, before I wrote my bug report. You can now use the API.
In any case, writing to Debug.data.sourceMap is waay to risky in relation to what you want to achieve . If I happen to rename sourceMap in the next version of Debug Utils, Total Initialization will break for all users. That would be terrible.

Btw. your Github version (broken link) currently uses 4 or 3 instead of 3 or 2. Have you tested that? I definitely needed the second to make it work in my tests. But I don't understand incDebugLevel, so it might just be me not using it properly.
Thanks for reporting the broken link!

Oh, I see you are doing +1 from within Debug.beginFile now. I've fixed that up now.

I've also converted the main post to a markdown file on GitHub: Lua/Total Initialization.md at master Β· BribeFromTheHive/Lua

It is way easier to maintain things this way, and commit all changes from within vsCode. Maintaining code and documentation on THW is getting really long in the tooth for me, as I hardly have any time to work on this side project anymore.

Btw, do you have a Git-friendly tracking of Debug Utils (e.g. on GitHub)? I was trying to comb through your new version for changes, because on my side I kept having to add a bunch of diagnostic-disable lines for the undeclared globals (since I don't have Debug Console in my directory, whereas you do).
 
Level 20
Joined
Jul 10, 2009
Messages
474
Oh, I see you are doing +1 from within Debug.beginFile now
Was always that way, if I'm not mistaken :) But I think you had your own implementation before that was taking 3+ ?
Btw, do you have a Git-friendly tracking of Debug Utils (e.g. on GitHub)? I was trying to comb through your new version for changes, because on my side I kept having to add a bunch of diagnostic-disable lines for the undeclared globals (since I don't have Debug Console in my directory, whereas you do).
I have a private repo that contains Debug Utils plus the map I'm working on. As such, I'd like to keep it private for the time being. I maybe open another repo for my public resources in the future.

Did you know you can compare versions in VS Code without git?
Select two files, right click and choose "Compare Selected".
1673830930746.png


Maintaining code and documentation on THW is getting really long in the tooth for me
I know what you mean. Releasing a new version is a tedious process. Hive could benefit from git support, but I reckon Ralle has more important To-Do's to work on :)
 

Uncle

Warcraft Moderator
Level 63
Joined
Aug 10, 2018
Messages
6,456
What would be unsafe?
Safe was the wrong word to use, I was having trouble getting it to work but it's all good now, I probably should've just deleted my post, sorry.

Edit: Is it possible to order the init functions inside of the Lite version? If not, is order determined by where you place your Lua scripts in the Trigger Editor (top to bottom?).
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Update - bugs were fixed, some internal variable names were changed. Overall, I am not too pleased with this resource, as it doesn't play ball with Sumneko's extension as well as native 'require' statements would. I think what is needed is to make this type of system part of the build process for Lua development, but injecting this type of wrapper around the code before inserting it into the war3map.lua file (same with Debug Utils for that matter). Anyway, hope this clears up all the bugs.

I've also got the unit tests set up for ZeroBrane Studio here: Lua/tests/Total Init tests.lua at master Β· BribeFromTheHive/Lua
 
Level 6
Joined
Jun 30, 2017
Messages
41
I made something that I think is a useful addition to this system.

Basically, I found that wrapping entire scripts in OnInit.module calls is a iron-clad way of avoiding the good old code-invalid-therefore-you're-booted-to-main-menu issue that WC3 does. Like this:
Lua:
if Debug then Debug.beginFile "Test" end
OnInit.module("Test", function(require)
    --- script content goes here...
end)
if Debug then Debug.endFile() end

However, If I wanted something executed at a certain point in init phase, I would have to use .global, .final, .somethingElse instead of .module. You also can't do something like this:
Lua:
if Debug then Debug.beginFile "Test" end
OnInit.module("Test", function(require)

    -- script content

    OnInit.global(function(require)
        -- global init phase for script
    end)
end)
if Debug then Debug.endFile() end

Because of module, the script can execute past the global phase making it so that OnInit.global has no effect.

With the following changes, I made it so that initializer calls past their init stage will immediately call the init callback, therefore fixing the problem of OnInit.global callback inside OnInit.module sometimes not executing.

1707067421271.png
1707067437049.png


PS: There also seemed to have been an issue with EmmyLua annotation regarding the OnInit.module, and I think this fixes it:
1707067493481.png


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 = {} ---@type table<integer, fun(require: Require)>

    ---@param libraryName    string | Initializer.Callback
    ---@param func?          Initializer.Callback
    ---@param debugLineNum?  integer
    local function callUserFunc(libraryName, func, debugLineNum)
        if not func then
            ---@cast libraryName Initializer.Callback
            func = libraryName
        else
            assert(type(libraryName) == 'string')
            if debugLineNum and Debug then
                Debug.beginFile(libraryName, 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)

        coroutine.wrap(func)(Require)
        if library then
            library:resume()
        end
    end

    local initKeyNames = {
        root = 'root',
        config = 'config',
        main = 'main',
        ['InitGlobals'] = 'global',
        ['InitCustomTriggers'] = 'trig',
        ['RunInitializationTriggers'] = 'map',
        ['MarkGameStarted'] = 'final'
    }

    ---@param name string
    ---@param continue? function
    local function runInitializers(name, continue)
        --print('running:', name, tostring(initFuncQueue[name]))
        if name ~= 'module' and name ~= 'library' then
            OnInit[initKeyNames[name]] = callUserFunc
        end

        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          Initializer.Callback
        ---@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
 
Top