- Joined
- Sep 26, 2009
- Messages
- 9,534
Total Initialization
Special Thanks:
For laying the framework for requirements in WarCraft 3 Lua:
Why not just use do...end blocks in the Lua root?
While Lua only has the Lua root for initialization, Lua can use run-time hooks on the InitBlizzard function in order to postpone its own code until heavy WarCraft 3 natives need to be called.
First, let me show you the sequence of initialization that JassHelper used:
There were two very key problems with JassHelper's initializer:
Basic API for initializer functions:
Note: You can optionally include a string as an argument to give your initializer a name. This is useful in two scenarios:
The Code:
Prints (with the help of the "try" function):
Use the following site to test: JDoodle - Online Compiler, Editor for Java, C/C++, etc
Ensure you copy the latest version of Total Initialization into the designated part of the script.
If you don't have any need of libraries or requirements, you might find this "Lite" version to be more your taste:
-
Allows you to initialize your script in a safe, easy-to-debug environment.
-
Allows your map's triggers to be arranged in any order* (similar to JassHelper).
-
Enables functionality similar to Lua modules and vJass libraries.
-
Provides helpful tools to hook into certain key points in the initialization process (e.g. once all udg_ globals are declared, or once the game has started).
Special Thanks:
- @Eikonium for the "try" function and for challenging bad API approaches, leading me to discovering far better API for this resource.
- @HerlySQR for GetStackTrace, which makes debugging a much more straightforward process.
- @Tasyen for helping me to better understand the "main" function, and for discovering MarkGameStarted can be hooked for OnInit.final's needs.
- @Luashine for showing how I can implement OnInit.config, which - in turn - led to an actual OnInit.main (previous attempts had failed)
- @Forsakn and Troll-Brain for help with early debugging (primarily with the pairs desync issue)
For laying the framework for requirements in WarCraft 3 Lua:
- @Almia's [Lua] Module System
- @ScorpioT1000's wc3-wlpm-module-manager/wlpm-module-manager.lua at master · Indaxia/wc3-wlpm-module-manager
- @Troll-Brain's lua require ersatz
Why not just use do...end blocks in the Lua root?
While Lua only has the Lua root for initialization, Lua can use run-time hooks on the InitBlizzard function in order to postpone its own code until heavy WarCraft 3 natives need to be called.
- Creating WarCraft 3 objects in the Lua root is dangerous as it causes desyncs.
- The Lua root is an unstable place to initialize (e.g. it doesn't allow "print", which makes debugging extremely difficult)
- do...end blocks force you to organize your triggers from top to bottom based on their requirements.
- The Lua root is not split into separate pcalls, which means that failures can easily crash the entire loading sequence without showing an error message.
- The Lua root is not yieldable, which means you need to do everything immediately or hook onto something like InitBlizzard or MarkGameStarted to await these loading steps.
First, let me show you the sequence of initialization that JassHelper used:
There were two very key problems with JassHelper's initializer:
- Initialization precedence was Module > Struct > Library > Requirement, rather than Requirement > Module > Struct > Library. This created a problem with required libraries having not been loaded in time for a module to depend on them, meaning that everything in the matured vJass era needed to be initialized with a module in order to circumvent the problem.
- The initializers ran before InitGlobals, so it created a permanent rift between the GUI and vJass interfaces by ostracizing one from the other (if vJass changed the value of a udg_ variable, InitGlobals would just overwrite it again).
- The Lua root runs.
- OnInit functions that require nothing - or - already have their requirements fulfilled in the Lua root.
- OnInit functions that have their requirements fulfilled based on other OnInit declarations.
- OnInit "custom" initializers run sequentially, prolonging the initialization queue.
- Repeat step 2-4 until all executables are loaded and all subsequent initializers have run.
- OnInit.final is the final initializer, which is called after the loading screen has transitioned into the actual game screen.
- Display error messages for missing requirements.
Basic API for initializer functions:
Lua:
OnInit.root(function()
print "This is called immediately"
end)
OnInit.config(function()
print "This is called during the map config process (in game lobby)"
end)
OnInit.main(function()
print "This is called during the loading screen"
end)
OnInit(function()
print "All udg_ variables have been initialized"
end)
OnInit.trig(function()
print "All InitTrig_ functions have been called"
end)
OnInit.map(function()
print "All Map Initialization events have run"
end)
OnInit.final(function()
print "The game has now started"
end)
- If you don't add anything to the global API but want it to be useful as a requirment.
- If you want it to be accurately defined for initializers that optionally require it.
local someLibrary = Require "SomeLibrary"
- Functions similarly to Lua's built-in (but disabled in WarCraft 3) "require" function, provided that you use it from an OnInit callback function.
- Returns either the return value of the OnInit function that declared its name as "SomeLibrary" or will return _G["SomeLibrary"] or just "true" if that named library was initialized but did not return anything nor add itself to the _G table.
- Will throw an error if the requirement was not found by the time the map has fully loaded.
- Can also require elements of a table: Require "table.property.subProperty"
local optionalRequirement = Require.optionally "OptionalRequirement"
- Similar to the Require method, but will only wait if the optional requirement was already declared as an uninitialized library. This name can be whatever suits you (optional/lazy/nonStrict), since it uses the __index method rather than limit itself to one keyword.
The Code:
Lua:
if Debug then Debug.beginFile 'TotalInitialization' end
--[[——————————————————————————————————————————————————————
Total Initialization version 5.3.1
Created by: Bribe
Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
Inspiration: Almia, ScorpioT1000, Troll-Brain
Hosted at: https://github.com/BribeFromTheHive/Lua/blob/master/TotalInitialization.lua
Debug library hosted at: https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
————————————————————————————————————————————————————————————]]
---Calls the user's initialization function during the map's loading process. The first argument should either be the init function,
---or it should be the string to give the initializer a name (works similarly to a module name/identically to a vJass library name).
---
---To use requirements, call `Require.strict 'LibraryName'` or `Require.optional 'LibraryName'`. Alternatively, the OnInit callback
---function can take the `Require` table as a single parameter: `OnInit(function(import) import.strict 'ThisIsTheSameAsRequire' end)`.
---
-- - `OnInit.global` or just `OnInit` is called after InitGlobals and is the standard point to initialize.
-- - `OnInit.trig` is called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
-- - `OnInit.map` is the last point in initialization before the loading screen is completed.
-- - `OnInit.final` occurs immediately after the loading screen has disappeared, and the game has started.
---@class OnInit
--
--Simple Initialization without declaring a library name:
---@overload async fun(initCallback: Initializer.Callback)
--
--Advanced initialization with a library name and an optional third argument to signal to Eikonium's DebugUtils that the file has ended.
---@overload async fun(libraryName: string, initCallback: Initializer.Callback, debugLineNum?: integer)
--
--A way to yield your library to allow other libraries in the same initialization sequence to load, then resume once they have loaded.
---@overload async fun(customInitializerName: string)
OnInit = {}
---@alias Initializer.Callback fun(require?: Requirement | {[string]: Requirement}):...?
---@alias Requirement async fun(reqName: string, source?: table): unknown
-- `Require` will yield the calling `OnInit` initialization function until the requirement (referenced as a string) exists. It will check the
-- global API (for example, does 'GlobalRemap' exist) and then check for any named OnInit resources which might use that same string as its name.
--
-- Due to the way Sumneko's syntax highlighter works, the return value will only be linted for defined @class objects (and doesn't work for regular
-- globals like `TimerStart`). I tried to request the functionality here: https://github.com/sumneko/lua-language-server/issues/1792 , however it
-- was closed. Presumably, there are other requests asking for it, but I wouldn't count on it.
--
-- To declare a requirement, use: `Require.strict 'SomeLibrary'` or (if you don't care about the missing linting functionality) `Require 'SomeLibrary'`
--
-- To optionally require something, use any other suffix (such as `.optionally` or `.nonstrict`): `Require.optional 'SomeLibrary'`
--
---@class Require: { [string]: Requirement }
---@overload async fun(reqName: string, source?: table): string
Require = {}
do
local library = {} --You can change this to false if you don't use `Require` nor the `OnInit.library` API.
--CONFIGURABLE LEGACY API FUNCTION:
---@param _ENV table
---@param OnInit any
local function assignLegacyAPI(_ENV, OnInit)
OnGlobalInit = OnInit; OnTrigInit = OnInit.trig; OnMapInit = OnInit.map; OnGameStart = OnInit.final --Global Initialization Lite API
--OnMainInit = OnInit.main; OnLibraryInit = OnInit.library; OnGameInit = OnInit.final --short-lived experimental API
--onGlobalInit = OnInit; onTriggerInit = OnInit.trig; onInitialization = OnInit.map; onGameStart = OnInit.final --original Global Initialization API
--OnTriggerInit = OnInit.trig; OnInitialization = OnInit.map --Forsakn's Ordered Indices API
end
--END CONFIGURABLES
local _G, rawget, insert =
_G, rawget, table.insert
local initFuncQueue = {}
---@param name string
---@param continue? function
local function runInitializers(name, continue)
--print('running:', name, tostring(initFuncQueue[name]))
if initFuncQueue[name] then
for _,func in ipairs(initFuncQueue[name]) do
coroutine.wrap(func)(Require)
end
initFuncQueue[name] = nil
end
if library then
library:resume()
end
if continue then
continue()
end
end
local function initEverything()
---@param hookName string
---@param continue? function
local function hook(hookName, continue)
local hookedFunc = rawget(_G, hookName)
if hookedFunc then
rawset(_G, hookName,
function()
hookedFunc()
runInitializers(hookName, continue)
end
)
else
runInitializers(hookName, continue)
end
end
hook(
'InitGlobals',
function()
hook(
'InitCustomTriggers',
function()
hook('RunInitializationTriggers')
end
)
end
)
hook(
'MarkGameStarted',
function()
if library then
for _,func in ipairs(library.queuedInitializerList) do
func(nil, true) --run errors for missing requirements.
end
for _,func in pairs(library.yieldedModuleMatrix) do
func(true) --run errors for modules that aren't required.
end
end
OnInit = nil
Require = nil
end
)
end
---@param initName string
---@param libraryName string | Initializer.Callback
---@param func? Initializer.Callback
---@param debugLineNum? integer
---@param incDebugLevel? boolean
local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
if not func then
---@cast libraryName Initializer.Callback
func = libraryName
else
assert(type(libraryName) == 'string')
if debugLineNum and Debug then
Debug.beginFile(libraryName, incDebugLevel and 3 or 2)
Debug.data.sourceMap[#Debug.data.sourceMap].lastLine = debugLineNum
end
if library then
func = library:create(libraryName, func)
end
end
assert(type(func) == 'function')
--print('adding user func: ' , initName , libraryName, debugLineNum, incDebugLevel)
initFuncQueue[initName] = initFuncQueue[initName] or {}
insert(initFuncQueue[initName], func)
if initName == 'root' or initName == 'module' then
runInitializers(initName)
end
end
---@param name string
local function createInit(name)
---@async
---@param libraryName string --Assign your callback a unique name, allowing other OnInit callbacks can use it as a requirement.
---@param userInitFunc Initializer.Callback --Define a function to be called at the chosen point in the initialization process. It can optionally take the `Require` object as a parameter. Its optional return value(s) are passed to a requiring library via the `Require` object (defaults to `true`).
---@param debugLineNum? integer --If the Debug library is present, you can call Debug.getLine() for this parameter (which should coincide with the last line of your script file). This will neatly tie-in with OnInit's built-in Debug library functionality to define a starting line and an ending line for your module.
---@overload async fun(userInitFunc: Initializer.Callback)
return function(libraryName, userInitFunc, debugLineNum)
addUserFunc(name, libraryName, userInitFunc, debugLineNum)
end
end
OnInit.global = createInit 'InitGlobals' -- Called after InitGlobals, and is the standard point to initialize.
OnInit.trig = createInit 'InitCustomTriggers' -- Called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
OnInit.map = createInit 'RunInitializationTriggers' -- Called last in the script's loading screen sequence. Runs after the GUI "Map Initialization" events have run.
OnInit.final = createInit 'MarkGameStarted' -- Called immediately after the loading screen has disappeared, and the game has started.
do
---@param self table
---@param libraryNameOrInitFunc function | string
---@param userInitFunc function
---@param debugLineNum number
local function __call(
self,
libraryNameOrInitFunc,
userInitFunc,
debugLineNum
)
if userInitFunc or type(libraryNameOrInitFunc) == 'function' then
addUserFunc(
'InitGlobals', --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
libraryNameOrInitFunc,
userInitFunc,
debugLineNum,
true
)
elseif library then
library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
else
error(
"Bad OnInit args: "..
tostring(libraryNameOrInitFunc) .. ", " ..
tostring(userInitFunc)
)
end
end
setmetatable(OnInit --[[@as table]], { __call = __call })
end
do --if you don't need the initializers for 'root', 'config' and 'main', you can delete this do...end block.
local gmt = getmetatable(_G) or
getmetatable(setmetatable(_G, {}))
local rawIndex = gmt.__newindex or rawset
local hookMainAndConfig
---@param _G table
---@param key string
---@param fnOrDiscard unknown
function hookMainAndConfig(_G, key, fnOrDiscard)
if key == 'main' or key == 'config' then
---@cast fnOrDiscard function
if key == 'main' then
runInitializers 'root'
end
rawIndex(_G, key, function()
if key == 'config' then
fnOrDiscard()
elseif gmt.__newindex == hookMainAndConfig then
gmt.__newindex = rawIndex --restore the original __newindex if no further hooks on __newindex exist.
end
runInitializers(key)
if key == 'main' then
fnOrDiscard()
end
end)
else
rawIndex(_G, key, fnOrDiscard)
end
end
gmt.__newindex = hookMainAndConfig
OnInit.root = createInit 'root' -- Runs immediately during the Lua root, but is yieldable (allowing requirements) and pcalled.
OnInit.config = createInit 'config' -- Runs when `config` is called. Credit to @Luashine: https://www.hiveworkshop.com/threads/inject-main-config-from-we-trigger-code-like-jasshelper.338201/
OnInit.main = createInit 'main' -- Runs when `main` is called. Idea from @Tasyen: https://www.hiveworkshop.com/threads/global-initialization.317099/post-3374063
end
if library then
library.queuedInitializerList = {}
library.customDeclarationList = {}
library.yieldedModuleMatrix = {}
library.moduleValueMatrix = {}
function library:pack(name, ...)
self.moduleValueMatrix[name] = table.pack(...)
end
function library:resume()
if self.queuedInitializerList[1] then
local continue, tempQueue, forceOptional
::initLibraries::
repeat
continue=false
self.queuedInitializerList, tempQueue =
{}, self.queuedInitializerList
for _,func in ipairs(tempQueue) do
if func(forceOptional) then
continue=true --Something was initialized; therefore further systems might be able to initialize.
else
insert(self.queuedInitializerList, func) --If the queued initializer returns false, that means its requirement wasn't met, so we re-queue it.
end
end
until not continue or not self.queuedInitializerList[1]
if self.customDeclarationList[1] then
self.customDeclarationList, tempQueue =
{}, self.customDeclarationList
for _,func in ipairs(tempQueue) do
func() --unfreeze any custom initializers.
end
elseif not forceOptional then
forceOptional = true
else
return
end
goto initLibraries
end
end
local function declareName(name, initialValue)
assert(type(name) == 'string')
assert(library.moduleValueMatrix[name] == nil)
library.moduleValueMatrix[name] =
initialValue and { true, n = 1 }
end
function library:create(name, userFunc)
assert(type(userFunc) == 'function')
declareName(name, false) --declare itself as a non-loaded library.
return function()
self:pack(name, userFunc(Require)) --pack return values to allow multiple values to be communicated.
if self.moduleValueMatrix[name].n == 0 then
self:pack(name, true) --No values were returned; therefore simply package the value as `true`
end
end
end
---@async
function library:declare(name)
declareName(name, true) --declare itself as a loaded library.
local co = coroutine.running()
insert(
self.customDeclarationList,
function()
coroutine.resume(co)
end
)
coroutine.yield() --yields the calling function until after all currently-queued initializers have run.
end
local processRequirement
---@async
function processRequirement(
optional,
requirement,
explicitSource
)
if type(optional) == 'string' then
optional, requirement, explicitSource =
true, optional, requirement --optional requirement (processed by the __index method)
else
optional = false --strict requirement (processed by the __call method)
end
local source = explicitSource or _G
assert(type(source)=='table')
assert(type(requirement)=='string')
::reindex::
local subSource, subReq =
requirement:match("([\x25w_]+)\x25.(.+)") --Check if user is requiring using "table.property" syntax
if subSource and subReq then
source,
requirement =
processRequirement(subSource, source), --If the container is nil, yield until it is not.
subReq
if type(source)=='table' then
explicitSource = source
goto reindex --check for further nested properties ("table.property.subProperty.anyOthers").
else
return --The source table for the requirement wasn't found, so disregard the rest (this only happens with optional requirements).
end
end
local function loadRequirement(unpack)
local package = rawget(source, requirement) --check if the requirement exists in the host table.
if not package and not explicitSource then
if library.yieldedModuleMatrix[requirement] then
library.yieldedModuleMatrix[requirement]() --load module if it exists
end
package = library.moduleValueMatrix[requirement] --retrieve the return value from the module.
if unpack and type(package)=='table' then
return table.unpack(package, 1, package.n) --using unpack allows any number of values to be returned by the required library.
end
end
return package
end
local co, loaded
local function checkReqs(forceOptional, printErrors)
if not loaded then
loaded = loadRequirement()
loaded = loaded or optional and
(loaded==nil or forceOptional)
if loaded then
if co then coroutine.resume(co) end --resume only if it was yielded in the first place.
return loaded
elseif printErrors then
coroutine.resume(co, true)
end
end
end
if not checkReqs() then --only yield if the requirement doesn't already exist.
co = coroutine.running()
insert(library.queuedInitializerList, checkReqs)
if coroutine.yield() then
error("Missing Requirement: "..requirement) --handle the error within the user's function to get an accurate stack trace via the `try` function.
end
end
return loadRequirement(true)
end
---@type Requirement
function Require.strict(name, explicitSource)
return processRequirement(nil, name, explicitSource)
end
setmetatable(Require --[[@as table]], {
__call = processRequirement,
__index = function()
return processRequirement
end
})
local module = createInit 'module'
--- `OnInit.module` will only call the OnInit function if the module is required by another resource, rather than being called at a pre-
--- specified point in the loading process. It works similarly to Go, in that including modules in your map that are not actually being
--- required will throw an error message.
---@param name string
---@param func fun(require?: Initializer.Callback):any
---@param debugLineNum? integer
OnInit.module = function(name, func, debugLineNum)
if func then
local userFunc = func
func = function(require)
local co = coroutine.running()
library.yieldedModuleMatrix[name] =
function(failure)
library.yieldedModuleMatrix[name] = nil
coroutine.resume(co, failure)
end
if coroutine.yield() then
error("Module declared but not required: "..name)
end
return userFunc(require)
end
end
module(name, func, debugLineNum)
end
end
if assignLegacyAPI then --This block handles legacy code.
---Allows packaging multiple requirements into one table and queues the initialization for later.
---@deprecated
---@param initList string | table
---@param userFunc function
function OnInit.library(initList, userFunc)
local typeOf = type(initList)
assert(typeOf=='table' or typeOf=='string')
assert(type(userFunc) == 'function')
local function caller(use)
if typeOf=='string' then
use(initList)
else
for _,initName in ipairs(initList) do
use(initName)
end
if initList.optional then
for _,initName in ipairs(initList.optional) do
use.lazily(initName)
end
end
end
end
if initList.name then
OnInit(initList.name, caller)
else
OnInit(caller)
end
end
local legacyTable = {}
assignLegacyAPI(legacyTable, OnInit)
for key,func in pairs(legacyTable) do
rawset(_G, key, func)
end
OnInit.final(function()
for key in pairs(legacyTable) do
rawset(_G, key, nil)
end
end)
end
initEverything()
end
if Debug then Debug.endFile() end
Lua:
OnInit("Yuna", function(needs)
print("Yuna has arrived, thanks to Tidus, who is:",needs "Tidus")
print("Yuna only optionally needs Kimhari, who is:",needs.optionally "Kimahri")
end)
OnInit("Tidus", function(requires)
print("Tidus has loaded, thanks to Jecht, who is:", requires "Jecht")
print "Tidus is declaring Rikku and yielding for initializers that need her."
OnInit "Rikku"
return "The Star Player of the Zanarkand Abes"
end)
OnInit("Cid", function(needs)
print("Cid has arrived, thanks to Rikku, who is:",needs "Rikku")
end)
OnInit(function(braucht)
print("Spira does not exist, so this library will never run.", braucht "Spira")
end)
OnInit("Jecht", function(needs)
print("Jecht requires Blitzball, which is:",needs"Blitzball")
print("Jecht has arrived, without Sin, who is:",needs.readyOrNot "Sin")
return "A Retired Blitzball Player", "An Alcoholic", "A bad father to Tidus"
end)
OnInit("Wakka", function(needs)
print("Wakka requires Blitzball, which is:",needs"Blitzball")
print("Wakka optionally requires Lulu, who is:", needs.ifHeFeelsLikeIt "Lulu")
end)
OnInit("Lulu", function(needs)
print("Lulu optionally requires Wakka, who is:", needs.ifSheFeelsLikeIt "Wakka")
end)
OnInit("Blitzball", function()
print "Blitzball has no requirements."
return "Round", "Popular", "Extremely Dangerous"
end)
OnInit("Bevel", function(import)
print("Bevel will not wait for Zanarkand, which is:", import.sleepily "Zanarkand")
end)
OnInit.trig("Ronso Fangs", function()
print "The Ronso Fangs run last, because they are late bloomers."
end)
Use the following site to test: JDoodle - Online Compiler, Editor for Java, C/C++, etc
Ensure you copy the latest version of Total Initialization into the designated part of the script.
Lua:
DoNothing = function()end
MarkGameStarted = function() bj_gameStarted = true end
InitGlobals = DoNothing --this is always found at the top of the map script, even when there are no udg_ globals
-- Total Initialization script goes here --
-- (write your own unit tests here) --
-- auto-generated functions like "CreateAllUnits" are placed here --
-- map header script is placed here --
-- GUI triggers are placed here --
-- this marks the end of the user's access to the Lua root, everything is auto-generated by World Editor beyond this point --
InitCustomTriggers = DoNothing
RunInitializationTriggers = DoNothing
function main()
InitGlobals()
InitCustomTriggers()
RunInitializationTriggers()
end
config = DoNothing
-- end of Lua root --
config() --called during the game lobby menu/prior to the game's loading screen.
main() --called first during the map loading screen.
MarkGameStarted() --synthesize that the game has loaded.
If you don't have any need of libraries or requirements, you might find this "Lite" version to be more your taste:
Lua:
--Global Initialization 'Lite' by Bribe, with special thanks to Tasyen and Eikonium
--Last updated 11 Nov 2022
do
local addInit
function OnGlobalInit(initFunc) addInit("InitGlobals", initFunc) end -- Runs once all GUI variables are instantiated.
function OnTrigInit (initFunc) addInit("InitCustomTriggers", initFunc) end -- Runs once all InitTrig_ are called.
function OnMapInit (initFunc) addInit("RunInitializationTriggers", initFunc) end -- Runs once all Map Initialization triggers are run.
function OnGameStart (initFunc) addInit("MarkGameStarted", initFunc) end -- Runs once the game has actually started.
do
local initializers = {}
addInit=function(initName, initFunc)
initializers[initName] = initializers[initName] or {}
table.insert(initializers[initName], initFunc)
end
local function init(initName, continue)
if initializers[initName] then
for _,initFunc in ipairs(initializers[initName]) do pcall(initFunc) end
end
if continue then continue() end
end
local function hook(name, continue)
local _name=rawget(_G, name)
if _name then
rawset(_G, name, function()
_name()
init(name, continue) --run the initializer after the hooked handler function has been called.
end)
else
init(name, continue) --run initializer immediately
end
end
hook("InitGlobals",function()
hook("InitCustomTriggers",function() --InitCustomTriggers and RunInitializationTriggers are declared after the users' code,
hook "RunInitializationTriggers" --hence users need to wait until they have been declared.
end)
end)
hook "MarkGameStarted"
end
end
Last edited: